Open source Android slider

Reviewed By: bestander

Differential Revision: D3127200

fb-gh-sync-id: d3d51b312c2e32cc7a0f4c0bc084139343e97c3e
fbshipit-source-id: d3d51b312c2e32cc7a0f4c0bc084139343e97c3e
This commit is contained in:
Martin Konicek 2016-04-06 04:49:47 -07:00 committed by Facebook Github Bot 4
parent 29a1a05cbb
commit a461d25601
14 changed files with 792 additions and 4 deletions

View File

@ -0,0 +1,169 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
'use strict';
var React = require('react-native');
var {
Slider,
Text,
StyleSheet,
View,
} = React;
var SliderExample = React.createClass({
getDefaultProps() {
return {
value: 0,
}
},
getInitialState() {
return {
value: this.props.value,
};
},
render() {
return (
<View>
<Text style={styles.text} >
{this.state.value && +this.state.value.toFixed(3)}
</Text>
<Slider
{...this.props}
onValueChange={(value) => this.setState({value: value})} />
</View>
);
}
});
var SlidingCompleteExample = React.createClass({
getInitialState() {
return {
slideCompletionValue: 0,
slideCompletionCount: 0,
};
},
render() {
return (
<View>
<SliderExample
{...this.props}
onSlidingComplete={(value) => this.setState({
slideCompletionValue: value,
slideCompletionCount: this.state.slideCompletionCount + 1})} />
<Text>
Completions: {this.state.slideCompletionCount} Value: {this.state.slideCompletionValue}
</Text>
</View>
);
}
});
var styles = StyleSheet.create({
slider: {
height: 10,
margin: 10,
},
text: {
fontSize: 14,
textAlign: 'center',
fontWeight: '500',
margin: 10,
},
});
exports.title = '<Slider>';
exports.displayName = 'SliderExample';
exports.description = 'Slider input for numeric values';
exports.examples = [
{
title: 'Default settings',
render(): ReactElement {
return <SliderExample />;
}
},
{
title: 'Initial value: 0.5',
render(): ReactElement {
return <SliderExample value={0.5} />;
}
},
{
title: 'minimumValue: -1, maximumValue: 2',
render(): ReactElement {
return (
<SliderExample
minimumValue={-1}
maximumValue={2}
/>
);
}
},
{
title: 'step: 0.25',
render(): ReactElement {
return <SliderExample step={0.25} />;
}
},
{
title: 'onSlidingComplete',
render(): ReactElement {
return (
<SlidingCompleteExample />
);
}
},
{
title: 'Custom min/max track tint color',
platform: 'ios',
render(): ReactElement {
return (
<SliderExample
minimumTrackTintColor={'red'}
maximumTrackTintColor={'green'}
/>
);
}
},
{
title: 'Custom thumb image',
platform: 'ios',
render(): ReactElement {
return <SliderExample thumbImage={require('./uie_thumb_big.png')} />;
}
},
{
title: 'Custom track image',
platform: 'ios',
render(): ReactElement {
return <SliderExample trackImage={require('./slider.png')} />;
}
},
{
title: 'Custom min/max track image',
platform: 'ios',
render(): ReactElement {
return (
<SliderExample
minimumTrackImage={require('./slider-left.png')}
maximumTrackImage={require('./slider-right.png')}
/>
);
}
},
];

View File

