Open source ViewPagerAndroid

Reviewed By: @foghina

Differential Revision: D2513014

fb-gh-sync-id: d9bb668d76939ad85b657233c8b7beac9b244fab
This commit is contained in:
Martin Konicek
2015-10-06 19:43:31 -07:00
committed by facebook-github-bot-4
parent 6244fd003d
commit 0a419650ce
10 changed files with 716 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ var COMPONENTS = [
require('./ToolbarAndroidExample'), require('./ToolbarAndroidExample'),
require('./TouchableExample'), require('./TouchableExample'),
require('./ViewExample'), require('./ViewExample'),
require('./ViewPagerAndroidExample.android'),
]; ];
var APIS = [ var APIS = [

View File

@@ -0,0 +1,223 @@
/**
* 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.
*
*/
'use strict';
var React = require('react-native');
var {
Image,
StyleSheet,
Text,
TouchableWithoutFeedback,
TouchableOpacity,
View,
ViewPagerAndroid,
} = React;
var PAGES = 5;
var BGCOLOR = ['#fdc08e', '#fff6b9', '#99d1b7', '#dde5fe', '#f79273'];
var IMAGE_URIS = [
'http://apod.nasa.gov/apod/image/1410/20141008tleBaldridge001h990.jpg',
'http://apod.nasa.gov/apod/image/1409/volcanicpillar_vetter_960.jpg',
'http://apod.nasa.gov/apod/image/1409/m27_snyder_960.jpg',
'http://apod.nasa.gov/apod/image/1409/PupAmulti_rot0.jpg',
'http://apod.nasa.gov/apod/image/1510/lunareclipse_27Sep_beletskycrop4.jpg',
];
var LikeCount = React.createClass({
getInitialState: function() {
return {
likes: 7,
};
},
onClick: function() {
this.setState({likes: this.state.likes + 1});
},
render: function() {
var thumbsUp = '\uD83D\uDC4D';
return (
<View style={styles.likeContainer}>
<TouchableOpacity onPress={this.onClick} style={styles.likeButton}>
<Text style={styles.likesText}>
{thumbsUp + ' Like'}
</Text>
</TouchableOpacity>
<Text style={styles.likesText}>
{this.state.likes + ' likes'}
</Text>
</View>
);
},
});
var Button = React.createClass({
_handlePress: function() {
if (this.props.enabled && this.props.onPress) {
this.props.onPress();
}
},
render: function() {
return (
<TouchableWithoutFeedback onPress={this._handlePress}>
<View style={[styles.button, this.props.enabled ? {} : styles.buttonDisabled]}>
<Text style={styles.buttonText}>{this.props.text}</Text>
</View>
</TouchableWithoutFeedback>
);
}
});
var ProgressBar = React.createClass({
render: function() {
var fractionalPosition = (this.props.progress.position + this.props.progress.offset);
var progressBarSize = (fractionalPosition / (PAGES - 1)) * this.props.size;
return (
<View style={[styles.progressBarContainer, {width: this.props.size}]}>
<View style={[styles.progressBar, {width: progressBarSize}]}/>
</View>
);
}
});
var ViewPagerAndroidExample = React.createClass({
statics: {
title: '<ViewPagerAndroid>',
description: 'Container that allows to flip left and right between child views.'
},
getInitialState: function() {
return {page: 0, progress: {position: 0, offset: 0}};
},
onPageSelected: function(e) {
this.setState({page: e.nativeEvent.position});
},
onPageScroll: function(e) {
this.setState({progress: e.nativeEvent});
},
move: function(delta) {
var page = this.state.page + delta;
this.viewPager && this.viewPager.setPage(page);
this.setState({page});
},
go: function(page) {
this.viewPager && this.viewPager.setPage(page);
this.setState({page});
},
render: function() {
var pages = [];
for (var i = 0; i < PAGES; i++) {
var pageStyle = {
backgroundColor: BGCOLOR[i % BGCOLOR.length],
alignItems: 'center',
padding: 20,
};
pages.push(
<View key={i} style={pageStyle} collapsable={false}>
<Image
style={styles.image}
source={{uri: IMAGE_URIS[i % BGCOLOR.length]}}
/>
<LikeCount />
</View>
);
}
var page = this.state.page;
return (
<View style={styles.container}>
<ViewPagerAndroid
style={styles.viewPager}
initialPage={0}
onPageScroll={this.onPageScroll}
onPageSelected={this.onPageSelected}
ref={viewPager => { this.viewPager = viewPager; }}>
{pages}
</ViewPagerAndroid>
<View style={styles.buttons}>
<Button text="Start" enabled={page > 0} onPress={() => this.go(0)}/>
<Button text="Prev" enabled={page > 0} onPress={() => this.move(-1)}/>
<Text style={styles.buttonText}>Page {page + 1} / {PAGES}</Text>
<ProgressBar size={100} progress={this.state.progress}/>
<Button text="Next" enabled={page < PAGES - 1} onPress={() => this.move(1)}/>
<Button text="Last" enabled={page < PAGES - 1} onPress={() => this.go(PAGES - 1)}/>
</View>
</View>
);
},
});
var styles = StyleSheet.create({
buttons: {
flexDirection: 'row',
height: 30,
backgroundColor: 'black',
alignItems: 'center',
justifyContent: 'space-between',
},
button: {
flex: 1,
width: 0,
margin: 5,
borderColor: 'gray',
borderWidth: 1,
backgroundColor: 'gray',
},
buttonDisabled: {
backgroundColor: 'black',
opacity: 0.5,
},
buttonText: {
color: 'white',
},
container: {
flex: 1,
backgroundColor: 'white',
},
image: {
width: 300,
height: 200,
padding: 20,
},
likeButton: {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
borderColor: '#333333',
borderWidth: 1,
borderRadius: 5,
flex: 1,
margin: 8,
padding: 8,
},
likeContainer: {
flexDirection: 'row',
},
likesText: {
flex: 1,
fontSize: 18,
alignSelf: 'center',
},
progressBarContainer: {
height: 10,
margin: 10,
borderColor: '#eeeeee',
borderWidth: 2,
},
progressBar: {
alignSelf: 'flex-start',
flex: 1,
backgroundColor: '#eeeeee',
},
viewPager: {
flex: 1,
},
});
module.exports = ViewPagerAndroidExample;

View File

@@ -0,0 +1,178 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule ViewPagerAndroid
*/
'use strict';
var NativeMethodsMixin = require('NativeMethodsMixin');
var React = require('React');
var ReactElement = require('ReactElement');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var ReactPropTypes = require('ReactPropTypes');
var createReactNativeComponentClass = require('createReactNativeComponentClass');
var dismissKeyboard = require('dismissKeyboard');
var VIEWPAGER_REF = 'viewPager';
var ViewPagerValidAttributes = {
selectedPage: true,
};
/**
* Container that allows to flip left and right between child views. Each
* child view of the `ViewPagerAndroid` will be treated as a separate page
* and will be streched to fill the `ViewPagerAndroid`.
*
* It is important all children are `<View>`s and not composite components.
* You can set style properties like `padding` or `backgroundColor` for each
* child.
*
* Example:
*
* ```
* render: function() {
* return (
* <ViewPagerAndroid
* style={styles.viewPager}
* initialPage={0}>
* <View style={styles.pageStyle}>
* <Text>First page</Text>
* </View>
* <View style={styles.pageStyle}>
* <Text>Second page</Text>
* </View>
* </ViewPagerAndroid>
* );
* }
*
* ...
*
* var styles = {
* ...
* pageStyle: {
* alignItems: 'center',
* padding: 20,
* }
* }
* ```
*/
var ViewPagerAndroid = React.createClass({
propTypes: {
/**
* Index of initial page that should be selected. Use `setPage` method to
* update the page, and `onPageSelected` to monitor page changes
*/
initialPage: ReactPropTypes.number,
/**
* Executed when transitioning between pages (ether because of animation for
* the requested page change or when user is swiping/dragging between pages)
* The `event.nativeEvent` object for this callback will carry following data:
* - position - index of first page from the left that is currently visible
* - offset - value from range [0,1) describing stage between page transitions.
* Value x means that (1 - x) fraction of the page at "position" index is
* visible, and x fraction of the next page is visible.
*/
onPageScroll: ReactPropTypes.func,
/**
* This callback will be caleld once ViewPager finish navigating to selected page
* (when user swipes between pages). The `event.nativeEvent` object passed to this
* callback will have following fields:
* - position - index of page that has been selected
*/
onPageSelected: ReactPropTypes.func,
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default), drags do not dismiss the keyboard.
* - 'on-drag', the keyboard is dismissed when a drag begins.
*/
keyboardDismissMode: ReactPropTypes.oneOf([
'none', // default
'on-drag',
]),
},
getInitialState: function() {
return {
selectedPage: this.props.initialPage,
};
},
getInnerViewNode: function() {
return this.refs[VIEWPAGER_REF].getInnerViewNode();
},
_childrenWithOverridenStyle: function() {
// Override styles so that each page will fill the parent. Native component
// will handle positioning of elements, so it's not important to offset
// them correctly.
return React.Children.map(this.props.children, function(child) {
var newProps = {
...child.props,
style: [child.props.style, {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
width: undefined,
height: undefined,
}],
collapsable: false,
};
if (child.type && child.type.displayName && (child.type.displayName !== 'View')) {
console.warn('Each ViewPager child must be a <View>. Was ' + child.type.displayName);
}
return ReactElement.createElement(child.type, newProps);
});
},
_onPageScroll: function(event) {
if (this.props.onPageScroll) {
this.props.onPageScroll(event);
}
if (this.props.keyboardDismissMode === 'on-drag') {
dismissKeyboard();
}
},
_onPageSelected: function(event) {
var selectedPage = event.nativeEvent.position;
this.setState({
selectedPage,
});
if (this.props.onPageSelected) {
this.props.onPageSelected(event);
}
},
setPage: function(selectedPage) {
this.setState({
selectedPage,
});
},
render: function() {
return (
<NativeAndroidViewPager
ref={VIEWPAGER_REF}
style={this.props.style}
selectedPage={this.state.selectedPage}
onPageScroll={this._onPageScroll}
onPageSelected={this._onPageSelected}
children={this._childrenWithOverridenStyle()}
/>
);
},
});
var NativeAndroidViewPager = createReactNativeComponentClass({
validAttributes: {
...ReactNativeViewAttributes.UIView,
...ViewPagerValidAttributes,
},
uiViewClassName: 'AndroidViewPager',
});
module.exports = ViewPagerAndroid;

View File

@@ -0,0 +1,13 @@
/**
* 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 ViewPagerAndroid
*/
'use strict';
module.exports = require('UnimplementedView');

View File

@@ -48,6 +48,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
TouchableOpacity: require('TouchableOpacity'), TouchableOpacity: require('TouchableOpacity'),
TouchableWithoutFeedback: require('TouchableWithoutFeedback'), TouchableWithoutFeedback: require('TouchableWithoutFeedback'),
View: require('View'), View: require('View'),
ViewPagerAndroid: require('ViewPagerAndroid'),
WebView: require('WebView'), WebView: require('WebView'),
// APIs // APIs

View File

@@ -34,6 +34,7 @@ import com.facebook.react.views.text.ReactVirtualTextViewManager;
import com.facebook.react.views.textinput.ReactTextInputManager; import com.facebook.react.views.textinput.ReactTextInputManager;
import com.facebook.react.views.toolbar.ReactToolbarManager; import com.facebook.react.views.toolbar.ReactToolbarManager;
import com.facebook.react.views.view.ReactViewManager; import com.facebook.react.views.view.ReactViewManager;
import com.facebook.react.views.viewpager.ReactViewPagerManager;
/** /**
* Package defining basic modules and view managers. * Package defining basic modules and view managers.
@@ -68,6 +69,7 @@ public class MainReactPackage implements ReactPackage {
new ReactTextViewManager(), new ReactTextViewManager(),
new ReactToolbarManager(), new ReactToolbarManager(),
new ReactViewManager(), new ReactViewManager(),
new ReactViewPagerManager(),
new ReactVirtualTextViewManager()); new ReactVirtualTextViewManager());
} }
} }

View File

@@ -0,0 +1,56 @@
/**
* 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.viewpager;
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 {@link ReactViewPager} when user scrolls between pages (or when animating
* between pages).
*
* Additional data provided by this event:
* - position - index of first page from the left that is currently visible
* - offset - value from range [0,1) describing stage between page transitions. Value x means that
* (1 - x) fraction of the page at "position" index is visible, and x fraction of the next page
* is visible.
*/
/* package */ class PageScrollEvent extends Event<PageScrollEvent> {
public static final String EVENT_NAME = "topPageScroll";
private final int mPosition;
private final float mOffset;
protected PageScrollEvent(int viewTag, long timestampMs, int position, float offset) {
super(viewTag, timestampMs);
mPosition = position;
mOffset = offset;
}
@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);
eventData.putDouble("offset", mOffset);
return eventData;
}
}

