diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.java b/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.java index 9689ef849ac5d..ed400adb73e3e 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.java +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.java @@ -49,6 +49,8 @@ import versioned.host.exp.exponent.modules.api.cognito.RNAWSCognitoModule; import versioned.host.exp.exponent.modules.api.components.datetimepicker.RNDateTimePickerPackage; import versioned.host.exp.exponent.modules.api.components.maskedview.RNCMaskedViewPackage; +import versioned.host.exp.exponent.modules.api.components.picker.RNCPickerPackage; +import versioned.host.exp.exponent.modules.api.components.slider.ReactSliderPackage; import versioned.host.exp.exponent.modules.api.components.gesturehandler.react.RNGestureHandlerModule; import versioned.host.exp.exponent.modules.api.components.gesturehandler.react.RNGestureHandlerPackage; import versioned.host.exp.exponent.modules.api.components.lottie.LottiePackage; @@ -251,6 +253,8 @@ public List createViewManagers(ReactApplicationContext reactContext new RNSharedElementPackage(), new RNDateTimePickerPackage(), new RNCMaskedViewPackage(), + new RNCPickerPackage(), + new ReactSliderPackage(), new RNCViewPagerPackage(), new ExpoAppearancePackage() )); diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/PickerItemSelectEvent.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/PickerItemSelectEvent.java new file mode 100644 index 0000000000000..6176576d0b3c4 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/PickerItemSelectEvent.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.picker; + +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 { + public static final String EVENT_NAME = "topSelect"; + + private final int mPosition; + + public PickerItemSelectEvent(int id, int position) { + super(id); + 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; + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/RNCPickerPackage.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/RNCPickerPackage.java new file mode 100644 index 0000000000000..e19f2791ed2b8 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/RNCPickerPackage.java @@ -0,0 +1,32 @@ + +package versioned.host.exp.exponent.modules.api.components.picker; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.bridge.JavaScriptModule; +public class RNCPickerPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + // Deprecated from RN 0.47 + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + List list = new ArrayList<>(); + list.add(new ReactDialogPickerManager()); + list.add(new ReactDropdownPickerManager()); + return list; + } +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactDialogPickerManager.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactDialogPickerManager.java new file mode 100644 index 0000000000000..566ad20343750 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactDialogPickerManager.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.picker; + +import android.widget.Spinner; + +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.ThemedReactContext; + + +/** + * {@link ReactPickerManager} for {@link ReactPicker} with {@link Spinner#MODE_DIALOG}. + */ +@ReactModule(name = ReactDialogPickerManager.REACT_CLASS) +public class ReactDialogPickerManager extends ReactPickerManager { + + public 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); + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactDropdownPickerManager.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactDropdownPickerManager.java new file mode 100644 index 0000000000000..460754484f4b0 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactDropdownPickerManager.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.picker; + +import android.widget.Spinner; + +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.ThemedReactContext; + +/** + * {@link ReactPickerManager} for {@link ReactPicker} with {@link Spinner#MODE_DROPDOWN}. + */ +@ReactModule(name = ReactDropdownPickerManager.REACT_CLASS) +public class ReactDropdownPickerManager extends ReactPickerManager { + + public 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); + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactPicker.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactPicker.java new file mode 100644 index 0000000000000..4413dd288a469 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactPicker.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.picker; + +import android.content.Context; +import androidx.appcompat.widget.AppCompatSpinner; +import android.util.AttributeSet; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Spinner; + +import com.facebook.react.common.annotations.VisibleForTesting; + +import javax.annotation.Nullable; + +public class ReactPicker extends AppCompatSpinner { + + private int mMode = Spinner.MODE_DIALOG; + private @Nullable Integer mPrimaryColor; + private @Nullable OnSelectListener mOnSelectListener; + private @Nullable Integer mStagedSelection; + + private final OnItemSelectedListener mItemSelectedListener = new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (mOnSelectListener != null) { + mOnSelectListener.onItemSelected(position); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + if (mOnSelectListener != null) { + mOnSelectListener.onItemSelected(-1); + } + } + }; + + /** + * 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); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + // onItemSelected gets fired immediately after layout because checkSelectionChanged() in + // AdapterView updates the selection position from the default INVALID_POSITION. + // To match iOS behavior, which no onItemSelected during initial layout. + // We setup the listener after layout. + if (getOnItemSelectedListener() == null) + setOnItemSelectedListener(mItemSelectedListener); + } + + public void setOnSelectListener(@Nullable OnSelectListener onSelectListener) { + 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()) { + setOnItemSelectedListener(null); + setSelection(position, false); + setOnItemSelectedListener(mItemSelectedListener); + } + } + + public @Nullable Integer getPrimaryColor() { + return mPrimaryColor; + } + + public void setPrimaryColor(@Nullable Integer primaryColor) { + mPrimaryColor = primaryColor; + } + + @VisibleForTesting + public int getMode() { + return mMode; + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactPickerManager.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactPickerManager.java new file mode 100644 index 0000000000000..26f92ac757352 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/picker/ReactPickerManager.java @@ -0,0 +1,157 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.picker; + +import android.content.Context; +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.SimpleViewManager; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.events.EventDispatcher; +import javax.annotation.Nullable; + +/** + * {@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 { + + @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 { + + 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("label")); + 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(), position)); + } + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlider.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlider.java new file mode 100644 index 0000000000000..e044c45135c8d --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlider.java @@ -0,0 +1,236 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + *

This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package versioned.host.exp.exponent.modules.api.components.slider; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import androidx.appcompat.widget.AppCompatSeekBar; +import java.net.URL; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import javax.annotation.Nullable; + +/** + * Slider that behaves more like the iOS one, for consistency. + * + *

On iOS, the value is 0..1. Android SeekBar only supports integer values. For consistency, we + * pretend in JS that the value is 0..1 but set the SeekBar value to 0..100. + * + *

Note that the slider is _not_ a controlled component (setValue isn't called during dragging). + */ +public class ReactSlider extends AppCompatSeekBar { + + /** + * If step is 0 (unset) we default to this total number of steps. Don't use 100 which leads to + * rounding errors (0.200000000001). + */ + private static int DEFAULT_TOTAL_STEPS = 128; + + /** + * We want custom min..max range. Android only supports 0..max range so we implement this + * ourselves. + */ + private double mMinValue = 0; + + private double mMaxValue = 0; + + /** + * Value sent from JS (setState). Doesn't get updated during drag (slider is not a controlled + * component). + */ + private double mValue = 0; + + /** If zero it's determined automatically. */ + private double mStep = 0; + + private double mStepCalculated = 0; + + private String mAccessibilityUnits; + + private List mAccessibilityIncrements; + + public ReactSlider(Context context, @Nullable AttributeSet attrs, int style) { + super(context, attrs, style); + disableStateListAnimatorIfNeeded(); + } + + private void disableStateListAnimatorIfNeeded() { + // We disable the state list animator for Android 6 and 7; this is a hack to prevent T37452851 + // and https://github.com/facebook/react-native/issues/9979 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + super.setStateListAnimator(null); + } + } + + /* package */ void setMaxValue(double max) { + mMaxValue = max; + updateAll(); + } + + /* package */ void setMinValue(double min) { + mMinValue = min; + updateAll(); + } + + /* package */ void setValue(double value) { + mValue = value; + updateValue(); + } + + /* package */ void setStep(double step) { + mStep = step; + updateAll(); + } + + void setAccessibilityUnits(String accessibilityUnits) { + mAccessibilityUnits = accessibilityUnits; + } + + void setAccessibilityIncrements(List accessibilityIncrements) { + mAccessibilityIncrements = accessibilityIncrements; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED || + (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED && this.isAccessibilityFocused())) { + this.setupAccessibility(); + } + } + + @Override + public void announceForAccessibility(CharSequence text) { + Context ctx = this.getContext(); + final AccessibilityManager manager = (AccessibilityManager) ctx.getSystemService(Context.ACCESSIBILITY_SERVICE); + + if (manager.isEnabled()) { + final AccessibilityEvent e = AccessibilityEvent.obtain(); + e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); + e.setClassName(this.getClass().getName()); + e.setPackageName(ctx.getPackageName()); + e.getText().add(text); + + TimerTask task = new TimerTask() { + @Override + public void run() { + manager.sendAccessibilityEvent(e); + } + }; + + Timer timer = new Timer(); + timer.schedule(task, 1000); + } + } + + private void setupAccessibility() { + if (mAccessibilityUnits != null && mAccessibilityIncrements != null && mAccessibilityIncrements.size() - 1 == (int)mMaxValue) { + int index = (int)mValue; + String sliderValue = mAccessibilityIncrements.get(index); + int stringLength = mAccessibilityUnits.length(); + + String spokenUnits = mAccessibilityUnits; + if (sliderValue != null && Integer.parseInt(sliderValue) == 1) { + spokenUnits = spokenUnits.substring(0, stringLength - 1); + } + + this.announceForAccessibility(String.format("%s %s", sliderValue, spokenUnits)); + } + } + + + + /** + * Convert SeekBar's native progress value (e.g. 0..100) to a value passed to JS (e.g. -1.0..2.5). + */ + public double toRealProgress(int seekBarProgress) { + if (seekBarProgress == getMax()) { + return mMaxValue; + } + return seekBarProgress * getStepValue() + mMinValue; + } + + /** Update underlying native SeekBar's values. */ + private void updateAll() { + if (mStep == 0) { + mStepCalculated = (mMaxValue - mMinValue) / (double) DEFAULT_TOTAL_STEPS; + } + setMax(getTotalSteps()); + updateValue(); + } + + /** Update value only (optimization in case only value is set). */ + private void updateValue() { + setProgress((int) Math.round((mValue - mMinValue) / (mMaxValue - mMinValue) * getTotalSteps())); + } + + private int getTotalSteps() { + return (int) Math.ceil((mMaxValue - mMinValue) / getStepValue()); + } + + private double getStepValue() { + return mStep > 0 ? mStep : mStepCalculated; + } + + private BitmapDrawable getBitmapDrawable(final String uri) { + BitmapDrawable bitmapDrawable = null; + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future future = executorService.submit(new Callable() { + @Override + public BitmapDrawable call() { + BitmapDrawable bitmapDrawable = null; + try { + Bitmap bitmap = null; + if (uri.startsWith("http://") || uri.startsWith("https://") || + uri.startsWith("file://") || uri.startsWith("asset://") || uri.startsWith("data:")) { + bitmap = BitmapFactory.decodeStream(new URL(uri).openStream()); + } else { + int drawableId = getResources() + .getIdentifier(uri, "drawable", getContext() + .getPackageName()); + bitmap = BitmapFactory.decodeResource(getResources(), drawableId); + } + + bitmapDrawable = new BitmapDrawable(getResources(), bitmap); + } catch (Exception e) { + e.printStackTrace(); + } + return bitmapDrawable; + } + }); + try { + bitmapDrawable = future.get(); + } catch (Exception e) { + e.printStackTrace(); + } + return bitmapDrawable; + } + + public void setThumbImage(final String uri) { + if (uri != null) { + setThumb(getBitmapDrawable(uri)); + // Enable alpha channel for the thumbImage + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setSplitTrack(false); + } + } else { + setThumb(getThumb()); + } + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderEvent.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderEvent.java new file mode 100644 index 0000000000000..55052d6be73ca --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderEvent.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.slider; + +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; + +/** + * Event emitted by a ReactSliderManager when user changes slider position. + */ +public class ReactSliderEvent extends Event { + + public static final String EVENT_NAME = "topChange"; + + private final double mValue; + private final boolean mFromUser; + + public ReactSliderEvent(int viewId, double value, boolean fromUser) { + super(viewId); + mValue = value; + mFromUser = fromUser; + } + + public double getValue() { + return mValue; + } + + public boolean isFromUser() { + return mFromUser; + } + + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + return 0; + } + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putDouble("value", getValue()); + eventData.putBoolean("fromUser", isFromUser()); + return eventData; + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderManager.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderManager.java new file mode 100644 index 0000000000000..2275f28ead78e --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderManager.java @@ -0,0 +1,242 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.slider; + +import android.os.Build; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.view.View; +import android.widget.SeekBar; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.LayoutShadowNode; +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.annotations.ReactProp; +import com.facebook.yoga.YogaMeasureFunction; +import com.facebook.yoga.YogaMeasureMode; +import com.facebook.yoga.YogaMeasureOutput; +import com.facebook.yoga.YogaNode; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Manages instances of {@code ReactSlider}. + * + * Note that the slider is _not_ a controlled component. + */ +public class ReactSliderManager extends SimpleViewManager { + + private static final int STYLE = android.R.attr.seekBarStyle; + + public static final String REACT_CLASS = "RNCSlider"; + + static class ReactSliderShadowNode extends LayoutShadowNode implements + YogaMeasureFunction { + + private int mWidth; + private int mHeight; + private boolean mMeasured; + + private ReactSliderShadowNode() { + initMeasureFunction(); + } + + private void initMeasureFunction() { + setMeasureFunction(this); + } + + @Override + public long measure( + YogaNode node, + float width, + YogaMeasureMode widthMode, + float height, + YogaMeasureMode heightMode) { + if (!mMeasured) { + SeekBar reactSlider = new ReactSlider(getThemedContext(), null, STYLE); + final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + reactSlider.measure(spec, spec); + mWidth = reactSlider.getMeasuredWidth(); + mHeight = reactSlider.getMeasuredHeight(); + mMeasured = true; + } + + return YogaMeasureOutput.make(mWidth, mHeight); + } + } + + private static final SeekBar.OnSeekBarChangeListener ON_CHANGE_LISTENER = + new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) { + ReactContext reactContext = (ReactContext) seekbar.getContext(); + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent( + new ReactSliderEvent( + seekbar.getId(), + ((ReactSlider) seekbar).toRealProgress(progress), + fromUser)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekbar) { + ReactContext reactContext = (ReactContext) seekbar.getContext(); + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent( + new ReactSlidingStartEvent( + seekbar.getId(), + ((ReactSlider) seekbar).toRealProgress(seekbar.getProgress()))); + } + + @Override + public void onStopTrackingTouch(SeekBar seekbar) { + ReactContext reactContext = (ReactContext) seekbar.getContext(); + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent( + new ReactSlidingCompleteEvent( + seekbar.getId(), + ((ReactSlider) seekbar).toRealProgress(seekbar.getProgress()))); + } + }; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public LayoutShadowNode createShadowNodeInstance() { + return new ReactSliderShadowNode(); + } + + @Override + public Class getShadowNodeClass() { + return ReactSliderShadowNode.class; + } + + @Override + protected ReactSlider createViewInstance(ThemedReactContext context) { + ReactSlider slider = new ReactSlider(context, null, STYLE); + + if (Build.VERSION.SDK_INT >= 21) { + /** + * The "splitTrack" parameter should have "false" value, + * otherwise the SeekBar progress line doesn't appear when it is rotated. + */ + slider.setSplitTrack(false); + } + + return slider; + } + + @ReactProp(name = ViewProps.ENABLED, defaultBoolean = true) + public void setEnabled(ReactSlider view, boolean enabled) { + view.setEnabled(enabled); + } + + @ReactProp(name = "value", defaultDouble = 0d) + public void setValue(ReactSlider view, double value) { + view.setOnSeekBarChangeListener(null); + view.setValue(value); + view.setOnSeekBarChangeListener(ON_CHANGE_LISTENER); + } + + @ReactProp(name = "minimumValue", defaultDouble = 0d) + public void setMinimumValue(ReactSlider view, double value) { + view.setMinValue(value); + } + + @ReactProp(name = "maximumValue", defaultDouble = 1d) + public void setMaximumValue(ReactSlider view, double value) { + view.setMaxValue(value); + } + + @ReactProp(name = "step", defaultDouble = 0d) + public void setStep(ReactSlider view, double value) { + view.setStep(value); + } + + @ReactProp(name = "thumbTintColor", customType = "Color") + public void setThumbTintColor(ReactSlider view, Integer color) { + if (view.getThumb() != null) { + if (color == null) { + view.getThumb().clearColorFilter(); + } else { + view.getThumb().setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + } + } + + @ReactProp(name = "minimumTrackTintColor", customType = "Color") + public void setMinimumTrackTintColor(ReactSlider view, Integer color) { + LayerDrawable drawable = (LayerDrawable) view.getProgressDrawable().getCurrent(); + Drawable progress = drawable.findDrawableByLayerId(android.R.id.progress); + if (color == null) { + progress.clearColorFilter(); + } else { + progress.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + } + + @ReactProp(name = "thumbImage") + public void setThumbImage(ReactSlider view, @Nullable ReadableMap source) { + String uri = null; + if (source != null) { + uri = source.getString("uri"); + } + view.setThumbImage(uri); + } + + @ReactProp(name = "maximumTrackTintColor", customType = "Color") + public void setMaximumTrackTintColor(ReactSlider view, Integer color) { + LayerDrawable drawable = (LayerDrawable) view.getProgressDrawable().getCurrent(); + Drawable background = drawable.findDrawableByLayerId(android.R.id.background); + if (color == null) { + background.clearColorFilter(); + } else { + background.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + } + + @ReactProp(name = "inverted", defaultBoolean = false) + public void setInverted(ReactSlider view, boolean inverted) { + if (inverted) view.setScaleX(-1f); + else view.setScaleX(1f); + } + + @ReactProp(name = "accessibilityUnits") + public void setAccessibilityUnits(ReactSlider view, String accessibilityUnits) { + view.setAccessibilityUnits(accessibilityUnits); + } + + @ReactProp(name = "accessibilityIncrements") + public void setAccessibilityIncrements(ReactSlider view, ReadableArray accessibilityIncrements) { + List objectList = accessibilityIncrements.toArrayList(); + List stringList = new ArrayList<>(); + for(Object item: objectList) { + stringList.add((String)item); + } + view.setAccessibilityIncrements(stringList); + } + + @Override + protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSlider view) { + view.setOnSeekBarChangeListener(ON_CHANGE_LISTENER); + } + + @Override + public Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of(ReactSlidingCompleteEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRNCSliderSlidingComplete"), + ReactSlidingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRNCSliderSlidingStart")); + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderPackage.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderPackage.java new file mode 100644 index 0000000000000..fd821664308d4 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSliderPackage.java @@ -0,0 +1,29 @@ +package versioned.host.exp.exponent.modules.api.components.slider; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.bridge.JavaScriptModule; + +public class ReactSliderPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + // Deprecated from RN 0.47 + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + @SuppressWarnings("rawtypes") + public List createViewManagers(ReactApplicationContext reactContext) { + return Arrays.asList(new ReactSliderManager()); + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlidingCompleteEvent.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlidingCompleteEvent.java new file mode 100644 index 0000000000000..0e5c5e2c54616 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlidingCompleteEvent.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.slider; + +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; + +/** + * Event emitted when the user finishes dragging the slider. + */ +public class ReactSlidingCompleteEvent extends Event { + + public static final String EVENT_NAME = "topSlidingComplete"; + + private final double mValue; + + public ReactSlidingCompleteEvent(int viewId, double value) { + super(viewId); + mValue = value; + } + + public double getValue() { + return mValue; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + return 0; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putDouble("value", getValue()); + return eventData; + } + +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlidingStartEvent.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlidingStartEvent.java new file mode 100644 index 0000000000000..7e333c65d52b7 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/slider/ReactSlidingStartEvent.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package versioned.host.exp.exponent.modules.api.components.slider; + +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; + +/** + * Event emitted when the user starts dragging the slider. + */ + +public class ReactSlidingStartEvent extends Event { + public static final String EVENT_NAME = "topSlidingStart"; + + private final double mValue; + + public ReactSlidingStartEvent(int viewId, double value) { + super(viewId); + mValue = value; + } + + public double getValue() { + return mValue; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + return 0; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putDouble("value", getValue()); + return eventData; + } + +} \ No newline at end of file diff --git a/apps/native-component-list/package.json b/apps/native-component-list/package.json index c5b8c65bbd402..d982df3ccd958 100644 --- a/apps/native-component-list/package.json +++ b/apps/native-component-list/package.json @@ -17,6 +17,8 @@ "@react-native-community/masked-view": "0.1.6", "@react-native-community/netinfo": "5.9.0", "@react-native-community/segmented-control": "1.6.1", + "@react-native-community/picker": "1.6.0", + "@react-native-community/slider": "3.0.0-rc.2", "@react-native-community/viewpager": "3.3.0", "@react-navigation/web": "2.0.0-alpha.0", "date-format": "^2.0.0", diff --git a/apps/native-component-list/src/screens/AV/Slider.tsx b/apps/native-component-list/src/screens/AV/Slider.tsx index e34d271e4e3d6..d62913b009818 100644 --- a/apps/native-component-list/src/screens/AV/Slider.tsx +++ b/apps/native-component-list/src/screens/AV/Slider.tsx @@ -1,3 +1,3 @@ -import { Slider } from 'react-native'; +import Slider from '@react-native-community/slider'; export default Slider; diff --git a/apps/native-component-list/src/screens/AV/Slider.web.tsx b/apps/native-component-list/src/screens/AV/Slider.web.tsx deleted file mode 100644 index 38186d30d6997..0000000000000 --- a/apps/native-component-list/src/screens/AV/Slider.web.tsx +++ /dev/null @@ -1,455 +0,0 @@ -/** - * Thanks to Jean Regisser - * Reference: https://github.com/jeanregisser/react-native-slider - * - * @providesModule Slider - */ - -// @ts-ignore -// tslint:disable max-classes-per-file -import applyNativeMethods from 'react-native-web/dist/modules/applyNativeMethods'; -import { - View, - StyleSheet, - Animated, - PanResponder, - Easing, - PanResponderInstance, - StyleProp, - ViewStyle, - LayoutChangeEvent, - GestureResponderEvent, - PanResponderGestureState, -} from 'react-native'; -// import Rect from './Rect'; -import React, { PureComponent } from 'react'; - -class Rect { - x: number; - y: number; - width: number; - height: number; - - constructor(x: number, y: number, width: number, height: number) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - - containsPoint(x: number, y: number) { - return x >= this.x && y >= this.y && x <= this.x + this.width && y <= this.y + this.height; - } -} - -const TRACK_SIZE = 4; -const THUMB_SIZE = 20; -const THUMB_TOUCH_SIZE = 40; - -const DEFAULT_ANIMATION_CONFIGS = { - spring: { - friction: 7, - tension: 100, - }, - timing: { - duration: 150, - easing: Easing.inOut(Easing.ease), - delay: 0, - }, -}; - -interface Props { - /** - * Set to true to animate values with default 'timing' animation type - */ - animateTransitions: boolean; - - /** - * Used to configure the animation parameters. These are the same parameters in the Animated library. - */ - animationConfig: object; - - /** - * Custom Animation type. 'spring' or 'timing'. - */ - animationType: 'spring' | 'timing'; - - /** - * Set this to true to visually see the thumb touch rect in green. - */ - debugTouchArea: boolean; - - disabled: boolean; - maximumTrackTintColor: string; - maximumValue: number; - minimumTrackTintColor: string; - minimumValue: number; - onSlidingComplete: (value: number) => void; - onSlidingStart: () => void; - onValueChange: (value: number) => void; - step: number; - style: StyleProp; - testID: string; - value: number; -} - -interface State { - containerSize: { width: number; height: number }; - trackSize: { width: number; height: number }; - thumbSize: { width: number; height: number }; - allMeasured: boolean; - value: Animated.Value; -} - -class Slider extends PureComponent { - _panResponder?: PanResponderInstance; - _previousLeft?: number; - _store: { [key: string]: { width: number; height: number; [x: string]: any } } = {}; - - static defaultProps = { - value: 0, - minimumValue: 0, - maximumValue: 1, - step: 0, - minimumTrackTintColor: '#009688', - maximumTrackTintColor: '#939393', - debugTouchArea: false, - animationType: 'timing', - }; - - constructor(props: Props) { - super(props); - this.state = { - containerSize: { width: 0, height: 0 }, - trackSize: { width: 0, height: 0 }, - thumbSize: { width: 0, height: 0 }, - allMeasured: false, - value: new Animated.Value(props.value), - }; - this._panResponder = PanResponder.create({ - onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, - onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, - onPanResponderGrant: this._handlePanResponderGrant, - onPanResponderMove: this._handlePanResponderMove, - onPanResponderRelease: this._handlePanResponderEnd, - onPanResponderTerminationRequest: this._handlePanResponderRequestEnd, - onPanResponderTerminate: this._handlePanResponderEnd, - }); - } - - componentDidUpdate(prevProps: Props, _prevState: State) { - if (this.props.value !== prevProps.value) { - if (this.props.animateTransitions) { - this._setCurrentValueAnimated(this.props.value); - } else { - this._setCurrentValue(this.props.value); - } - } - } - - render() { - const { - minimumValue, - maximumValue, - minimumTrackTintColor, - maximumTrackTintColor, - style, - debugTouchArea, - animationType, - ...other - } = this.props; - const { value, containerSize, thumbSize, allMeasured } = this.state; - const thumbLeft = value.interpolate({ - inputRange: [minimumValue, isNaN(maximumValue) ? minimumValue + 0.1 : maximumValue], - outputRange: [0, containerSize.width - thumbSize.width], - }); - const valueVisibleStyle: { opacity?: number } = {}; - if (!allMeasured) { - valueVisibleStyle.opacity = 0; - } - - const minimumTrackStyle = { - position: 'absolute', - width: Animated.add(thumbLeft, thumbSize.width / 2), - backgroundColor: minimumTrackTintColor, - ...valueVisibleStyle, - }; - - const touchOverflowStyle = this._getTouchOverflowStyle(); - - return ( - - - - - - {debugTouchArea === true && this._renderDebugThumbTouchRect(thumbLeft)} - - - ); - } - - _getPropsForComponentUpdate(props: Props) { - const { value, onValueChange, onSlidingStart, onSlidingComplete, style, ...otherProps } = props; - return otherProps; - } - - _handleStartShouldSetPanResponder = ( - e: GestureResponderEvent, - gestureState: PanResponderGestureState - ): boolean => { - // Should we become active when the user presses down on the thumb? - return this._thumbHitTest(e); - }; - - _handleMoveShouldSetPanResponder( - e: GestureResponderEvent, - gestureState: PanResponderGestureState - ): boolean { - // Should we become active when the user moves a touch over the thumb? - return false; - } - - _handlePanResponderGrant = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => { - this._previousLeft = this._getThumbLeft(this._getCurrentValue()); - }; - - _handlePanResponderMove = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => { - if (this.props.disabled) { - return; - } - - this._setCurrentValue(this._getValue(gestureState)); - this._fireChangeEvent('onValueChange'); - }; - - _handlePanResponderRequestEnd(e: GestureResponderEvent, gestureState: PanResponderGestureState) { - // Should we allow another component to take over this pan? - return false; - } - - _handlePanResponderEnd = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => { - if (this.props.disabled) { - return; - } - - this._setCurrentValue(this._getValue(gestureState)); - this._fireChangeEvent('onSlidingComplete'); - }; - - _measureContainer = (x: LayoutChangeEvent) => { - this._handleMeasure('containerSize', x); - }; - - _measureTrack = (x: LayoutChangeEvent) => { - this._handleMeasure('trackSize', x); - }; - - _measureThumb = (x: LayoutChangeEvent) => { - this._handleMeasure('thumbSize', x); - }; - - _handleMeasure = (name: string, x: LayoutChangeEvent) => { - const { width, height } = x.nativeEvent.layout; - const size = { width, height }; - - const currentSize = this._store[name]; - if (currentSize && width === currentSize.width && height === currentSize.height) { - return; - } - this._store[name] = size; - - const store = this._store; - if (store.containerSize && store.trackSize && store.thumbSize) { - this.setState({ - containerSize: store.containerSize, - trackSize: store.trackSize, - thumbSize: store.thumbSize, - allMeasured: true, - }); - } - }; - - _getRatio = (value: number) => { - return (value - this.props.minimumValue) / (this.props.maximumValue - this.props.minimumValue); - }; - - _getThumbLeft = (value: number) => { - const ratio = this._getRatio(value); - return ratio * (this.state.containerSize.width - this.state.thumbSize.width); - }; - - _getValue = (gestureState: PanResponderGestureState) => { - const length = this.state.containerSize.width - this.state.thumbSize.width; - const thumbLeft = this._previousLeft! + gestureState.dx; - - const ratio = thumbLeft / length; - - if (this.props.step) { - return Math.max( - this.props.minimumValue, - Math.min( - this.props.maximumValue, - this.props.minimumValue + - Math.round( - (ratio * (this.props.maximumValue - this.props.minimumValue)) / this.props.step - ) * - this.props.step - ) - ); - } else { - return Math.max( - this.props.minimumValue, - Math.min( - this.props.maximumValue, - ratio * (this.props.maximumValue - this.props.minimumValue) + this.props.minimumValue - ) - ); - } - }; - - _getCurrentValue = () => { - // @ts-ignore - return this.state.value.__getValue(); - }; - - _setCurrentValue = (value: number) => { - this.state.value.setValue(value); - }; - - _setCurrentValueAnimated = (value: number) => { - const animationType = this.props.animationType; - const animationConfig = Object.assign( - {}, - DEFAULT_ANIMATION_CONFIGS[animationType], - this.props.animationConfig, - { toValue: value } - ); - - Animated[animationType](this.state.value, animationConfig).start(); - }; - - _fireChangeEvent = (event: 'onValueChange' | 'onSlidingComplete') => { - if (this.props[event]) { - this.props[event](this._getCurrentValue()); - } - }; - - _getTouchOverflowSize = () => { - const { allMeasured, thumbSize, containerSize } = this.state; - - const size: { width?: number; height?: number } = {}; - if (allMeasured === true) { - size.width = Math.max(0, THUMB_TOUCH_SIZE - thumbSize.width); - size.height = Math.max(0, THUMB_TOUCH_SIZE - containerSize.height); - } - - return size; - }; - - _getTouchOverflowStyle = () => { - const { width, height } = this._getTouchOverflowSize(); - - const touchOverflowStyle: ViewStyle = {}; - if (width !== undefined && height !== undefined) { - const verticalMargin = -height / 2; - touchOverflowStyle.marginTop = verticalMargin; - touchOverflowStyle.marginBottom = verticalMargin; - - const horizontalMargin = -width / 2; - touchOverflowStyle.marginLeft = horizontalMargin; - touchOverflowStyle.marginRight = horizontalMargin; - } - - if (this.props.debugTouchArea === true) { - touchOverflowStyle.backgroundColor = 'orange'; - touchOverflowStyle.opacity = 0.5; - } - - return touchOverflowStyle; - }; - - _thumbHitTest = (e: GestureResponderEvent) => { - const nativeEvent = e.nativeEvent; - const thumbTouchRect = this._getThumbTouchRect(); - return thumbTouchRect.containsPoint(nativeEvent.locationX, nativeEvent.locationY); - }; - - _getThumbTouchRect = () => { - const { thumbSize, containerSize } = this.state; - const touchOverflowSize = this._getTouchOverflowSize(); - - return new Rect( - touchOverflowSize.width! / 2 + - this._getThumbLeft(this._getCurrentValue()) + - (thumbSize.width - THUMB_TOUCH_SIZE) / 2, - touchOverflowSize.height! / 2 + (containerSize.height - THUMB_TOUCH_SIZE) / 2, - THUMB_TOUCH_SIZE, - THUMB_TOUCH_SIZE - ); - }; - - _renderDebugThumbTouchRect = (thumbLeft: Animated.AnimatedInterpolation) => { - const thumbTouchRect = this._getThumbTouchRect(); - const positionStyle = { - left: thumbLeft, - top: thumbTouchRect.y, - width: thumbTouchRect.width, - height: thumbTouchRect.height, - }; - - return ( - - ); - }; -} - -const defaultStyles = StyleSheet.create({ - container: { - height: 40, - justifyContent: 'center', - }, - track: { - height: TRACK_SIZE, - borderRadius: TRACK_SIZE / 2, - }, - thumb: { - position: 'absolute', - width: THUMB_SIZE, - height: THUMB_SIZE, - borderRadius: THUMB_SIZE / 2, - }, - touchArea: { - position: 'absolute', - backgroundColor: 'transparent', - top: 0, - left: 0, - right: 0, - bottom: 0, - }, - debugThumbTouchArea: { - position: 'absolute', - backgroundColor: 'green', - opacity: 0.5, - }, -}); - -export default applyNativeMethods(Slider); diff --git a/apps/native-component-list/src/screens/AppleAuthenticationScreen.tsx b/apps/native-component-list/src/screens/AppleAuthenticationScreen.tsx index c35d844f6aef8..1ab0712fb373e 100644 --- a/apps/native-component-list/src/screens/AppleAuthenticationScreen.tsx +++ b/apps/native-component-list/src/screens/AppleAuthenticationScreen.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import Slider from '@react-native-community/slider'; import { Subscription } from '@unimodules/core'; import * as AppleAuthentication from 'expo-apple-authentication'; -import { Alert, AsyncStorage, ScrollView, StyleSheet, View, Text, Button, Slider } from 'react-native'; +import React from 'react'; +import { Alert, AsyncStorage, ScrollView, StyleSheet, View, Text, Button } from 'react-native'; import MonoText from '../components/MonoText'; diff --git a/apps/native-component-list/src/screens/BrightnessScreen.tsx b/apps/native-component-list/src/screens/BrightnessScreen.tsx index 2a9018f715fc5..334aa16e3674e 100644 --- a/apps/native-component-list/src/screens/BrightnessScreen.tsx +++ b/apps/native-component-list/src/screens/BrightnessScreen.tsx @@ -1,9 +1,11 @@ -import React from 'react'; -import { ScrollView, Text, View, Slider } from 'react-native'; +import Slider from '@react-native-community/slider'; import * as Brightness from 'expo-brightness'; import * as Permissions from 'expo-permissions'; -import HeadingText from '../components/HeadingText'; +import React from 'react'; +import { ScrollView, Text, View } from 'react-native'; + import Button from '../components/Button'; +import HeadingText from '../components/HeadingText'; interface State { initBrightness: { [type: string]: number }; diff --git a/apps/native-component-list/src/screens/GL/GLHeadlessRenderingScreen.tsx b/apps/native-component-list/src/screens/GL/GLHeadlessRenderingScreen.tsx index 42caaa06a187a..1e3a399227a94 100644 --- a/apps/native-component-list/src/screens/GL/GLHeadlessRenderingScreen.tsx +++ b/apps/native-component-list/src/screens/GL/GLHeadlessRenderingScreen.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import Slider from '@react-native-community/slider'; +import { Asset } from 'expo-asset'; +import * as FileSystem from 'expo-file-system'; import * as GL from 'expo-gl'; import { GLView } from 'expo-gl'; -import * as FileSystem from 'expo-file-system'; -import { Asset } from 'expo-asset'; -import { Image, Slider, StyleSheet, Text, View } from 'react-native'; +import React from 'react'; +import { Image, StyleSheet, Text, View } from 'react-native'; import exampleImage from '../../../assets/images/example3.jpg'; @@ -175,7 +176,7 @@ export default class GLHeadlessRenderingScreen extends React.PureComponent<{}, S this.setState({ contrast }, () => { this.draw(); }); - } + }; render() { const { contrast, snapshot } = this.state; @@ -197,7 +198,10 @@ export default class GLHeadlessRenderingScreen extends React.PureComponent<{}, S /> )} - {`Contrast: ${parseInt(String(contrast * 100), 10)}%`} + {`Contrast: ${parseInt( + String(contrast * 100), + 10 + )}%`} any) => ({ name, getJson }); const EXAMPLES = [ - makeExample('Hamburger Arrow', () => - require('../../assets/animations/HamburgerArrow.json') - ), - makeExample('Line Animation', () => - require('../../assets/animations/LineAnimation.json') - ), - makeExample('Lottie Logo 1', () => - require('../../assets/animations/LottieLogo1.json') - ), - makeExample('Lottie Logo 2', () => - require('../../assets/animations/LottieLogo2.json') - ), + makeExample('Hamburger Arrow', () => require('../../assets/animations/HamburgerArrow.json')), + makeExample('Line Animation', () => require('../../assets/animations/LineAnimation.json')), + makeExample('Lottie Logo 1', () => require('../../assets/animations/LottieLogo1.json')), + makeExample('Lottie Logo 2', () => require('../../assets/animations/LottieLogo2.json')), makeExample('Lottie Walkthrough', () => require('../../assets/animations/LottieWalkthrough.json') ), - makeExample('Pin Jump', () => - require('../../assets/animations/PinJump.json') - ), - makeExample('Twitter Heart', () => - require('../../assets/animations/TwitterHeart.json') - ), - makeExample('Watermelon', () => - require('../../assets/animations/Watermelon.json') - ), - makeExample('Motion Corpse', () => - require('../../assets/animations/MotionCorpse-Jrcanest.json') - ), + makeExample('Pin Jump', () => require('../../assets/animations/PinJump.json')), + makeExample('Twitter Heart', () => require('../../assets/animations/TwitterHeart.json')), + makeExample('Watermelon', () => require('../../assets/animations/Watermelon.json')), + makeExample('Motion Corpse', () => require('../../assets/animations/MotionCorpse-Jrcanest.json')), ].reduce<{ [key: string]: { name: string; getJson: () => any } }>( (acc, e) => ({ ...acc, @@ -52,13 +36,9 @@ const EXAMPLES = [ const ExamplePicker: React.FunctionComponent<{ value: string; - onChange: (value: string) => void; + onChange: (value: any, index?: number) => void; }> = ({ value, onChange }) => ( - + {Object.values(EXAMPLES).map(({ name }) => ( ))} @@ -72,14 +52,7 @@ const PlayerControls: React.FunctionComponent<{ onConfigChange: (config: Config) => void; config: Config; progress: Animated.Value; -}> = ({ - onPlayPress, - onResetPress, - onProgressChange, - onConfigChange, - config, - progress, -}) => ( +}> = ({ onPlayPress, onResetPress, onProgressChange, onConfigChange, config, progress }) => ( @@ -92,8 +65,7 @@ const PlayerControls: React.FunctionComponent<{ flexDirection: 'row', justifyContent: 'space-between', paddingBottom: 10, - }} - > + }}> Use Imperative API: { if (finished) this.forceUpdate(); }); } - } + }; onResetPress = () => { if (this.state.config.imperative && this.anim) { @@ -182,11 +154,11 @@ export default class LottieScreen extends React.Component<{}, State> { } }); } - } + }; setAnim = (anim: any) => { this.anim = anim; - } + }; render() { return ( @@ -201,9 +173,7 @@ export default class LottieScreen extends React.Component<{}, State> { ref={this.setAnim} style={styles.animation} source={EXAMPLES[this.state.exampleName].getJson()} - progress={ - this.state.config.imperative ? undefined : this.state.progress - } + progress={this.state.config.imperative ? undefined : this.state.progress} /> diff --git a/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.android.tsx b/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.android.tsx index 66081d808e8ed..48bfb86c2eed9 100644 --- a/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.android.tsx +++ b/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.android.tsx @@ -1,14 +1,13 @@ +import { Picker } from '@react-native-community/picker'; +import Slider from '@react-native-community/slider'; import React from 'react'; - import { ActivityIndicator, Alert, DrawerLayoutAndroid, Image, - Picker, ProgressBarAndroid, RefreshControl, - Slider, Switch, StatusBar, SectionList, @@ -23,10 +22,10 @@ import { TouchableOpacityProps, } from 'react-native'; // @ts-ignore -import TouchableBounce from 'react-native/Libraries/Components/Touchable/TouchableBounce'; +import WebView from 'react-native-webview'; // @ts-ignore +import TouchableBounce from 'react-native/Libraries/Components/Touchable/TouchableBounce'; import { ScrollView as NavigationScrollView } from 'react-navigation'; -import WebView from 'react-native-webview'; import { Colors, Layout } from '../../constants'; import ModalExample from '../ModalExample'; @@ -45,7 +44,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { isRefreshing: false, }; - sections: Array<{ title: string, data: Array<() => JSX.Element> }>; + sections: Array<{ title: string; data: Array<() => JSX.Element> }>; constructor(props: any) { super(props); @@ -74,7 +73,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { this.setState({ isRefreshing: false }); }, 3000); this.setState({ isRefreshing: true, timeoutId: timeout }); - } + }; componentWillUnmount() { clearTimeout(this.state.timeoutId); @@ -88,8 +87,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', - }} - > + }}> DrawerLayoutAndroid ); @@ -99,8 +97,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { drawerWidth={300} // @ts-ignore drawerPosition="left" - renderNavigationView={renderNavigationView} - > + renderNavigationView={renderNavigationView}> { _renderItem = ({ item }: any) => { return {item()}; - } + }; _renderSectionHeader = ({ section: { title } }: any) => { return ( @@ -130,11 +127,11 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { {title} ); - } + }; _renderModal = () => { return ; - } + }; _renderVerticalScrollView = () => { return ( @@ -145,7 +142,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { ); - } + }; _renderDrawerLayout = () => { return ( @@ -153,7 +150,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { Swipe from the left of the screen to see the drawer. ); - } + }; _renderActivityIndicator = () => { const Spacer = () => ; @@ -168,7 +165,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { ); - } + }; _renderAlert = () => { const showAlert = () => { @@ -191,7 +188,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { ); - } + }; _renderHorizontalScrollView = () => { const imageStyle = { @@ -218,11 +215,11 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { /> ); - } + }; _renderPicker = () => { return ; - } + }; _renderProgressBar = () => { return ( @@ -233,11 +230,11 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { ); - } + }; _renderSlider = () => { return ; - } + }; _renderStatusBar = () => { const randomAnimation = () => { @@ -259,11 +256,11 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { ); - } + }; _renderSwitch = () => { return ; - } + }; _renderText = () => { const linkStyle = { color: Colors.tintColor, marginVertical: 3 }; @@ -282,11 +279,11 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { ); - } + }; _renderTextInput = () => { return ; - } + }; _renderTouchables = () => { const buttonStyle = { @@ -307,8 +304,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { {}} - > + onPress={() => {}}> Highlight! @@ -321,8 +317,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { {}} - delayPressIn={0} - > + delayPressIn={0}> Native feedback! @@ -334,7 +329,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { ); - } + }; _renderWebView = () => { return ( @@ -359,7 +354,7 @@ export default class ReactNativeCoreScreen extends React.Component<{}, State> { /> ); - } + }; } class PickerExample extends React.Component { @@ -371,8 +366,7 @@ class PickerExample extends React.Component { return ( this.setState({ language: lang })} - > + onValueChange={lang => this.setState({ language: lang })}> diff --git a/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.ios.tsx b/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.ios.tsx index 417174094167b..46daac387dbdf 100644 --- a/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.ios.tsx +++ b/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.ios.tsx @@ -1,15 +1,15 @@ -import React from 'react'; +import { Picker } from '@react-native-community/picker'; +import Slider from '@react-native-community/slider'; +import * as React from 'react'; import { ActionSheetIOS, ActivityIndicator, Alert, Image, - Picker, ProgressViewIOS, RefreshControl, SectionList, SegmentedControlIOS, - Slider, Switch, StatusBar, ScrollView, @@ -21,11 +21,11 @@ import { View, TouchableOpacityProps, } from 'react-native'; +import WebView from 'react-native-webview'; // @ts-ignore import TouchableBounce from 'react-native/Libraries/Components/Touchable/TouchableBounce'; // @ts-ignore import { NavigationScreenProps, ScrollView as NavigationScrollView } from 'react-navigation'; -import WebView from 'react-native-webview'; import Colors from '../../constants/Colors'; import Layout from '../../constants/Layout'; @@ -40,7 +40,7 @@ export default class ReactNativeCoreScreen extends React.Component JSX.Element> }>; + sections: { title: string; data: (() => JSX.Element)[] }[]; _sectionList?: React.Component; diff --git a/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.web.tsx b/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.web.tsx index b1b014628efdc..1f25651d97156 100644 --- a/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.web.tsx +++ b/apps/native-component-list/src/screens/ReactNativeCore/ReactNativeCoreScreen.web.tsx @@ -1,3 +1,4 @@ +import Slider from '@react-native-community/slider'; import React from 'react'; import { ActivityIndicator, @@ -5,7 +6,6 @@ import { Image, Picker, RefreshControl, - Slider, Switch, StatusBar, SectionList, diff --git a/docs/pages/versions/unversioned/sdk/picker.md b/docs/pages/versions/unversioned/sdk/picker.md new file mode 100644 index 0000000000000..c33158ae48004 --- /dev/null +++ b/docs/pages/versions/unversioned/sdk/picker.md @@ -0,0 +1,22 @@ +--- +title: Picker +sourceCodeUrl: 'https://github.com/react-native-community/react-native-picker' +--- + +import InstallSection from '~/components/plugins/InstallSection'; +import PlatformsSection from '~/components/plugins/PlatformsSection'; +import Video from '~/components/plugins/Video' + + + +A component that provides access to the system UI for picking between several options. + + + +## Installation + + + +## Usage + +See full documentation at [react-native-community/react-native-picker](https://github.com/react-native-community/react-native-picker). diff --git a/docs/pages/versions/unversioned/sdk/slider.md b/docs/pages/versions/unversioned/sdk/slider.md new file mode 100644 index 0000000000000..dad90e968928e --- /dev/null +++ b/docs/pages/versions/unversioned/sdk/slider.md @@ -0,0 +1,22 @@ +--- +title: Slider +sourceCodeUrl: 'https://github.com/react-native-community/react-native-slider' +--- + +import InstallSection from '~/components/plugins/InstallSection'; +import PlatformsSection from '~/components/plugins/PlatformsSection'; +import Video from '~/components/plugins/Video' + + + +A component that provides access to the system UI for a slider control, that allows users to pick among a range of values by dragging an anchor. + + + +## Installation + + + +## Usage + +See full documentation at [react-native-community/react-native-slider](https://github.com/react-native-community/react-native-slider). diff --git a/ios/Exponent.xcodeproj/project.pbxproj b/ios/Exponent.xcodeproj/project.pbxproj index 393863b237afb..5b42fb5434aa4 100644 --- a/ios/Exponent.xcodeproj/project.pbxproj +++ b/ios/Exponent.xcodeproj/project.pbxproj @@ -222,6 +222,7 @@ B236C4F324740C1E00D0CB66 /* EXScopedNotificationSchedulerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = B236C4F224740C1E00D0CB66 /* EXScopedNotificationSchedulerModule.m */; }; B23D9AA324758C6600D09AC8 /* EXScopedNotificationPresentationModule.m in Sources */ = {isa = PBXBuildFile; fileRef = B23D9AA224758C6600D09AC8 /* EXScopedNotificationPresentationModule.m */; }; B2B492172462F5EF001576D8 /* EXScopedNotificationsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B2B492162462F5EF001576D8 /* EXScopedNotificationsEmitter.m */; }; + B2B492172462F5EF001576D8 /* EXScopedNotificationsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B2B492162462F5EF001576D8 /* EXScopedNotificationsEmitter.m */; }; B2F7C02B246B09890060EE06 /* EXScopedNotificationsHandlerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = B2F7C02A246B09890060EE06 /* EXScopedNotificationsHandlerModule.m */; }; B2F7C02E246B09AE0060EE06 /* EXScopedNotificationsUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = B2F7C02D246B09AE0060EE06 /* EXScopedNotificationsUtils.m */; }; B4FF3D8631BB48D290E44742 /* RNCConnectionState.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FE9493F87604B4B8CBB87C8 /* RNCConnectionState.m */; settings = {COMPILER_FLAGS = "-w"; }; }; @@ -348,6 +349,10 @@ F581B81CB601426CB5658AEE /* RNSharedElementCornerRadii.m in Sources */ = {isa = PBXBuildFile; fileRef = 780F0BF062134A048B33344C /* RNSharedElementCornerRadii.m */; settings = {COMPILER_FLAGS = "-w"; }; }; F77DDB931E04AC1100624CA2 /* SafariServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F77DDB921E04AC1100624CA2 /* SafariServices.framework */; }; FA526593B78C87A7E1C34561 /* libPods-Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D8BAB8D6102FB06BE5BE10B /* libPods-Tests.a */; }; + E7A65010FA85413EA748C757 /* RNCPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 26AEDCCD9FDB4296B2B4AF5E /* RNCPicker.m */; settings = {COMPILER_FLAGS = "-w"; }; }; + E2BF7EA6366243D98A78D9E8 /* RNCPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 93C9E2950F474416B3F7A60E /* RNCPickerManager.m */; settings = {COMPILER_FLAGS = "-w"; }; }; + ABC18CFCC0224BF0B95B691A /* RNCSlider.m in Sources */ = {isa = PBXBuildFile; fileRef = EC7E234A4A564DFE84B2B1D0 /* RNCSlider.m */; settings = {COMPILER_FLAGS = "-w"; }; }; + BCB61D782B4944E89A1F0371 /* RNCSliderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CCF7710E4F58478DA6A63F09 /* RNCSliderManager.m */; settings = {COMPILER_FLAGS = "-w"; }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1064,6 +1069,14 @@ F502F71E43A44658A6B20C26 /* RNCSegmentedControlManager.m */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; path = RNCSegmentedControlManager.m; sourceTree = ""; }; F77DDB921E04AC1100624CA2 /* SafariServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SafariServices.framework; path = System/Library/Frameworks/SafariServices.framework; sourceTree = SDKROOT; }; FA872B0AA66044B4B8C574F0 /* RNSScreenStack.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; path = RNSScreenStack.h; sourceTree = ""; }; + C9B790CD211D4E869D7099DD /* RNCPicker.h */ = {isa = PBXFileReference; name = "RNCPicker.h"; path = "RNCPicker.h"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; explicitFileType = undefined; includeInIndex = 0; }; + 26AEDCCD9FDB4296B2B4AF5E /* RNCPicker.m */ = {isa = PBXFileReference; name = "RNCPicker.m"; path = "RNCPicker.m"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; explicitFileType = undefined; includeInIndex = 0; }; + 55321D87F6F24AA280C9C7E7 /* RNCPickerManager.h */ = {isa = PBXFileReference; name = "RNCPickerManager.h"; path = "RNCPickerManager.h"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; explicitFileType = undefined; includeInIndex = 0; }; + 93C9E2950F474416B3F7A60E /* RNCPickerManager.m */ = {isa = PBXFileReference; name = "RNCPickerManager.m"; path = "RNCPickerManager.m"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; explicitFileType = undefined; includeInIndex = 0; }; + F1E9894638084CB4956C637C /* RNCSlider.h */ = {isa = PBXFileReference; name = "RNCSlider.h"; path = "RNCSlider.h"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; explicitFileType = undefined; includeInIndex = 0; }; + EC7E234A4A564DFE84B2B1D0 /* RNCSlider.m */ = {isa = PBXFileReference; name = "RNCSlider.m"; path = "RNCSlider.m"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; explicitFileType = undefined; includeInIndex = 0; }; + 1CF1D8E66E5B4C7A9476C145 /* RNCSliderManager.h */ = {isa = PBXFileReference; name = "RNCSliderManager.h"; path = "RNCSliderManager.h"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; explicitFileType = undefined; includeInIndex = 0; }; + CCF7710E4F58478DA6A63F09 /* RNCSliderManager.m */ = {isa = PBXFileReference; name = "RNCSliderManager.m"; path = "RNCSliderManager.m"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2080,6 +2093,8 @@ 6999211AF26A46738FB9E106 /* MaskedView */, 69CAB43F632147D3A686A75B /* ViewPager */, F10D02089BA74515B49FBBC2 /* SegmentedControl */, + 45255D23E23C4C79A634FC92 /* Picker */, + 0E05846EF744409D93CC1ED6 /* Slider */, ); path = Components; sourceTree = ""; @@ -2385,6 +2400,30 @@ path = "Build-Phases"; sourceTree = ""; }; + 45255D23E23C4C79A634FC92 /* Picker */ = { + isa = PBXGroup; + children = ( + C9B790CD211D4E869D7099DD /* RNCPicker.h */, + 26AEDCCD9FDB4296B2B4AF5E /* RNCPicker.m */, + 55321D87F6F24AA280C9C7E7 /* RNCPickerManager.h */, + 93C9E2950F474416B3F7A60E /* RNCPickerManager.m */, + ); + name = Picker; + path = Picker; + sourceTree = ""; + }; + 0E05846EF744409D93CC1ED6 /* Slider */ = { + isa = PBXGroup; + children = ( + F1E9894638084CB4956C637C /* RNCSlider.h */, + EC7E234A4A564DFE84B2B1D0 /* RNCSlider.m */, + 1CF1D8E66E5B4C7A9476C145 /* RNCSliderManager.h */, + CCF7710E4F58478DA6A63F09 /* RNCSliderManager.m */, + ); + name = Slider; + path = Slider; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -3002,6 +3041,10 @@ F581B81CB601426CB5658AEE /* RNSharedElementCornerRadii.m in Sources */, 97635343A010490BBF92C76F /* RNCSegmentedControl.m in Sources */, E75397FDFB6B4FF780EA612A /* RNCSegmentedControlManager.m in Sources */, + E7A65010FA85413EA748C757 /* RNCPicker.m in Sources */, + E2BF7EA6366243D98A78D9E8 /* RNCPickerManager.m in Sources */, + ABC18CFCC0224BF0B95B691A /* RNCSlider.m in Sources */, + BCB61D782B4944E89A1F0371 /* RNCSliderManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPicker.h b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPicker.h new file mode 100644 index 0000000000000..64866881b8136 --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPicker.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@interface RNCPicker : UIPickerView + +@property (nonatomic, copy) NSArray *items; +@property (nonatomic, assign) NSInteger selectedIndex; + +@property (nonatomic, strong) UIColor *color; +@property (nonatomic, strong) UIFont *font; +@property (nonatomic, assign) NSTextAlignment textAlign; + +@property (nonatomic, copy) RCTBubblingEventBlock onChange; + +@end diff --git a/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPicker.m b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPicker.m new file mode 100644 index 0000000000000..c6805a44ee631 --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPicker.m @@ -0,0 +1,112 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNCPicker.h" + +#import +#import + +@interface RNCPicker() +@end + +@implementation RNCPicker + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { + _color = [UIColor blackColor]; + _font = [UIFont systemFontOfSize:21]; // TODO: selected title default should be 23.5 + _selectedIndex = NSNotFound; + _textAlign = NSTextAlignmentCenter; + self.delegate = self; + [self selectRow:0 inComponent:0 animated:YES]; // Workaround for missing selection indicator lines (see https://stackoverflow.com/questions/39564660/uipickerview-selection-indicator-not-visible-in-ios10) + } + return self; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) + +- (void)setItems:(NSArray *)items +{ + _items = [items copy]; + [self setNeedsLayout]; +} + +- (void)setSelectedIndex:(NSInteger)selectedIndex +{ + if (_selectedIndex != selectedIndex) { + BOOL animated = _selectedIndex != NSNotFound; // Don't animate the initial value + _selectedIndex = selectedIndex; + dispatch_async(dispatch_get_main_queue(), ^{ + [self selectRow:selectedIndex inComponent:0 animated:animated]; + }); + } +} + +#pragma mark - UIPickerViewDataSource protocol + +- (NSInteger)numberOfComponentsInPickerView:(__unused UIPickerView *)pickerView +{ + return 1; +} + +- (NSInteger)pickerView:(__unused UIPickerView *)pickerView +numberOfRowsInComponent:(__unused NSInteger)component +{ + return _items.count; +} + +#pragma mark - UIPickerViewDelegate methods + +- (NSString *)pickerView:(__unused UIPickerView *)pickerView + titleForRow:(NSInteger)row + forComponent:(__unused NSInteger)component +{ + return [RCTConvert NSString:_items[row][@"label"]]; +} + +- (CGFloat)pickerView:(__unused UIPickerView *)pickerView rowHeightForComponent:(NSInteger)__unused component { + return _font.pointSize + 19; +} + +- (UIView *)pickerView:(UIPickerView *)pickerView + viewForRow:(NSInteger)row + forComponent:(NSInteger)component + reusingView:(UILabel *)label +{ + if (!label) { + label = [[UILabel alloc] initWithFrame:(CGRect){ + CGPointZero, + { + [pickerView rowSizeForComponent:component].width, + [pickerView rowSizeForComponent:component].height, + } + }]; + } + + label.font = _font; + + label.textColor = [RCTConvert UIColor:_items[row][@"textColor"]] ?: _color; + + label.textAlignment = _textAlign; + label.text = [self pickerView:pickerView titleForRow:row forComponent:component]; + return label; +} + +- (void)pickerView:(__unused UIPickerView *)pickerView + didSelectRow:(NSInteger)row inComponent:(__unused NSInteger)component +{ + _selectedIndex = row; + if (_onChange && _items.count > (NSUInteger)row) { + _onChange(@{ + @"newIndex": @(row), + @"newValue": RCTNullIfNil(_items[row][@"value"]), + }); + } +} + +@end diff --git a/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPickerManager.h b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPickerManager.h new file mode 100644 index 0000000000000..6d8aea2a9f02a --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPickerManager.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface RNCPickerManager : RCTViewManager + +@end diff --git a/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPickerManager.m b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPickerManager.m new file mode 100644 index 0000000000000..b5e930c425d53 --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Picker/RNCPickerManager.m @@ -0,0 +1,45 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNCPickerManager.h" +#import "RNCPicker.h" + +#import +#import + +@implementation RNCPickerManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [RNCPicker new]; +} + +RCT_EXPORT_VIEW_PROPERTY(items, NSArray) +RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(color, UIColor) +RCT_EXPORT_VIEW_PROPERTY(textAlign, NSTextAlignment) +RCT_CUSTOM_VIEW_PROPERTY(fontSize, NSNumber, RNCPicker) +{ + view.font = [RCTFont updateFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; +} +RCT_CUSTOM_VIEW_PROPERTY(fontWeight, NSString, __unused RNCPicker) +{ + view.font = [RCTFont updateFont:view.font withWeight:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontStyle, NSString, __unused RNCPicker) +{ + view.font = [RCTFont updateFont:view.font withStyle:json]; // defaults to normal +} +RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RNCPicker) +{ + view.font = [RCTFont updateFont:view.font withFamily:json ?: defaultView.font.familyName]; +} + +@end diff --git a/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSlider.h b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSlider.h new file mode 100644 index 0000000000000..1834f2067ba6e --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSlider.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@interface RNCSlider : UISlider + +@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderValueChange; +@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderSlidingStart; +@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderSlidingComplete; + +@property (nonatomic, assign) float step; +@property (nonatomic, assign) float lastValue; + +@property (nonatomic, strong) UIImage *trackImage; +@property (nonatomic, strong) UIImage *minimumTrackImage; +@property (nonatomic, strong) UIImage *maximumTrackImage; +@property (nonatomic, strong) UIImage *thumbImage; +@property (nonatomic, strong) NSString *accessibilityUnits; +@property (nonatomic, strong) NSArray *accessibilityIncrements; + +@end diff --git a/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSlider.m b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSlider.m new file mode 100644 index 0000000000000..c06a67254f523 --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSlider.m @@ -0,0 +1,130 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNCSlider.h" + +@implementation RNCSlider +{ + float _unclippedValue; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + return [super initWithFrame:frame]; +} + +- (void)setValue:(float)value +{ + _unclippedValue = value; + super.value = value; + [self setupAccessibility:value]; +} + +- (void)setValue:(float)value animated:(BOOL)animated +{ + _unclippedValue = value; + [super setValue:value animated:animated]; + [self setupAccessibility:value]; +} + +- (void)setupAccessibility:(float)value +{ + if (self.accessibilityUnits && self.accessibilityIncrements && [self.accessibilityIncrements count] - 1 == (int)self.maximumValue) { + int index = (int)value; + NSString *sliderValue = (NSString *)[self.accessibilityIncrements objectAtIndex:index]; + NSUInteger stringLength = [self.accessibilityUnits length]; + + NSString *spokenUnits = [NSString stringWithString:self.accessibilityUnits]; + if (sliderValue && [sliderValue intValue] == 1) { + spokenUnits = [spokenUnits substringToIndex:stringLength-1]; + } + + self.accessibilityValue = [NSString stringWithFormat:@"%@ %@", sliderValue, spokenUnits]; + } +} + +- (void)setMinimumValue:(float)minimumValue +{ + super.minimumValue = minimumValue; + super.value = _unclippedValue; +} + +- (void)setMaximumValue:(float)maximumValue +{ + super.maximumValue = maximumValue; + super.value = _unclippedValue; +} + +- (void)setTrackImage:(UIImage *)trackImage +{ + if (trackImage != _trackImage) { + _trackImage = trackImage; + CGFloat width = trackImage.size.width / 2; + UIImage *minimumTrackImage = [trackImage resizableImageWithCapInsets:(UIEdgeInsets){ + 0, width, 0, width + } resizingMode:UIImageResizingModeStretch]; + UIImage *maximumTrackImage = [trackImage resizableImageWithCapInsets:(UIEdgeInsets){ + 0, width, 0, width + } resizingMode:UIImageResizingModeStretch]; + [self setMinimumTrackImage:minimumTrackImage forState:UIControlStateNormal]; + [self setMaximumTrackImage:maximumTrackImage forState:UIControlStateNormal]; + } +} + +- (void)setMinimumTrackImage:(UIImage *)minimumTrackImage +{ + _trackImage = nil; + minimumTrackImage = [minimumTrackImage resizableImageWithCapInsets:(UIEdgeInsets){ + 0, minimumTrackImage.size.width, 0, 0 + } resizingMode:UIImageResizingModeStretch]; + [self setMinimumTrackImage:minimumTrackImage forState:UIControlStateNormal]; +} + +- (UIImage *)minimumTrackImage +{ + return [self thumbImageForState:UIControlStateNormal]; +} + +- (void)setMaximumTrackImage:(UIImage *)maximumTrackImage +{ + _trackImage = nil; + maximumTrackImage = [maximumTrackImage resizableImageWithCapInsets:(UIEdgeInsets){ + 0, 0, 0, maximumTrackImage.size.width + } resizingMode:UIImageResizingModeStretch]; + [self setMaximumTrackImage:maximumTrackImage forState:UIControlStateNormal]; +} + +- (UIImage *)maximumTrackImage +{ + return [self thumbImageForState:UIControlStateNormal]; +} + +- (void)setThumbImage:(UIImage *)thumbImage +{ + [self setThumbImage:thumbImage forState:UIControlStateNormal]; +} + +- (UIImage *)thumbImage +{ + return [self thumbImageForState:UIControlStateNormal]; +} + +- (void)setInverted:(BOOL)inverted +{ + if (inverted) { + self.transform = CGAffineTransformMakeScale(-1, 1); + } else { + self.transform = CGAffineTransformMakeScale(1, 1); + } +} + +- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event +{ + return YES; +} + +@end diff --git a/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSliderManager.h b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSliderManager.h new file mode 100644 index 0000000000000..b161d8525ca5d --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSliderManager.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface RNCSliderManager : RCTViewManager + +@end diff --git a/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSliderManager.m b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSliderManager.m new file mode 100644 index 0000000000000..b28d8174ac4ec --- /dev/null +++ b/ios/Exponent/Versioned/Core/Api/Components/Slider/RNCSliderManager.m @@ -0,0 +1,114 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNCSliderManager.h" + +#import +#import +#import "RNCSlider.h" +#import + +@implementation RNCSliderManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + RNCSlider *slider = [RNCSlider new]; + [slider addTarget:self action:@selector(sliderValueChanged:) + forControlEvents:UIControlEventValueChanged]; + [slider addTarget:self action:@selector(sliderTouchStart:) + forControlEvents:UIControlEventTouchDown]; + [slider addTarget:self action:@selector(sliderTouchEnd:) + forControlEvents:(UIControlEventTouchUpInside | + UIControlEventTouchUpOutside | + UIControlEventTouchCancel)]; + return slider; +} + +static void RNCSendSliderEvent(RNCSlider *sender, BOOL continuous, BOOL isSlidingStart) +{ + float value = sender.value; + + if (sender.step > 0 && + sender.step <= (sender.maximumValue - sender.minimumValue)) { + + value = + MAX(sender.minimumValue, + MIN(sender.maximumValue, + sender.minimumValue + round((sender.value - sender.minimumValue) / sender.step) * sender.step + ) + ); + + [sender setValue:value animated:NO]; + } + + if (continuous) { + if (sender.onRNCSliderValueChange && sender.lastValue != value) { + sender.onRNCSliderValueChange(@{ + @"value": @(value), + }); + } + } else { + if (sender.onRNCSliderSlidingComplete && !isSlidingStart) { + sender.onRNCSliderSlidingComplete(@{ + @"value": @(value), + }); + } + if (sender.onRNCSliderSlidingStart && isSlidingStart) { + sender.onRNCSliderSlidingStart(@{ + @"value": @(value), + }); + } + } + + sender.lastValue = value; +} + +- (void)sliderValueChanged:(RNCSlider *)sender +{ + RNCSendSliderEvent(sender, YES, NO); +} + +- (void)sliderTouchStart:(RNCSlider *)sender +{ + RNCSendSliderEvent(sender, NO, YES); +} + +- (void)sliderTouchEnd:(RNCSlider *)sender +{ + RNCSendSliderEvent(sender, NO, NO); +} + +RCT_EXPORT_VIEW_PROPERTY(value, float); +RCT_EXPORT_VIEW_PROPERTY(step, float); +RCT_EXPORT_VIEW_PROPERTY(trackImage, UIImage); +RCT_EXPORT_VIEW_PROPERTY(minimumTrackImage, UIImage); +RCT_EXPORT_VIEW_PROPERTY(maximumTrackImage, UIImage); +RCT_EXPORT_VIEW_PROPERTY(minimumValue, float); +RCT_EXPORT_VIEW_PROPERTY(maximumValue, float); +RCT_EXPORT_VIEW_PROPERTY(minimumTrackTintColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(maximumTrackTintColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(onRNCSliderValueChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onRNCSliderSlidingStart, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onRNCSliderSlidingComplete, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(thumbTintColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(thumbImage, UIImage); +RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL); +RCT_EXPORT_VIEW_PROPERTY(accessibilityUnits, NSString); +RCT_EXPORT_VIEW_PROPERTY(accessibilityIncrements, NSArray); + +RCT_CUSTOM_VIEW_PROPERTY(disabled, BOOL, RNCSlider) +{ + if (json) { + view.enabled = !([RCTConvert BOOL:json]); + } else { + view.enabled = defaultView.enabled; + } +} + +@end diff --git a/packages/expo/bundledNativeModules.json b/packages/expo/bundledNativeModules.json index 0bdf17bceb5d5..54373570a7f75 100644 --- a/packages/expo/bundledNativeModules.json +++ b/packages/expo/bundledNativeModules.json @@ -99,5 +99,7 @@ "unimodules-app-loader": "~1.0.2", "expo-notifications": "~0.1.7", "expo-splash-screen": "~0.2.3", - "firebase": "7.9.0" + "firebase": "7.9.0", + "@react-native-community/picker": "1.6.0", + "@react-native-community/slider": "3.0.0-rc.2" } diff --git a/tools/expotools/src/commands/UpdateVendoredModule.ts b/tools/expotools/src/commands/UpdateVendoredModule.ts index 4bb5366a2af46..efde8e1bba6f0 100644 --- a/tools/expotools/src/commands/UpdateVendoredModule.ts +++ b/tools/expotools/src/commands/UpdateVendoredModule.ts @@ -38,6 +38,7 @@ interface VendoredModuleUpdateStep { interface VendoredModuleConfig { repoUrl: string; packageName?: string; + packageJsonPath?: string; installableInManagedApps?: boolean; semverPrefix?: '~' | '^'; skipCleanup?: boolean; @@ -343,6 +344,35 @@ const vendoredModulesConfig: { [key: string]: VendoredModuleConfig } = { }, ], }, + '@react-native-community/picker': { + repoUrl: 'https://github.com/react-native-community/react-native-picker', + installableInManagedApps: true, + steps: [ + { + sourceIosPath: 'ios', + targetIosPath: 'Api/Components/Picker', + sourceAndroidPath: 'android/src/main/java/com/reactnativecommunity/picker', + targetAndroidPath: 'modules/api/components/picker', + sourceAndroidPackage: 'com.reactnativecommunity.picker', + targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.picker', + }, + ], + }, + '@react-native-community/slider': { + repoUrl: 'https://github.com/react-native-community/react-native-slider', + installableInManagedApps: true, + packageJsonPath: 'src', + steps: [ + { + sourceIosPath: 'src/ios', + targetIosPath: 'Api/Components/Slider', + sourceAndroidPath: 'src/android/src/main/java/com/reactnativecommunity/slider', + targetAndroidPath: 'modules/api/components/slider', + sourceAndroidPackage: 'com.reactnativecommunity.slider', + targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.slider', + }, + ], + }, }; async function getBundledNativeModulesAsync(): Promise<{ [key: string]: string }> { @@ -728,7 +758,7 @@ async function action(options: ActionOptions) { const { name, version } = await JsonFile.readAsync<{ name: string; version: string; - }>(path.join(tmpDir, 'package.json')); + }>(path.join(tmpDir, moduleConfig.packageJsonPath ?? '', 'package.json')); const semverPrefix = (options.semverPrefix != null ? options.semverPrefix : moduleConfig.semverPrefix) || ''; const versionRange = `${semverPrefix}${version}`; diff --git a/yarn.lock b/yarn.lock index 59ed483259746..9cdd438cb3b7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2431,6 +2431,16 @@ resolved "https://registry.yarnpkg.com/@react-native-community/segmented-control/-/segmented-control-1.6.1.tgz#47c5eb20664376e390c3412d9a7242c2df35cdb0" integrity sha512-6TCrkDHgchKU2RA0r+V49Uwvv9mMhLKCwx7sf6Jkj0D4RYs+qmxYFjoM82bOcaH2kOVF+cvRvABRn0Ai0my5LA== +"@react-native-community/picker@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@react-native-community/picker/-/picker-1.6.0.tgz#ddb2b04ca251b79980e3052ece4e685b7abf0444" + integrity sha512-ijB33rCW3m9vu8jbXAcEh+icCmK0pyvCXiGfAwPgGSaFLsmE27dsle3022EbE+dqqQgpxUNtOkrd5C3B+/dcuA== + +"@react-native-community/slider@3.0.0-rc.2": + version "3.0.0-rc.2" + resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.0-rc.2.tgz#51599c4402ef7b75eba4fdabf49d5f55cb4ffe37" + integrity sha512-Tp6LY4AEr4SubT3PmdFwKeKkai/nNyJxeXcvDyuX6OY6O6zxB7Wcf1NF3NNYGNH0zC5YqZCvhIPxo9tSxISffg== + "@react-native-community/viewpager@3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@react-native-community/viewpager/-/viewpager-3.3.0.tgz#e613747a43a31a6f3278f817ba96fdaaa7941f23"