Open sourced spinner aka picker aka drop down for android

Reviewed By: mkonicek

Differential Revision: D2830803

fb-gh-sync-id: e6b6fcdbe33d942180cf2c1041076ad71d0473ce
This commit is contained in:
Konstantin Raev
2016-01-15 06:23:15 -08:00
committed by facebook-github-bot-4
parent cd89016ee7
commit 18437093f2
11 changed files with 779 additions and 0 deletions

View File

@@ -32,6 +32,8 @@ import com.facebook.react.views.art.ARTRenderableViewManager;
import com.facebook.react.views.art.ARTSurfaceViewManager;
import com.facebook.react.views.drawer.ReactDrawerLayoutManager;
import com.facebook.react.views.image.ReactImageManager;
import com.facebook.react.views.picker.ReactDialogPickerManager;
import com.facebook.react.views.picker.ReactDropdownPickerManager;
import com.facebook.react.views.progressbar.ReactProgressBarViewManager;
import com.facebook.react.views.recyclerview.RecyclerViewBackedScrollViewManager;
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
@@ -82,7 +84,9 @@ public class MainReactPackage implements ReactPackage {
ARTRenderableViewManager.createARTShapeViewManager(),
ARTRenderableViewManager.createARTTextViewManager(),
new ARTSurfaceViewManager(),
new ReactDialogPickerManager(),
new ReactDrawerLayoutManager(),
new ReactDropdownPickerManager(),
new ReactHorizontalScrollViewManager(),
new ReactImageManager(),
new ReactProgressBarViewManager(),

View File

@@ -0,0 +1,32 @@
/**
* 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.picker;
import android.widget.Spinner;
import com.facebook.react.uimanager.ThemedReactContext;
/**
* {@link ReactPickerManager} for {@link ReactPicker} with {@link Spinner#MODE_DIALOG}.
*/
public class ReactDialogPickerManager extends ReactPickerManager {
private static final String REACT_CLASS = "AndroidDialogPicker";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected ReactPicker createViewInstance(ThemedReactContext reactContext) {
return new ReactPicker(reactContext, Spinner.MODE_DIALOG);
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.picker;
import android.widget.Spinner;
import com.facebook.react.uimanager.ThemedReactContext;
/**
* {@link ReactPickerManager} for {@link ReactPicker} with {@link Spinner#MODE_DROPDOWN}.
*/
public class ReactDropdownPickerManager extends ReactPickerManager {
private static final String REACT_CLASS = "AndroidDropdownPicker";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected ReactPicker createViewInstance(ThemedReactContext reactContext) {
return new ReactPicker(reactContext, Spinner.MODE_DROPDOWN);
}
}

View File

@@ -0,0 +1,148 @@
/**
* 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.picker;
import javax.annotation.Nullable;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Spinner;
import com.facebook.react.common.annotations.VisibleForTesting;
public class ReactPicker extends Spinner {
private int mMode = MODE_DIALOG;
private @Nullable Integer mPrimaryColor;
private boolean mSuppressNextEvent;
private @Nullable OnSelectListener mOnSelectListener;
private @Nullable Integer mStagedSelection;
/**
* Listener interface for ReactPicker events.
*/
public interface OnSelectListener {
void onItemSelected(int position);
}
public ReactPicker(Context context) {
super(context);
}
public ReactPicker(Context context, int mode) {
super(context, mode);
mMode = mode;
}
public ReactPicker(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReactPicker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public ReactPicker(Context context, AttributeSet attrs, int defStyle, int mode) {
super(context, attrs, defStyle, mode);
mMode = mode;
}
private final Runnable measureAndLayout = 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 spinner relies on a measure + layout pass happening after it calls requestLayout().
// Without this, the widget never actually changes the selection and doesn't call the
// appropriate listeners. Since we override onLayout in our ViewGroups, a layout pass never
// happens after a call to requestLayout, so we simulate one here.
post(measureAndLayout);
}
public void setOnSelectListener(@Nullable OnSelectListener onSelectListener) {
if (getOnItemSelectedListener() == null) {
setOnItemSelectedListener(
new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (!mSuppressNextEvent && mOnSelectListener != null) {
mOnSelectListener.onItemSelected(position);
}
mSuppressNextEvent = false;
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
if (!mSuppressNextEvent && mOnSelectListener != null) {
mOnSelectListener.onItemSelected(-1);
}
mSuppressNextEvent = false;
}
});
}
mOnSelectListener = onSelectListener;
}
@Nullable public OnSelectListener getOnSelectListener() {
return mOnSelectListener;
}
/**
* Will cache "selection" value locally and set it only once {@link #updateStagedSelection} is
* called
*/
public void setStagedSelection(int selection) {
mStagedSelection = selection;
}
public void updateStagedSelection() {
if (mStagedSelection != null) {
setSelectionWithSuppressEvent(mStagedSelection);
mStagedSelection = null;
}
}
/**
* Set the selection while suppressing the follow-up {@link OnSelectListener#onItemSelected(int)}
* event. This is used so we don't get an event when changing the selection ourselves.
*
* @param position the position of the selected item
*/
private void setSelectionWithSuppressEvent(int position) {
if (position != getSelectedItemPosition()) {
mSuppressNextEvent = true;
setSelection(position);
}
}
public @Nullable Integer getPrimaryColor() {
return mPrimaryColor;
}
public void setPrimaryColor(@Nullable Integer primaryColor) {
mPrimaryColor = primaryColor;
}
@VisibleForTesting
public int getMode() {
return mMode;
}
}