View File

@@ -0,0 +1,49 @@
/**
* 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.viewpager;
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 {@link ReactViewPager} when selected page changes.
*
* Additional data provided by this event:
* - position - index of page that has been selected
*/
/* package */ class PageSelectedEvent extends Event<PageSelectedEvent> {
public static final String EVENT_NAME = "topPageSelected";
private final int mPosition;
protected PageSelectedEvent(int viewTag, long timestampMs, int position) {
super(viewTag, timestampMs);
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;
}
}

View File

@@ -0,0 +1,132 @@
/**
* 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.viewpager;
import java.util.ArrayList;
import java.util.List;
import android.os.SystemClock;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.NativeGestureUtil;
/**
* Wrapper view for {@link ViewPager}. It's forwarding calls to {@link ViewGroup#addView} to add
* views to custom {@link PagerAdapter} instance which is used by {@link NativeViewHierarchyManager}
* to add children nodes according to react views hierarchy.
*/
/* package */ class ReactViewPager extends ViewPager {
private class Adapter extends PagerAdapter {
private List<View> mViews = new ArrayList<>();
void addView(View child, int index) {
mViews.add(index, child);
notifyDataSetChanged();
// This will prevent view pager from detaching views for pages that are not currently selected
// We need to do that since {@link ViewPager} relies on layout passes to position those views
// in a right way (also thanks to {@link ReactViewPagerManager#needsCustomLayoutForChildren}
// returning {@code true}). Currently we only call {@link View#measure} and
// {@link View#layout} after CSSLayout step.
// TODO(7323049): Remove this workaround once we figure out a way to re-layout some views on
// request
setOffscreenPageLimit(mViews.size());
}
@Override
public int getCount() {
return mViews.size();
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = mViews.get(position);
container.addView(view, 0, generateDefaultLayoutParams());
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
View view = mViews.get(position);
container.removeView(view);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
private class PageChangeListener implements OnPageChangeListener {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
mEventDispatcher.dispatchEvent(
new PageScrollEvent(getId(), SystemClock.uptimeMillis(), position, positionOffset));
}
@Override
public void onPageSelected(int position) {
if (!mIsCurrentItemFromJs) {
mEventDispatcher.dispatchEvent(
new PageSelectedEvent(getId(), SystemClock.uptimeMillis(), position));
}
}
@Override
public void onPageScrollStateChanged(int state) {
// don't send events
}
}
private final EventDispatcher mEventDispatcher;
private boolean mIsCurrentItemFromJs;
public ReactViewPager(ReactContext reactContext) {
super(reactContext);
mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
mIsCurrentItemFromJs = false;
setOnPageChangeListener(new PageChangeListener());
setAdapter(new Adapter());
}
@Override
public Adapter getAdapter() {
return (Adapter) super.getAdapter();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (super.onInterceptTouchEvent(ev)) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
return true;
}
return false;
}
/* package */ void addViewToAdapter(View child, int index) {
getAdapter().addView(child, index);
}
/* package */ void setCurrentItemFromJs(int item) {
mIsCurrentItemFromJs = true;
setCurrentItem(item);
mIsCurrentItemFromJs = false;
}
}

View File

@@ -0,0 +1,61 @@
/**
* 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.viewpager;
import java.util.Map;
import android.view.View;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ReactProp;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
/**
* Instance of {@link ViewManager} that provides native {@link ViewPager} view.
*/
public class ReactViewPagerManager extends ViewGroupManager<ReactViewPager> {
private static final String REACT_CLASS = "AndroidViewPager";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected ReactViewPager createViewInstance(ThemedReactContext reactContext) {
return new ReactViewPager(reactContext);
}
@ReactProp(name = "selectedPage")
public void setSelectedPage(ReactViewPager view, int page) {
// TODO(8496821): Handle selectedPage property cleanup correctly, now defaults to 0
view.setCurrentItemFromJs(page);
}
@Override
public boolean needsCustomLayoutForChildren() {
return true;
}
@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
PageScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageScroll"),
PageSelectedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageSelected")
);
}
@Override
public void addView(ReactViewPager parent, View child, int index) {
parent.addViewToAdapter(child, index);
}
}