/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.toolbar;
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.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.imagepipeline.image.QualityInfo;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.PixelUtil;
import javax.annotation.Nullable;
/**
* 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 static final String PROP_ICON_URI = "uri";
private static final String PROP_ICON_WIDTH = "width";
private static final String PROP_ICON_HEIGHT = "height";
private final DraweeHolder mLogoHolder;
private final DraweeHolder mNavIconHolder;
private final DraweeHolder mOverflowIconHolder;
private final MultiDraweeHolder<GenericDraweeHierarchy> mActionsHolder =
new MultiDraweeHolder<>();
private IconControllerListener mLogoControllerListener;
private IconControllerListener mNavIconControllerListener;
private IconControllerListener mOverflowIconControllerListener;
/**
* Attaches specific icon width & height to a BaseControllerListener which will be used to
* create the Drawable
*/
private abstract class IconControllerListener extends BaseControllerListener<ImageInfo> {
private final DraweeHolder mHolder;
private IconImageInfo mIconImageInfo;
public IconControllerListener(DraweeHolder holder) {
mHolder = holder;
}
public void setIconImageInfo(IconImageInfo iconImageInfo) {
mIconImageInfo = iconImageInfo;
}
@Override
public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
super.onFinalImageSet(id, imageInfo, animatable);
final ImageInfo info = mIconImageInfo != null ? mIconImageInfo : imageInfo;
setDrawable(new DrawableWithIntrinsicSize(mHolder.getTopLevelDrawable(), info));
}
protected abstract void setDrawable(Drawable d);
}
private class ActionIconControllerListener extends IconControllerListener {
private final MenuItem mItem;
ActionIconControllerListener(MenuItem item, DraweeHolder holder) {
super(holder);
mItem = item;
}
@Override
protected void setDrawable(Drawable d) {
mItem.setIcon(d);
}
}
/**
* Simple implementation of ImageInfo, only providing width & height
*/
private static class IconImageInfo implements ImageInfo {
private int mWidth;
private int mHeight;
public IconImageInfo(int width, int height) {
mWidth = width;
mHeight = height;
}
@Override
public int getWidth() {
return mWidth;
}
@Override
public int getHeight() {
return mHeight;
}
@Override
public QualityInfo getQualityInfo() {
return null;
}
}
public ReactToolbar(Context context) {
super(context);
mLogoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
mNavIconHolder = DraweeHolder.create(createDraweeHierarchy(), context);
mOverflowIconHolder = DraweeHolder.create(createDraweeHierarchy(), context);
mLogoControllerListener = new IconControllerListener(mLogoHolder) {
@Override
protected void setDrawable(Drawable d) {
setLogo(d);
}
};
mNavIconControllerListener = new IconControllerListener(mNavIconHolder) {
@Override
protected void setDrawable(Drawable d) {
setNavigationIcon(d);
}
};
mOverflowIconControllerListener = new IconControllerListener(mOverflowIconHolder) {
@Override
protected void setDrawable(Drawable d) {
setOverflowIcon(d);
}
};
}
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) {
setIconSource(source, mLogoControllerListener, mLogoHolder);
}
/* package */ void setNavIconSource(@Nullable ReadableMap source) {
setIconSource(source, mNavIconControllerListener, mNavIconHolder);
}
/* package */ void setOverflowIconSource(@Nullable ReadableMap source) {
setIconSource(source, mOverflowIconControllerListener, mOverflowIconHolder);
}
/* 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));
if (action.hasKey(PROP_ACTION_ICON)) {
setMenuItemIcon(item, action.getMap(PROP_ACTION_ICON));
}
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);
}
}
}
private void setMenuItemIcon(final MenuItem item, ReadableMap iconSource) {
DraweeHolder<GenericDraweeHierarchy> holder =
DraweeHolder.create(createDraweeHierarchy(), getContext());
ActionIconControllerListener controllerListener = new ActionIconControllerListener(item, holder);
controllerListener.setIconImageInfo(getIconImageInfo(iconSource));
setIconSource(iconSource, controllerListener, holder);
mActionsHolder.add(holder);
}
/**
* Sets an icon for a specific icon source. If the uri indicates an icon
* to be somewhere remote (http/https) or on the local filesystem, it uses fresco to load it.
* Otherwise it loads the Drawable from the Resources and directly returns it via a callback
*/
private void setIconSource(ReadableMap source, IconControllerListener controllerListener, DraweeHolder holder) {
String uri = source != null ? source.getString(PROP_ICON_URI) : null;
if (uri == null) {
controllerListener.setIconImageInfo(null);
controllerListener.setDrawable(null);
} else if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("file://")) {
controllerListener.setIconImageInfo(getIconImageInfo(source));
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setUri(Uri.parse(uri))
.setControllerListener(controllerListener)
.setOldController(holder.getController())
.build();
holder.setController(controller);
holder.getTopLevelDrawable().setVisible(true, true);
} else {
controllerListener.setDrawable(getDrawableByName(uri));
}
}
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) {
int drawableResId = getDrawableResourceByName(name);
if (drawableResId != 0) {
return getResources().getDrawable(getDrawableResourceByName(name));
} else {
return null;
}
}
private IconImageInfo getIconImageInfo(ReadableMap source) {
if (source.hasKey(PROP_ICON_WIDTH) && source.hasKey(PROP_ICON_HEIGHT)) {
final int width = Math.round(PixelUtil.toPixelFromDIP(source.getInt(PROP_ICON_WIDTH)));
final int height = Math.round(PixelUtil.toPixelFromDIP(source.getInt(PROP_ICON_HEIGHT)));
return new IconImageInfo(width, height);
} else {
return null;
}
}
}