@ -23,6 +23,10 @@ export type UIExplorerExample = {
};
var ComponentExamples: Array<UIExplorerExample> = [
{
key: 'SliderExample',
module: require('./SliderExample'),
},
{
key: 'ImageExample',
module: require('./ImageExample'),

View File

@ -50,8 +50,8 @@ var ComponentExamples: Array<UIExplorerExample> = [
module: require('./ListViewPagingExample'),
},
{
key: 'MapViewExample',
module: require('./MapViewExample'),
key: 'MapViewExample',
module: require('./MapViewExample'),
},
{
key: 'ModalExample',
@ -90,8 +90,8 @@ var ComponentExamples: Array<UIExplorerExample> = [
module: require('./SegmentedControlIOSExample'),
},
{
key: 'SliderIOSExample',
module: require('./SliderIOSExample'),
key: 'SliderExample',
module: require('./SliderExample'),
},
{
key: 'StatusBarExample',

View File

@ -0,0 +1,191 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule Slider
* @flow
*/
'use strict';
var Image = require('Image');
var NativeMethodsMixin = require('NativeMethodsMixin');
var Platform = require('Platform');
var PropTypes = require('ReactPropTypes');
var React = require('React');
var StyleSheet = require('StyleSheet');
var View = require('View');
var requireNativeComponent = require('requireNativeComponent');
type Event = Object;
var Slider = React.createClass({
mixins: [NativeMethodsMixin],
propTypes: {
...View.propTypes,
/**
* Used to style and layout the `Slider`. See `StyleSheet.js` and
* `ViewStylePropTypes.js` for more info.
*/
style: View.propTypes.style,
/**
* Initial value of the slider. The value should be between minimumValue
* and maximumValue, which default to 0 and 1 respectively.
* Default value is 0.
*
* *This is not a controlled component*, you don't need to update the
* value during dragging.
*/
value: PropTypes.number,
/**
* Step value of the slider. The value should be
* between 0 and (maximumValue - minimumValue).
* Default value is 0.
*/
step: PropTypes.number,
/**
* Initial minimum value of the slider. Default value is 0.
*/
minimumValue: PropTypes.number,
/**
* Initial maximum value of the slider. Default value is 1.
*/
maximumValue: PropTypes.number,
/**
* The color used for the track to the left of the button. Overrides the
* default blue gradient image.
* @platform ios
*/
minimumTrackTintColor: PropTypes.string,
/**
* The color used for the track to the right of the button. Overrides the
* default blue gradient image.
* @platform ios
*/
maximumTrackTintColor: PropTypes.string,
/**
* If true the user won't be able to move the slider.
* Default value is false.
*/
disabled: PropTypes.bool,
/**
* Assigns a single image for the track. Only static images are supported.
* The center pixel of the image will be stretched to fill the track.
* @platform ios
*/
trackImage: Image.propTypes.source,
/**
* Assigns a minimum track image. Only static images are supported. The
* rightmost pixel of the image will be stretched to fill the track.
* @platform ios
*/
minimumTrackImage: Image.propTypes.source,
/**
* Assigns a maximum track image. Only static images are supported. The
* leftmost pixel of the image will be stretched to fill the track.
* @platform ios
*/
maximumTrackImage: Image.propTypes.source,
/**
* Sets an image for the thumb. Only static images are supported.
* @platform ios
*/
thumbImage: Image.propTypes.source,
/**
* Callback continuously called while the user is dragging the slider.
*/
onValueChange: PropTypes.func,
/**
* Callback called when the user finishes changing the value (e.g. when
* the slider is released).
*/
onSlidingComplete: PropTypes.func,
/**
* Used to locate this view in UI automation tests.
*/
testID: PropTypes.string,
},
getDefaultProps: function() : any {
return {
disabled: false,
value: 0,
minimumValue: 0,
maximumValue: 1,
step: 0
};
},
render: function() {
let {style, onValueChange, onSlidingComplete, ...props} = this.props;
props.style = [styles.slider, style];
props.onValueChange = onValueChange && ((event: Event) => {
let userEvent = true;
if (Platform.OS === 'android') {
// On Android there's a special flag telling us the user is
// dragging the slider.
userEvent = event.nativeEvent.fromUser;
}
onValueChange && userEvent && onValueChange(event.nativeEvent.value);
});
props.onChange = props.onValueChange;
props.onSlidingComplete = onSlidingComplete && ((event: Event) => {
onSlidingComplete && onSlidingComplete(event.nativeEvent.value);
});
return <RCTSlider
{...props}
enabled={!this.props.disabled}
onStartShouldSetResponder={() => true}
onResponderTerminationRequest={() => false}
/>;
}
});
let styles;
if (Platform.OS === 'ios') {
styles = StyleSheet.create({
slider: {
height: 40,
},
});
} else {
styles = StyleSheet.create({
slider: {},
});
}
let options = {};
if (Platform.OS === 'android') {
options = {
nativeOnly: {
enabled: true,
}
};
}
const RCTSlider = requireNativeComponent('RCTSlider', Slider, options);
module.exports = Slider;

View File

@ -120,6 +120,11 @@ var SliderIOS = React.createClass({
},
render: function() {
console.warn(
'SliderIOS is deprecated and will be removed in ' +
'future versions of React Native. Use the cross-platform Slider ' +
'as a drop-in replacement.');
let {style, onValueChange, onSlidingComplete, ...props} = this.props;
props.style = [styles.slider, style];

View File

@ -31,6 +31,7 @@ var ReactNative = {
get ProgressViewIOS() { return require('ProgressViewIOS'); },
get ScrollView() { return require('ScrollView'); },
get SegmentedControlIOS() { return require('SegmentedControlIOS'); },
get Slider() { return require('Slider'); },
get SliderIOS() { return require('SliderIOS'); },
get SnapshotViewIOS() { return require('SnapshotViewIOS'); },
get Switch() { return require('Switch'); },

View File

@ -17,6 +17,7 @@ android_library(
react_native_target('java/com/facebook/react/views/progressbar:progressbar'),
react_native_target('java/com/facebook/react/views/recyclerview:recyclerview'),
react_native_target('java/com/facebook/react/views/scroll:scroll'),
react_native_target('java/com/facebook/react/views/slider:slider'),
react_native_target('java/com/facebook/react/views/swiperefresh:swiperefresh'),
react_native_target('java/com/facebook/react/views/switchview:switchview'),
react_native_target('java/com/facebook/react/views/text:text'),

View File

@ -47,6 +47,7 @@ import com.facebook.react.views.progressbar.ReactProgressBarViewManager;
import com.facebook.react.views.recyclerview.RecyclerViewBackedScrollViewManager;
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager;
import com.facebook.react.views.scroll.ReactScrollViewManager;
import com.facebook.react.views.slider.ReactSliderManager;
import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager;
import com.facebook.react.views.switchview.ReactSwitchManager;
import com.facebook.react.views.text.ReactRawTextManager;
@ -109,6 +110,7 @@ public class MainReactPackage implements ReactPackage {
new ReactProgressBarViewManager(),
new ReactRawTextManager(),
new ReactScrollViewManager(),
new ReactSliderManager(),
new ReactSwitchManager(),
new FrescoBasedReactTextInlineImageViewManager(),
new ReactTextInputManager(),

View File

@ -0,0 +1,23 @@
include_defs('//ReactAndroid/DEFS')
android_library(
name = 'slider',
srcs = glob(['*.java']),
deps = [
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/common:common'),
react_native_target('java/com/facebook/csslayout:csslayout'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
react_native_dep('android_res/android/support/v7/appcompat-orig:res-for-react-native'),
react_native_dep('third-party/android/support/v7/appcompat-orig:appcompat'),
react_native_dep('third-party/java/jsr-305:jsr-305'),
],
visibility = [
'PUBLIC',
],
)
project_config(
src_target = ':slider',
)

View File

@ -0,0 +1,111 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.slider;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.SeekBar;
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 SeekBar {
/**
* 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;
public ReactSlider(Context context, @Nullable AttributeSet attrs, int style) {
super(context, attrs, style);
}
/* 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();
}
/**
* 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 * mStep + mMinValue;
}
/**
* Update underlying native SeekBar's values.
*/
private void updateAll() {
if (mStep == 0) {
mStep = (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) / mStep);
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.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<ReactSliderEvent> {
public static final String EVENT_NAME = "topChange";
private final double mValue;
private final boolean mFromUser;
public ReactSliderEvent(int viewId, long timestampMs, double value, boolean fromUser) {
super(viewId, timestampMs);
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;
}
}

View File

@ -0,0 +1,155 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.slider;
import java.util.Map;
import android.view.View;
import android.view.ViewGroup;
import android.widget.SeekBar;
import com.facebook.csslayout.CSSNode;
import com.facebook.csslayout.MeasureOutput;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.SystemClock;
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;
/**
* Manages instances of {@code ReactSlider}.
*
* Note that the slider is _not_ a controlled component.
*/
public class ReactSliderManager extends SimpleViewManager<ReactSlider> {
private static final int STYLE = android.R.attr.seekBarStyle;
private static final String REACT_CLASS = "RCTSlider";
static class ReactSliderShadowNode extends LayoutShadowNode implements
CSSNode.MeasureFunction {
private int mWidth;
private int mHeight;
private boolean mMeasured;
private ReactSliderShadowNode() {
setMeasureFunction(this);
}
@Override
public void measure(CSSNode node, float width, float height, MeasureOutput measureOutput) {
if (!mMeasured) {
SeekBar reactSlider = new ReactSlider(getThemedContext(), null, STYLE);
final int spec = View.MeasureSpec.makeMeasureSpec(
ViewGroup.LayoutParams.WRAP_CONTENT,
View.MeasureSpec.UNSPECIFIED);
reactSlider.measure(spec, spec);
mWidth = reactSlider.getMeasuredWidth();
mHeight = reactSlider.getMeasuredHeight();
mMeasured = true;
}
measureOutput.width = mWidth;
measureOutput.height = 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(),
SystemClock.nanoTime(),
((ReactSlider)seekbar).toRealProgress(progress),
fromUser));
}
@Override
public void onStartTrackingTouch(SeekBar seekbar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekbar) {
ReactContext reactContext = (ReactContext) seekbar.getContext();
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
new ReactSlidingCompleteEvent(
seekbar.getId(),
SystemClock.nanoTime(),
((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) {
return new ReactSlider(context, null, STYLE);
}
@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);
}
@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", "onSlidingComplete"));
}
}

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.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<ReactSlidingCompleteEvent> {
public static final String EVENT_NAME = "topSlidingComplete";
private final double mValue;
public ReactSlidingCompleteEvent(int viewId, long timestampMs, double value) {
super(viewId, timestampMs);
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;
}
}

View File

@ -209,6 +209,7 @@ var components = [
'../Libraries/Components/RefreshControl/RefreshControl.js',
'../Libraries/Components/ScrollView/ScrollView.js',
'../Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.ios.js',
'../Libraries/Components/SliderIOS/Slider.js',
'../Libraries/Components/SliderIOS/SliderIOS.ios.js',
'../Libraries/Components/StatusBar/StatusBar.js',
'../Libraries/Components/Switch/Switch.js',