// Copyright 2004-present Facebook. All Rights Reserved. package com.facebook.react.views.toolbar; import javax.annotation.Nullable; import android.content.Context; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.controller.BaseControllerListener; import com.facebook.drawee.controller.ControllerListener; import com.facebook.drawee.drawable.ScalingUtils; import com.facebook.drawee.generic.GenericDraweeHierarchy; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.view.DraweeHolder; import com.facebook.drawee.view.MultiDraweeHolder; import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; /** * Custom implementation of the {@link Toolbar} widget that adds support for remote images in logo * and navigationIcon using fresco. */ public class ReactToolbar extends Toolbar { private static final String PROP_ACTION_ICON = "icon"; private static final String PROP_ACTION_SHOW = "show"; private static final String PROP_ACTION_SHOW_WITH_TEXT = "showWithText"; private static final String PROP_ACTION_TITLE = "title"; private final DraweeHolder mLogoHolder; private final DraweeHolder mNavIconHolder; private final DraweeHolder mOverflowIconHolder; private final MultiDraweeHolder<GenericDraweeHierarchy> mActionsHolder = new MultiDraweeHolder<>(); private final ControllerListener<ImageInfo> mLogoControllerListener = new BaseControllerListener<ImageInfo>() { @Override public void onFinalImageSet( String id, @Nullable final ImageInfo imageInfo, @Nullable Animatable animatable) { if (imageInfo != null) { final DrawableWithIntrinsicSize logoDrawable = new DrawableWithIntrinsicSize(mLogoHolder.getTopLevelDrawable(), imageInfo); setLogo(logoDrawable); } } }; private final ControllerListener<ImageInfo> mNavIconControllerListener = new BaseControllerListener<ImageInfo>() { @Override public void onFinalImageSet( String id, @Nullable final ImageInfo imageInfo, @Nullable Animatable animatable) { if (imageInfo != null) { final DrawableWithIntrinsicSize navIconDrawable = new DrawableWithIntrinsicSize(mNavIconHolder.getTopLevelDrawable(), imageInfo); setNavigationIcon(navIconDrawable); } } }; private final ControllerListener<ImageInfo> mOverflowIconControllerListener = new BaseControllerListener<ImageInfo>() { @Override public void onFinalImageSet( String id, @Nullable final ImageInfo imageInfo, @Nullable Animatable animatable) { if (imageInfo != null) { final DrawableWithIntrinsicSize overflowIconDrawable = new DrawableWithIntrinsicSize(mOverflowIconHolder.getTopLevelDrawable(), imageInfo); setOverflowIcon(overflowIconDrawable); } } }; private static class ActionIconControllerListener extends BaseControllerListener<ImageInfo> { private final MenuItem mItem; private final DraweeHolder mHolder; ActionIconControllerListener(MenuItem item, DraweeHolder holder) { mItem = item; mHolder = holder; } @Override public void onFinalImageSet( String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) { if (imageInfo != null) { mItem.setIcon(new DrawableWithIntrinsicSize(mHolder.getTopLevelDrawable(), imageInfo)); } } } public ReactToolbar(Context context) { super(context); mLogoHolder = DraweeHolder.create(createDraweeHierarchy(), context); mNavIconHolder = DraweeHolder.create(createDraweeHierarchy(), context); mOverflowIconHolder = DraweeHolder.create(createDraweeHierarchy(), context); } private final Runnable mLayoutRunnable = new Runnable() { @Override public void run() { measure( MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY)); layout(getLeft(), getTop(), getRight(), getBottom()); } }; @Override public void requestLayout() { super.requestLayout(); // The toolbar relies on a measure + layout pass happening after it calls requestLayout(). // Without this, certain calls (e.g. setLogo) only take effect after a second invalidation. post(mLayoutRunnable); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); detachDraweeHolders(); } @Override public void onStartTemporaryDetach() { super.onStartTemporaryDetach(); detachDraweeHolders(); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); attachDraweeHolders(); } @Override public void onFinishTemporaryDetach() { super.onFinishTemporaryDetach(); attachDraweeHolders(); } private void detachDraweeHolders() { mLogoHolder.onDetach(); mNavIconHolder.onDetach(); mOverflowIconHolder.onDetach(); mActionsHolder.onDetach(); } private void attachDraweeHolders() { mLogoHolder.onAttach(); mNavIconHolder.onAttach(); mOverflowIconHolder.onAttach(); mActionsHolder.onAttach(); } /* package */ void setLogoSource(@Nullable ReadableMap source) { String uri = source != null ? source.getString("uri") : null; if (uri == null) { setLogo(null); } else if (uri.startsWith("http://") || uri.startsWith("https://")) { DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(Uri.parse(uri)) .setControllerListener(mLogoControllerListener) .setOldController(mLogoHolder.getController()) .build(); mLogoHolder.setController(controller); } else { setLogo(getDrawableResourceByName(uri)); } } /* package */ void setNavIconSource(@Nullable ReadableMap source) { String uri = source != null ? source.getString("uri") : null; if (uri == null) { setNavigationIcon(null); } else if (uri.startsWith("http://") || uri.startsWith("https://")) { DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(Uri.parse(uri)) .setControllerListener(mNavIconControllerListener) .setOldController(mNavIconHolder.getController()) .build(); mNavIconHolder.setController(controller); } else { setNavigationIcon(getDrawableResourceByName(uri)); } } /* package */ void setOverflowIconSource(@Nullable ReadableMap source) { String uri = source != null ? source.getString("uri") : null; if (uri == null) { setOverflowIcon(null); } else if (uri.startsWith("http://") || uri.startsWith("https://")) { DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(Uri.parse(uri)) .setControllerListener(mOverflowIconControllerListener) .setOldController(mOverflowIconHolder.getController()) .build(); mOverflowIconHolder.setController(controller); } else { setOverflowIcon(getDrawableByName(uri)); } } /* package */ void setActions(@Nullable ReadableArray actions) { Menu menu = getMenu(); menu.clear(); mActionsHolder.clear(); if (actions != null) { for (int i = 0; i < actions.size(); i++) { ReadableMap action = actions.getMap(i); MenuItem item = menu.add(Menu.NONE, Menu.NONE, i, action.getString(PROP_ACTION_TITLE)); ReadableMap icon = action.hasKey(PROP_ACTION_ICON) ? action.getMap(PROP_ACTION_ICON) : null; if (icon != null) { String iconSource = icon.getString("uri"); if (iconSource.startsWith("http://") || iconSource.startsWith("https://")) { setMenuItemIcon(item, icon); } else { item.setIcon(getDrawableResourceByName(iconSource)); } } int showAsAction = action.hasKey(PROP_ACTION_SHOW) ? action.getInt(PROP_ACTION_SHOW) : MenuItem.SHOW_AS_ACTION_NEVER; if (action.hasKey(PROP_ACTION_SHOW_WITH_TEXT) && action.getBoolean(PROP_ACTION_SHOW_WITH_TEXT)) { showAsAction = showAsAction | MenuItem.SHOW_AS_ACTION_WITH_TEXT; } item.setShowAsAction(showAsAction); } } } /** * This is only used when the icon is remote (http/s). Creates & adds a new {@link DraweeHolder} * to {@link #mActionsHolder} and attaches a {@link ActionIconControllerListener} that just sets * the top level drawable when it's loaded. */ private void setMenuItemIcon(MenuItem item, ReadableMap icon) { String iconSource = icon.getString("uri"); DraweeHolder<GenericDraweeHierarchy> holder = DraweeHolder.create(createDraweeHierarchy(), getContext()); DraweeController controller = Fresco.newDraweeControllerBuilder() .setUri(Uri.parse(iconSource)) .setControllerListener(new ActionIconControllerListener(item, holder)) .setOldController(holder.getController()) .build(); holder.setController(controller); mActionsHolder.add(holder); } private GenericDraweeHierarchy createDraweeHierarchy() { return new GenericDraweeHierarchyBuilder(getResources()) .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) .setFadeDuration(0) .build(); } private int getDrawableResourceByName(String name) { return getResources().getIdentifier( name, "drawable", getContext().getPackageName()); } private Drawable getDrawableByName(String name) { return getResources().getDrawable(getDrawableResourceByName(name)); } }