/**
* 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.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.
*/
public class ReactViewPager extends ViewPager {
private class Adapter extends PagerAdapter {
private final List<View> mViews = new ArrayList<>();
private boolean mIsViewPagerInIntentionallyInconsistentState = false;
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 yoga step.
// TODO(7323049): Remove this workaround once we figure out a way to re-layout some views on
// request
setOffscreenPageLimit(mViews.size());
}
void removeViewAt(int index) {
mViews.remove(index);
notifyDataSetChanged();
// TODO(7323049): Remove this workaround once we figure out a way to re-layout some views on
// request
setOffscreenPageLimit(mViews.size());
}
/**
* Replace a set of views to the ViewPager adapter and update the ViewPager
*/
void setViews(List<View> views) {
mViews.clear();
mViews.addAll(views);
notifyDataSetChanged();
// we want to make sure we return POSITION_NONE for every view here, since this is only
// called after a removeAllViewsFromAdapter
mIsViewPagerInIntentionallyInconsistentState = false;
}
/**
* Remove all the views from the adapter and de-parents them from the ViewPager
* After calling this, it is expected that notifyDataSetChanged should be called soon
* afterwards.
*/
void removeAllViewsFromAdapter(ViewPager pager) {
mViews.clear();
pager.removeAllViews();
// set this, so that when the next addViews is called, we return POSITION_NONE for every
// entry so we can remove whichever views we need to and add the ones that we need to.
mIsViewPagerInIntentionallyInconsistentState = true;
}
View getViewAt(int index) {
return mViews.get(index);
}
@Override
public int getCount() {
return mViews.size();
}
@Override
public int getItemPosition(Object object) {
// if we've removed all views, we want to return POSITION_NONE intentionally
return mIsViewPagerInIntentionallyInconsistentState || !mViews.contains(object) ?
POSITION_NONE : mViews.indexOf(object);
}
@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) {
container.removeView((View) object);
}
@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(), position, positionOffset));
}
@Override
public void onPageSelected(int position) {
if (!mIsCurrentItemFromJs) {
mEventDispatcher.dispatchEvent(
new PageSelectedEvent(getId(), position));
}
}
@Override
public void onPageScrollStateChanged(int state) {
String pageScrollState;
switch (state) {
case SCROLL_STATE_IDLE:
pageScrollState = "idle";
break;
case SCROLL_STATE_DRAGGING:
pageScrollState = "dragging";
break;
case SCROLL_STATE_SETTLING:
pageScrollState = "settling";
break;
default:
throw new IllegalStateException("Unsupported pageScrollState");
}
mEventDispatcher.dispatchEvent(
new PageScrollStateChangedEvent(getId(), pageScrollState));
}
}
private final EventDispatcher mEventDispatcher;
private boolean mIsCurrentItemFromJs;
private boolean mScrollEnabled = true;
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 (!mScrollEnabled) {
return false;
}
if (super.onInterceptTouchEvent(ev)) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mScrollEnabled) {
return false;
}
return super.onTouchEvent(ev);
}
public void setCurrentItemFromJs(int item, boolean animated) {
mIsCurrentItemFromJs = true;
setCurrentItem(item, animated);
mIsCurrentItemFromJs = false;
}
public void setScrollEnabled(boolean scrollEnabled) {
mScrollEnabled = scrollEnabled;
}
/*package*/ void addViewToAdapter(View child, int index) {
getAdapter().addView(child, index);
}
/*package*/ void removeViewFromAdapter(int index) {
getAdapter().removeViewAt(index);
}
/*package*/ int getViewCountInAdapter() {
return getAdapter().getCount();
}
/*package*/ View getViewFromAdapter(int index) {
return getAdapter().getViewAt(index);
}
public void setViews(List<View> views) {
getAdapter().setViews(views);
}
public void removeAllViewsFromAdapter() {
getAdapter().removeAllViewsFromAdapter(this);
}
}