diff --git a/Examples/UIExplorer/ToolbarAndroidExample.android.js b/Examples/UIExplorer/ToolbarAndroidExample.android.js
index 359ba087e..c621296d9 100644
--- a/Examples/UIExplorer/ToolbarAndroidExample.android.js
+++ b/Examples/UIExplorer/ToolbarAndroidExample.android.js
@@ -93,6 +93,14 @@ var ToolbarAndroidExample = React.createClass({
Touch the icon to reset the custom colors to the default (theme-provided) ones.
+
+
+
);
},
diff --git a/Examples/UIExplorer/bunny.png b/Examples/UIExplorer/bunny.png
new file mode 100644
index 000000000..0d94af660
Binary files /dev/null and b/Examples/UIExplorer/bunny.png differ
diff --git a/Examples/UIExplorer/hawk.png b/Examples/UIExplorer/hawk.png
new file mode 100644
index 000000000..7205b1e47
Binary files /dev/null and b/Examples/UIExplorer/hawk.png differ
diff --git a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js
index 371f49e42..9c1a5b877 100644
--- a/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js
+++ b/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js
@@ -13,11 +13,13 @@
var Image = require('Image');
var NativeMethodsMixin = require('NativeMethodsMixin');
+var RCTUIManager = require('NativeModules').UIManager;
var React = require('React');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var ReactPropTypes = require('ReactPropTypes');
var requireNativeComponent = require('requireNativeComponent');
+var resolveAssetSource = require('resolveAssetSource');
/**
* React component that wraps the Android-only [`Toolbar` widget][0]. A Toolbar can display a logo,
@@ -27,6 +29,12 @@ var requireNativeComponent = require('requireNativeComponent');
*
* If the toolbar has an only child, it will be displayed between the title and actions.
*
+ * Although the Toolbar supports remote images for the logo, navigation and action icons, this
+ * should only be used in DEV mode where `require('./some_icon.png')` translates into a packager
+ * URL. In release mode you should always use a drawable resource for these icons. Using
+ * `require('./some_icon.png')` will do this automatically for you, so as long as you don't
+ * explicitly use e.g. `{uri: 'http://...'}`, you will be good.
+ *
* Example:
*
* ```
@@ -115,16 +123,10 @@ var ToolbarAndroid = React.createClass({
...this.props,
};
if (this.props.logo) {
- if (!this.props.logo.isStatic) {
- throw 'logo prop should be a static image (obtained via ix)';
- }
- nativeProps.logo = this.props.logo.uri;
+ nativeProps.logo = resolveAssetSource(this.props.logo);
}
if (this.props.navIcon) {
- if (!this.props.navIcon.isStatic) {
- throw 'navIcon prop should be static image (obtained via ix)';
- }
- nativeProps.navIcon = this.props.navIcon.uri;
+ nativeProps.navIcon = resolveAssetSource(this.props.navIcon);
}
if (this.props.actions) {
nativeProps.actions = [];
@@ -133,10 +135,10 @@ var ToolbarAndroid = React.createClass({
...this.props.actions[i],
};
if (action.icon) {
- if (!action.icon.isStatic) {
- throw 'action icons should be static images (obtained via ix)';
- }
- action.icon = action.icon.uri;
+ action.icon = resolveAssetSource(action.icon);
+ }
+ if (action.show) {
+ action.show = RCTUIManager.ToolbarAndroid.Constants.ShowAsAction[action.show];
}
nativeProps.actions.push(action);
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/DrawableWithIntrinsicSize.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/DrawableWithIntrinsicSize.java
new file mode 100644
index 000000000..849e1e44d
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/DrawableWithIntrinsicSize.java
@@ -0,0 +1,42 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+package com.facebook.react.views.toolbar;
+
+import android.graphics.drawable.Drawable;
+
+import com.facebook.drawee.drawable.ForwardingDrawable;
+import com.facebook.imagepipeline.image.ImageInfo;
+import com.facebook.react.uimanager.PixelUtil;
+
+/**
+ * Fresco currently sets drawables' intrinsic size to (-1, -1). This is to guarantee that scaling is
+ * performed correctly. In the case of the Toolbar, we don't have access to the widget's internal
+ * ImageView, which has width/height set to WRAP_CONTENT, which relies on intrinsic size.
+ *
+ * To work around this we have this class which just wraps another Drawable, but returns the correct
+ * dimensions in getIntrinsicWidth/Height. This makes WRAP_CONTENT work in Toolbar's internals.
+ *
+ * This drawable uses the size of a loaded image to determine the intrinsic size. It therefore can't
+ * be used safely until *after* an image has loaded, and must be replaced when the image is
+ * replaced.
+ */
+public class DrawableWithIntrinsicSize extends ForwardingDrawable implements Drawable.Callback {
+
+ private final ImageInfo mImageInfo;
+
+ public DrawableWithIntrinsicSize(Drawable drawable, ImageInfo imageInfo) {
+ super(drawable);
+ mImageInfo = imageInfo;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return (int) PixelUtil.toDIPFromPixel(mImageInfo.getWidth());
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return (int) PixelUtil.toDIPFromPixel(mImageInfo.getHeight());
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java
new file mode 100644
index 000000000..91741c1c6
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java
@@ -0,0 +1,250 @@
+// 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.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 MultiDraweeHolder mActionsHolder =
+ new MultiDraweeHolder<>();
+
+ private final ControllerListener mLogoControllerListener =
+ new BaseControllerListener() {
+ @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 mNavIconControllerListener =
+ new BaseControllerListener() {
+ @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 static class ActionIconControllerListener extends BaseControllerListener {
+ 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);
+ }
+
+ 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();
+ mLogoHolder.onAttach();
+ mNavIconHolder.onAttach();
+ }
+
+ private void detachDraweeHolders() {
+ mLogoHolder.onDetach();
+ mNavIconHolder.onDetach();
+ mActionsHolder.onDetach();
+ }
+
+ private void attachDraweeHolders() {
+ mLogoHolder.onAttach();
+ mNavIconHolder.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 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 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());
+ }
+
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java
index 6b9a988dd..93fe8a61d 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbarManager.java
@@ -11,19 +11,20 @@ package com.facebook.react.views.toolbar;
import javax.annotation.Nullable;
+import java.util.Map;
+
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.SystemClock;
-import android.support.v7.widget.Toolbar;
-import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.facebook.react.R;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ReactProp;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerModule;
@@ -32,52 +33,39 @@ import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.toolbar.events.ToolbarClickEvent;
/**
- * Manages instances of Toolbar.
+ * Manages instances of ReactToolbar.
*/
-public class ReactToolbarManager extends ViewGroupManager {
+public class ReactToolbarManager extends ViewGroupManager {
private static final String REACT_CLASS = "ToolbarAndroid";
- 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";
-
@Override
public String getName() {
return REACT_CLASS;
}
@Override
- protected Toolbar createViewInstance(ThemedReactContext reactContext) {
- return new Toolbar(reactContext);
+ protected ReactToolbar createViewInstance(ThemedReactContext reactContext) {
+ return new ReactToolbar(reactContext);
}
@ReactProp(name = "logo")
- public void setLogo(Toolbar view, @Nullable String logo) {
- if (logo != null) {
- view.setLogo(getDrawableResourceByName(view.getContext(), logo));
- } else {
- view.setLogo(null);
- }
+ public void setLogo(ReactToolbar view, @Nullable ReadableMap logo) {
+ view.setLogoSource(logo);
}
@ReactProp(name = "navIcon")
- public void setNavIcon(Toolbar view, @Nullable String navIcon) {
- if (navIcon != null) {
- view.setNavigationIcon(getDrawableResourceByName(view.getContext(), navIcon));
- } else {
- view.setNavigationIcon(null);
- }
+ public void setNavIcon(ReactToolbar view, @Nullable ReadableMap navIcon) {
+ view.setNavIconSource(navIcon);
}
@ReactProp(name = "subtitle")
- public void setSubtitle(Toolbar view, @Nullable String subtitle) {
+ public void setSubtitle(ReactToolbar view, @Nullable String subtitle) {
view.setSubtitle(subtitle);
}
@ReactProp(name = "subtitleColor", customType = "Color")
- public void setSubtitleColor(Toolbar view, @Nullable Integer subtitleColor) {
+ public void setSubtitleColor(ReactToolbar view, @Nullable Integer subtitleColor) {
int[] defaultColors = getDefaultColors(view.getContext());
if (subtitleColor != null) {
view.setSubtitleTextColor(subtitleColor);
@@ -87,12 +75,12 @@ public class ReactToolbarManager extends ViewGroupManager {
}
@ReactProp(name = "title")
- public void setTitle(Toolbar view, @Nullable String title) {
+ public void setTitle(ReactToolbar view, @Nullable String title) {
view.setTitle(title);
}
@ReactProp(name = "titleColor", customType = "Color")
- public void setTitleColor(Toolbar view, @Nullable Integer titleColor) {
+ public void setTitleColor(ReactToolbar view, @Nullable Integer titleColor) {
int[] defaultColors = getDefaultColors(view.getContext());
if (titleColor != null) {
view.setTitleTextColor(titleColor);
@@ -102,37 +90,12 @@ public class ReactToolbarManager extends ViewGroupManager {
}
@ReactProp(name = "actions")
- public void setActions(Toolbar view, @Nullable ReadableArray actions) {
- Menu menu = view.getMenu();
- menu.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));
- String icon = action.hasKey(PROP_ACTION_ICON) ? action.getString(PROP_ACTION_ICON) : null;
- if (icon != null) {
- item.setIcon(getDrawableResourceByName(view.getContext(), icon));
- }
- String show = action.hasKey(PROP_ACTION_SHOW) ? action.getString(PROP_ACTION_SHOW) : null;
- if (show != null) {
- int showAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
- if ("always".equals(show)) {
- showAsAction = MenuItem.SHOW_AS_ACTION_ALWAYS;
- } else if ("ifRoom".equals(show)) {
- showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
- }
- 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);
- }
- }
- }
+ public void setActions(ReactToolbar view, @Nullable ReadableArray actions) {
+ view.setActions(actions);
}
@Override
- protected void addEventEmitters(final ThemedReactContext reactContext, final Toolbar view) {
+ protected void addEventEmitters(final ThemedReactContext reactContext, final ReactToolbar view) {
final EventDispatcher mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
.getEventDispatcher();
view.setNavigationOnClickListener(
@@ -145,7 +108,7 @@ public class ReactToolbarManager extends ViewGroupManager {
});
view.setOnMenuItemClickListener(
- new Toolbar.OnMenuItemClickListener() {
+ new ReactToolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
mEventDispatcher.dispatchEvent(
@@ -158,6 +121,17 @@ public class ReactToolbarManager extends ViewGroupManager {
});
}
+ @Nullable
+ @Override
+ public Map getExportedViewConstants() {
+ return MapBuilder.of(
+ "ShowAsAction",
+ MapBuilder.of(
+ "never", MenuItem.SHOW_AS_ACTION_NEVER,
+ "always", MenuItem.SHOW_AS_ACTION_ALWAYS,
+ "ifRoom", MenuItem.SHOW_AS_ACTION_IF_ROOM));
+ }
+
@Override
public boolean needsCustomLayoutForChildren() {
return true;
@@ -205,12 +179,4 @@ public class ReactToolbarManager extends ViewGroupManager {
}
}
- private static int getDrawableResourceByName(Context context, String name) {
- name = name.toLowerCase().replace("-", "_");
- return context.getResources().getIdentifier(
- name,
- "drawable",
- context.getPackageName());
- }
-
}