Android 换种方式实现ViewPager
一、可行性分析
ViewPager 是一款相对成熟的 Pager 切换 View,能够实现各种优秀的页面效果,也有不少问题,比如频繁会 requestLayout,另外的话如果是加载到 ListView 或者 RecyclerView 非固定头部,会偶现白屏或者 drawble 状态无法更新,还有就是 fragment 数量无法更新,需要重写 FragmentPagerAdapter 才行。
使用 RecyclerView 相对 ViewPager 来说,会避免很多问题,比如如果是轮播组件 View 可以复用而且会避免白屏问题,当然今天我们使用 RecyclerView 代替 ViewPager 虽然也没有实现复用,但并不影响和 ViewPager 同样的体验。
二、代码实现
具体原理是我们在 RecyclerView.Adapter 的如下两个方法中实现 fragment 的 detach 和 attach,这样可以保证 Fragment 的生命周期得到准确执行。
onViewAttachedToWindow
onViewDetachedFromWindow
FragmentPagerAdapter 源码如下(核心代码),另外需要指明的一点是我们使用 PagerSnapHelper 来辅助页面滑动:
public abstract class FragmentPagerAdapter extends RecyclerView.Adapter<FragmentViewHolder> {
private static final String TAG = "FragmentPagerAdapter";
private final FragmentManager mFragmentManager;
private Fragment mCurrentPrimaryItem = null;
private PagerSnapHelper snapHelper;
private RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState != RecyclerView.SCROLL_STATE_IDLE) return;
if (snapHelper == null) return;
View snapView = snapHelper.findSnapView(recyclerView.getLayoutManager());
if (snapView == null) return;
FragmentViewHolder holder = (FragmentViewHolder) recyclerView.getChildViewHolder(snapView);
setPrimaryItem(holder.getHelper().getFragment());
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
};
public FragmentPagerAdapter(FragmentManager fm) {
this.mFragmentManager = fm;
}
@Override
public FragmentViewHolder onCreateViewHolder(ViewGr0up parent, int position) {
RecyclerView recyclerView = (RecyclerView) parent;
if (snapHelper == null) {
snapHelper = new PagerSnapHelper();
recyclerView.addOnScrollListener(onScrollListener);
snapHelper.attachToRecyclerView(recyclerView);
}
FragmentHelper host = new FragmentHelper(recyclerView, getItemViewType(position));
return new FragmentViewHolder(host);
}
@Override
public void onBindViewHolder(FragmentViewHolder holder, int position) {
holder.getHelper().updateFragment();
}
public abstract Fragment getFragment(int viewType);
@Override
public abstract int getItemViewType(int position);
public Fragment instantiateItem(FragmentHelper host, int position, int fragmentType) {
FragmentTransaction transaction = host.beginTransaction(mFragmentManager);
final long itemId = getItemId(position);
String name = makeFragmentName(host.getContainerId(), itemId, fragmentType);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (BuildConfig.DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
transaction.attach(fragment);
} else {
fragment = getFragment(fragmentType);
if (BuildConfig.DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
transaction.add(host.getContainerId(), fragment,
makeFragmentName(host.getContainerId(), itemId, fragmentType));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
@Override
public abstract long getItemId(int position);
@SuppressWarnings("ReferenceEquality")
public void setPrimaryItem(Fragment fragment) {
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
private static String makeFragmentName(int viewId, long id, int fragmentType) {
return "android:recyclerview:fragment:" + viewId + ":" + id + ":" + fragmentType;
}
@Override
public void onViewAttachedToWindow(FragmentViewHolder holder) {
super.onViewAttachedToWindow(holder);
FragmentHelper host = holder.getHelper();
Fragment fragment = instantiateItem(holder.getHelper(), holder.getAdapterPosition(), getItemViewType(holder.getAdapterPosition()));
host.setFragment(fragment);
host.finishUpdate();
if (BuildConfig.DEBUG) {
Log.d("Fragment", holder.getHelper().getFragment().getTag() + " attach");
}
}
@Override
public void onViewDetachedFromWindow(FragmentViewHolder holder) {
super.onViewDetachedFromWindow(holder);
destroyItem(holder.getHelper(), holder.getAdapterPosition());
holder.getHelper().finishUpdate();
if (BuildConfig.DEBUG) {
Log.d("Fragment", holder.getHelper().getFragment().getTag() + " detach");
}
}
public void destroyItem(FragmentHelper host, int position) {
FragmentTransaction transaction = host.beginTransaction(mFragmentManager);
if (BuildConfig.DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + host.getFragment()
+ " v=" + ((Fragment) host.getFragment()).getView());
transaction.detach((Fragment) host.getFragment());
}
}
ViewHolder 源码,本类的主要作用是给 FragmentManager 打桩,其次还有个作用是连接 FragmentHelper(负责 Fragment 的事务)
public class FragmentViewHolder extends RecyclerView.ViewHolder {
private FragmentHelper mHelper;
public FragmentViewHolder(FragmentHelper host) {
super(host.getFragmentView());
this.mHelper = host;
}
public FragmentHelper getHelper() {
return mHelper;
}
}
FragmentHelper 源码
public class FragmentHelper {
private final int id;
private final Context context;
private Fragment fragment;
private ViewGr0up containerView;
private FragmentTransaction fragmentTransaction;
public FragmentHelper(RecyclerView recyclerView, int fragmentType) {
this.id = recyclerView.getId() + fragmentType + 1;
// 本id依赖于fragment,因此为防止fragmentManager将RecyclerView视为容器,直接将View加载到RecyclerView中,这种View缺少VewHolder,会出现空指针问题,这里加1
Activity activity = getRealActivity(recyclerView.getContext());
this.id = getUniqueFakeId(activity,this.id);
this.context = recyclerView.getContext();
this.containerView = buildDefualtContainer(this.context,this.id);
}
public FragmentHelper(RecyclerView recyclerView,int layoutId, int fragmentType) {
this.context = recyclerView.getContext();
this.containerView = (ViewGr0up) LayoutInflater.from( this.context).inflate(layoutId,recyclerView,false);
Activity activity = getRealActivity(recyclerView.getContext());
this.id = getUniqueFakeId(activity,this.id);
this.containerView.setId(id);
// 本id依赖于fragment,因此为防止fragmentManager多次复用同一个view,这里加1
}
private int getUniqueFakeId(Activity activity, int id) {
if(activity==null){
return id;
}
int newId = id;
do{
View v = activity.findViewById(id);
if(v!=null){
newId += 1;
continue;
}
newId = id;
break;
}while (true);
return newId;
}
public void setFragment(Fragment fragment) {
this.fragment = fragment;
}
public View getFragmentView() {
return containerView;
}
private static ViewGr0up buildDefualtContainer(Context context,int id) {
FrameLayout frameLayout = new FrameLayout(context);
RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT);
frameLayout.setLayoutParams(lp);
frameLayout.setId(id);
return frameLayout;
}
public int getContainerId() {
return id;
}
public void updateFragment() {
}
public Fragment getFragment() {
return fragment;
}
public void finishUpdate() {
if (fragmentTransaction != null) {
fragmentTransaction.commitNowAllowingStateLoss();
fragmentTransaction = null;
}
}
public FragmentTransaction beginTransaction(FragmentManager fragmentManager) {
if (this.fragmentTransaction == null) {
this.fragmentTransaction = fragmentManager.beginTransaction();
}
return this.fragmentTransaction;
}
}
以上提供了一个非常完美的 FragmentPagerAdapter,来支持 RecyclerView 加载 Fragment
三、新问题
在 Fragment 使用 RecyclerView 列表时会出现如下问题
1、交互不准确,比如垂直滑动会变成 Pager 滑动效果
2、页面 fling 效果出现闪动
3、事件冲突,导致滑动不了
因此为了解决上述问题,进行了一下规避
public class RecyclerPager extends RecyclerView {
private final DisplayMetrics mDisplayMetrics;
private int pageTouchSlop = 0;
float startX = 0;
float startY = 0;
boolean canHorizontalSlide = false;
public RecyclerPager(Context context) {
this(context, null);
}
public RecyclerPager(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RecyclerPager(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
pageTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
mDisplayMetrics = getResources().getDisplayMetrics();
}
private int captureMoveAction = 0;
private int captureMoveCounter = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = e.getX();
startY = e.getY();
canHorizontalSlide = false;
captureMoveCounter = 0;
Log.w("onTouchEvent_Pager", "down startY=" + startY + ",startX=" + startX);
break;
case MotionEvent.ACTION_MOVE:
float currentX = e.getX();
float currentY = e.getY();
float dx = currentX - startX;
float dy = currentY - startY;
if (!canHorizontalSlide && Math.abs(dy) > Math.abs(dx)) {
startX = currentX;
startY = currentY;
if (tryCaptureMoveAction(e)) {
canHorizontalSlide = false;
return true;
}
break;
}
if (Math.abs(dx) > pageTouchSlop && canScrollHorizontally((int) -dx)) {
canHorizontalSlide = true;
}
//这里取相反数,滑动方向与滚动方向是相反的
Log.d("onTouchEvent_Pager", "move dx=" + dx +",dy="+dy+ ",currentX=" + currentX+",currentY="+currentY + ",canHorizontalSlide=" + canHorizontalSlide);
if (canHorizontalSlide) {
startX = currentX;
startY = currentY;
if (captureMoveAction == MotionEvent.ACTION_MOVE) {
return super.dispatchTouchEvent(e);
}
if (tryCaptureMoveAction(e)) {
canHorizontalSlide = false;
return true;
}
}
break;
}
return super.dispatchTouchEvent(e);
}
/**
* 尝试捕获事件,防止事件后被父/子View主动捕获后无法改变捕获状态,简单的说就是没有cancel掉事件
*
* @param e 当前事件
* @return 返回ture表示发送了cancel->down事件
*/
private boolean tryCaptureMoveAction(MotionEvent e) {
if (captureMoveAction == MotionEvent.ACTION_MOVE) {
return false;
}
captureMoveCounter++;
if (captureMoveCounter != 2) {
return false;
}
MotionEvent eventDownMask = MotionEvent.obtain(e);
eventDownMask.setAction(MotionEvent.ACTION_DOWN);
Log.d("onTouchEvent_Pager", "事件转换");
super.dispatchTouchEvent(eventDownMask);
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
super.onInterceptTouchEvent(e); //该逻辑需要保留,因为recyclerView有自身事件处理
captureMoveAction = e.getAction();
switch (e.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
canHorizontalSlide = false;//不要拦截该类事件
break;
}
if (canHorizontalSlide) {
return true;
}
return false;
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
consumed[1] = dy;
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
@Override
public int getMinFlingVelocity() {
return (int) (super.getMinFlingVelocity() * mDisplayMetrics.density);
}
@Override
public int getMaxFlingVelocity() {
return (int) (super.getMaxFlingVelocity()* mDisplayMetrics.density);
}
@Override
public boolean fling(int velocityX, int velocityY) {
velocityX = (int) (velocityX / mDisplayMetrics.scaledDensity);
return super.fling(velocityX, velocityY);
}
}
四、使用
创建一个 fragment
@SuppressLint("ValidFragment")
public static class TestFragment extends Fragment{
private final int color;
private String name;
private int[] colors = {
0xffDC143C,
0xff66CDAA,
0xffDEB887,
Color.RED,
Color.BLACK,
Color.CYAN,
Color.GRAY
};
public TestFragment(int viewType) {
this.name = "id#"+viewType;
this.color = colors[viewType%colors.length];
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGr0up container, @Nullable Bundle savedInstanceState) {
View convertView = inflater.inflate(R.layout.test_fragment, container, false);
TextView textView = convertView.findViewById(R.id.text);
textView.setText("fagment: "+name);
convertView.setBackgroundColor(color);
if(BuildConfig.DEBUG){
Log.d("Fragment","onCreateView "+name);
}
return convertView;
}
@Override
public void onResume() {
super.onResume();
if(BuildConfig.DEBUG){
Log.d("Fragment","onResume");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.d("Fragment","setUserVisibleHint"+name);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if(BuildConfig.DEBUG){
Log.d("Fragment","onDestroyView" +name);
}
}
}
接着我们实现 FragmentPagerAdapter
public static class MyFragmentPagerAdapter extends FragmentPagerAdapter{
public MyFragmentPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getFragment(int viewType) {
return new TestFragment(viewType);
}
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return 3;
}
}
下面设置 Adapter
RecyclerView recyclerPagerView = findViewById(R.id.loopviews);
recyclerPagerView.setLayoutManager(new
LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
recyclerPagerView.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));
五、总结
整个过程轻松而愉快,当然本篇主要学习的是RcyclerView事件冲突的解决,突发奇想然后就写了个轮子,看样子是没什么大问题。
来源:juejin.cn/post/7307887970664595456