Files
react-navigation/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java
Krzysztof Magiera 8cf82d1cbe Fix layout of header items on Android native stack. (#289)
This change fixes the issue when left item added to native stack header on Android would by default be shifted from the screen edge by some amount. This turned out to be the default config of the native toolbar which applies some padding on the left side. WIn this change we reset that padding to always be 0 to let the position be specified from react. Note that the setting we reset does not influence the position of the native back navigation button as Android does not apply the padding in case navigation back icon is rendered.
2020-01-20 22:21:07 +01:00

318 lines
8.9 KiB
Java

package com.swmansion.rnscreens;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.views.text.ReactFontManager;
import java.util.ArrayList;
public class ScreenStackHeaderConfig extends ViewGroup {
private final ArrayList<ScreenStackHeaderSubview> mConfigSubviews = new ArrayList<>(3);
private String mTitle;
private int mTitleColor;
private String mTitleFontFamily;
private float mTitleFontSize;
private int mBackgroundColor;
private boolean mIsHidden;
private boolean mGestureEnabled = true;
private boolean mIsBackButtonHidden;
private boolean mIsShadowHidden;
private boolean mDestroyed;
private int mTintColor;
private final Toolbar mToolbar;
private boolean mIsAttachedToWindow = false;
private OnClickListener mBackClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
getScreenStack().dismiss(getScreenFragment());
}
};
public ScreenStackHeaderConfig(Context context) {
super(context);
setVisibility(View.GONE);
mToolbar = new Toolbar(context);
// reset content insets to be 0 to allow react position custom navbar views. Note that this does
// not affect platform native back button as toolbar does not apply left inset when navigation
// button is specified
mToolbar.setContentInsetsAbsolute(0, 0);
// set primary color as background by default
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(android.R.attr.colorPrimary, tv, true)) {
mToolbar.setBackgroundColor(tv.data);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// no-op
}
public void destroy() {
mDestroyed = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttachedToWindow = true;
onUpdate();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsAttachedToWindow = false;
}
private Screen getScreen() {
ViewParent screen = getParent();
if (screen instanceof Screen) {
return (Screen) screen;
}
return null;
}
private ScreenStack getScreenStack() {
Screen screen = getScreen();
if (screen != null) {
ScreenContainer container = screen.getContainer();
if (container instanceof ScreenStack) {
return (ScreenStack) container;
}
}
return null;
}
private ScreenStackFragment getScreenFragment() {
ViewParent screen = getParent();
if (screen instanceof Screen) {
Fragment fragment = ((Screen) screen).getFragment();
if (fragment instanceof ScreenStackFragment) {
return (ScreenStackFragment) fragment;
}
}
return null;
}
public boolean isDismissable() {
return mGestureEnabled;
}
public void onUpdate() {
Screen parent = (Screen) getParent();
final ScreenStack stack = getScreenStack();
boolean isRoot = stack == null ? true : stack.getRootScreen() == parent;
boolean isTop = stack == null ? true : stack.getTopScreen() == parent;
if (!mIsAttachedToWindow || !isTop || mDestroyed) {
return;
}
if (mIsHidden) {
if (mToolbar.getParent() != null) {
getScreenFragment().removeToolbar();
}
return;
}
if (mToolbar.getParent() == null) {
getScreenFragment().setToolbar(mToolbar);
}
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
activity.setSupportActionBar(mToolbar);
ActionBar actionBar = activity.getSupportActionBar();
// hide back button
actionBar.setDisplayHomeAsUpEnabled(isRoot ? false : !mIsBackButtonHidden);
// when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites
// navigation click listener. The default behavior set in the wrapper is to call into
// menu options handlers, but we prefer the back handling logic to stay here instead.
mToolbar.setNavigationOnClickListener(mBackClickListener);
// shadow
getScreenFragment().setToolbarShadowHidden(mIsShadowHidden);
// title
actionBar.setTitle(mTitle);
TextView titleTextView = getTitleTextView();
if (mTitleColor != 0) {
mToolbar.setTitleTextColor(mTitleColor);
}
if (titleTextView != null) {
if (mTitleFontFamily != null) {
titleTextView.setTypeface(ReactFontManager.getInstance().getTypeface(
mTitleFontFamily, 0, getContext().getAssets()));
}
if (mTitleFontSize > 0) {
titleTextView.setTextSize(mTitleFontSize);
}
}
// background
if (mBackgroundColor != 0) {
mToolbar.setBackgroundColor(mBackgroundColor);
}
// color
if (mTintColor != 0) {
Drawable navigationIcon = mToolbar.getNavigationIcon();
if (navigationIcon != null) {
navigationIcon.setColorFilter(mTintColor, PorterDuff.Mode.SRC_ATOP);
}
}
// subviews
for (int i = mToolbar.getChildCount() - 1; i >= 0; i--) {
if (mToolbar.getChildAt(i) instanceof ScreenStackHeaderSubview) {
mToolbar.removeViewAt(i);
}
}
for (int i = 0, size = mConfigSubviews.size(); i < size; i++) {
ScreenStackHeaderSubview view = mConfigSubviews.get(i);
ScreenStackHeaderSubview.Type type = view.getType();
if (type == ScreenStackHeaderSubview.Type.BACK) {
// we special case BACK button header config type as we don't add it as a view into toolbar
// but instead just copy the drawable from imageview that's added as a first child to it.
View firstChild = view.getChildAt(0);
if (!(firstChild instanceof ImageView)) {
throw new JSApplicationIllegalArgumentException("Back button header config view should have Image as first child");
}
actionBar.setHomeAsUpIndicator(((ImageView) firstChild).getDrawable());
continue;
}
Toolbar.LayoutParams params =
new Toolbar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
switch (type) {
case LEFT:
// when there is a left item we need to disable navigation icon
// we also hide title as there is no other way to display left side items
mToolbar.setNavigationIcon(null);
mToolbar.setTitle(null);
params.gravity = Gravity.LEFT;
break;
case RIGHT:
params.gravity = Gravity.RIGHT;
break;
case TITLE:
params.width = LayoutParams.MATCH_PARENT;
mToolbar.setTitle(null);
case CENTER:
params.gravity = Gravity.CENTER_HORIZONTAL;
break;
}
view.setLayoutParams(params);
mToolbar.addView(view);
}
}
private void maybeUpdate() {
if (getParent() != null && !mDestroyed) {
onUpdate();
}
}
public ScreenStackHeaderSubview getConfigSubview(int index) {
return mConfigSubviews.get(index);
}
public int getConfigSubviewsCount() {
return mConfigSubviews.size();
}
public void removeConfigSubview(int index) {
mConfigSubviews.remove(index);
maybeUpdate();
}
public void removeAllConfigSubviews() {
mConfigSubviews.clear();
maybeUpdate();
}
public void addConfigSubview(ScreenStackHeaderSubview child, int index) {
mConfigSubviews.add(index, child);
maybeUpdate();
}
private TextView getTitleTextView() {
for (int i = 0, size = mToolbar.getChildCount(); i < size; i++) {
View view = mToolbar.getChildAt(i);
if (view instanceof TextView) {
TextView tv = (TextView) view;
if (tv.getText().equals(mToolbar.getTitle())) {
return tv;
}
}
}
return null;
}
public void setTitle(String title) {
mTitle = title;
}
public void setTitleFontFamily(String titleFontFamily) {
mTitleFontFamily = titleFontFamily;
}
public void setTitleFontSize(float titleFontSize) {
mTitleFontSize = titleFontSize;
}
public void setTitleColor(int color) {
mTitleColor = color;
}
public void setTintColor(int color) {
mTintColor = color;
}
public void setBackgroundColor(int color) {
mBackgroundColor = color;
}
public void setHideShadow(boolean hideShadow) {
mIsShadowHidden = hideShadow;
}
public void setGestureEnabled(boolean gestureEnabled) {
mGestureEnabled = gestureEnabled;
}
public void setHideBackButton(boolean hideBackButton) {
mIsBackButtonHidden = hideBackButton;
}
public void setHidden(boolean hidden) {
mIsHidden = hidden;
}
}