View File

@@ -0,0 +1,163 @@
/**
* 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.picker;
import javax.annotation.Nullable;
import android.content.Context;
import android.os.SystemClock;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.picker.events.PickerItemSelectEvent;
/**
* {@link ViewManager} for the {@link ReactPicker} view. This is abstract because the
* {@link Spinner} doesn't support setting the mode (dropdown/dialog) outside the constructor, so
* that is delegated to the separate {@link ReactDropdownPickerManager} and
* {@link ReactDialogPickerManager} components. These are merged back on the JS side into one
* React component.
*/
public abstract class ReactPickerManager extends SimpleViewManager<ReactPicker> {
@ReactProp(name = "items")
public void setItems(ReactPicker view, @Nullable ReadableArray items) {
if (items != null) {
ReadableMap[] data = new ReadableMap[items.size()];
for (int i = 0; i < items.size(); i++) {
data[i] = items.getMap(i);
}
ReactPickerAdapter adapter = new ReactPickerAdapter(view.getContext(), data);
adapter.setPrimaryTextColor(view.getPrimaryColor());
view.setAdapter(adapter);
} else {
view.setAdapter(null);
}
}
@ReactProp(name = ViewProps.COLOR, customType = "Color")
public void setColor(ReactPicker view, @Nullable Integer color) {
view.setPrimaryColor(color);
ReactPickerAdapter adapter = (ReactPickerAdapter) view.getAdapter();
if (adapter != null) {
adapter.setPrimaryTextColor(color);
}
}
@ReactProp(name = "prompt")
public void setPrompt(ReactPicker view, @Nullable String prompt) {
view.setPrompt(prompt);
}
@ReactProp(name = ViewProps.ENABLED, defaultBoolean = true)
public void setEnabled(ReactPicker view, boolean enabled) {
view.setEnabled(enabled);
}
@ReactProp(name = "selected")
public void setSelected(ReactPicker view, int selected) {
view.setStagedSelection(selected);
}
@Override
protected void onAfterUpdateTransaction(ReactPicker view) {
super.onAfterUpdateTransaction(view);
view.updateStagedSelection();
}
@Override
protected void addEventEmitters(
final ThemedReactContext reactContext,
final ReactPicker picker) {
picker.setOnSelectListener(
new PickerEventEmitter(
picker,
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()));
}
private static class ReactPickerAdapter extends ArrayAdapter<ReadableMap> {
private final LayoutInflater mInflater;
private @Nullable Integer mPrimaryTextColor;
public ReactPickerAdapter(Context context, ReadableMap[] data) {
super(context, 0, data);
mInflater = (LayoutInflater) Assertions.assertNotNull(
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE));
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return getView(position, convertView, parent, false);
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return getView(position, convertView, parent, true);
}
private View getView(int position, View convertView, ViewGroup parent, boolean isDropdown) {
ReadableMap item = getItem(position);
if (convertView == null) {
int layoutResId = isDropdown
? android.R.layout.simple_spinner_dropdown_item
: android.R.layout.simple_spinner_item;
convertView = mInflater.inflate(layoutResId, parent, false);
}
TextView textView = (TextView) convertView;
textView.setText(item.getString("text"));
if (!isDropdown && mPrimaryTextColor != null) {
textView.setTextColor(mPrimaryTextColor);
} else if (item.hasKey("color") && !item.isNull("color")) {
textView.setTextColor(item.getInt("color"));
}
return convertView;
}
public void setPrimaryTextColor(@Nullable Integer primaryTextColor) {
mPrimaryTextColor = primaryTextColor;
notifyDataSetChanged();
}
}
private static class PickerEventEmitter implements ReactPicker.OnSelectListener {
private final ReactPicker mReactPicker;
private final EventDispatcher mEventDispatcher;
public PickerEventEmitter(ReactPicker reactPicker, EventDispatcher eventDispatcher) {
mReactPicker = reactPicker;
mEventDispatcher = eventDispatcher;
}
@Override
public void onItemSelected(int position) {
mEventDispatcher.dispatchEvent( new PickerItemSelectEvent(
mReactPicker.getId(), SystemClock.uptimeMillis(), position));
}
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.picker.events;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class PickerItemSelectEvent extends Event<PickerItemSelectEvent> {
public static final String EVENT_NAME = "topSelect";
private final int mPosition;
public PickerItemSelectEvent(int id, long uptimeMillis, int position) {
super(id, uptimeMillis);
mPosition = position;
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
}
private WritableMap serializeEventData() {
WritableMap eventData = Arguments.createMap();
eventData.putInt("position", mPosition);
return eventData;
}
}