写点什么

Android 进阶:高仿抖音上下滑动分页视频,要求页面流畅

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

page.setTranslationX(xPosition);//set Y position to swipe in from topfloat yPosition = position * page.getHeight();page.setTranslationY(yPosition);} else {page.setAlpha(0);}} else {int pageWidth = page.getWidth();if (position < -1) { // [-Infinity,-1)// This page is way off-screen to the left.page.setAlpha(0);} else if (position <= 0) { // [-1,0]// Use the default slide transition when moving to the left pagepage.setAlpha(1);page.setTranslationX(0);page.setScaleX(1);page.setScaleY(1);} else if (position <= 1) { // (0,1]// Fade the page out.page.setAlpha(1 - position);// Counteract the default slide transitionpage.setTranslationX(pageWidth * -position);page.setTranslationY(0);// Scal


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


e the page down (between MIN_SCALE and 1)float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));page.setScaleX(scaleFactor);page.setScaleY(scaleFactor);} else { // (1,+Infinity]// This page is way off-screen to the right.page.setAlpha(0);}}}}


/**


  • 交换 x 轴和 y 轴的移动距离

  • @param event 获取事件类型的封装类 MotionEvent*/private MotionEvent swapXY(MotionEvent event) {//获取宽高 float width = getWidth();float height = getHeight();//将 Y 轴的移动距离转变成 X 轴的移动距离 float swappedX = (event.getY() / height) * width;//将 X 轴的移动距离转变成 Y 轴的移动距离 float swappedY = (event.getX() / width) * height;//重设 event 的位置 event.setLocation(swappedX, swappedY);return event;}


@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {mRecentTouchTime = System.currentTimeMillis();if (getCurrentItem() == 0 && getChildCount() == 0) {return false;}if (isVertical) {boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));swapXY(ev);// return touch coordinates to original reference frame for any child viewsreturn intercepted;} else {return super.onInterceptTouchEvent(ev);}}


@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (getCurrentItem() == 0 && getChildCount() == 0) {return false;}if (isVertical) {return super.onTouchEvent(swapXY(ev));} else {return super.onTouchEvent(ev);}}}

3.2 ViewPager 和 Fragment

  • 采用了 ViewPager+FragmentStatePagerAdapter+Fragment 来处理。为何选择使用 FragmentStatePagerAdapter,主要是因为使用 FragmentStatePagerAdapter 更省内存,但是销毁后新建也是需要时间的。一般情况下,如果你是用于 ViewPager 展示数量特别多的条目时,那么建议使用 FragmentStatePagerAdapter。关于 PagerAdapter 的深度解析,可以我这篇文章:[PagerAdapter 深度解析和实践优化](


)


  • 在 activity 中的代码如下所示


private void initViewPager() {List<Video> list = new ArrayList<>();ArrayList<Fragment> fragments = new ArrayList<>();for (int a = 0; a< DataProvider.VideoPlayerList.length ; a++){Video video = new Video(DataProvider.VideoPlayerTitle[a],10,"",DataProvider.VideoPlayerList[a]);list.add(video);fragments.add(VideoFragment.newInstant(DataProvider.VideoPlayerList[a]));}vp.setOffscreenPageLimit(1);vp.setCurrentItem(0);vp.setOrientation(DirectionalViewPager.VERTICAL);FragmentManager supportFragmentManager = getSupportFragmentManager();MyPagerAdapter myPagerAdapter = new MyPagerAdapter(fragments, supportFragmentManager);vp.setAdapter(myPagerAdapter);}


class MyPagerAdapter extends FragmentStatePagerAdapter{


private ArrayList<Fragment> list;


public MyPagerAdapter(ArrayList<Fragment> list , FragmentManager fm){super(fm);this.list = list;}


@Overridepublic Fragment getItem(int i) {return list.get(i);}


@Overridepublic int getCount() {return list!=null ? list.size() : 0;}}


  • 那么在 fragment 中如何处理呢?关于视频播放器,这里可以看我封装的库,[视频 lib](


)


public class VideoFragment extends Fragment{


public VideoPlayer videoPlayer;private String url;private int index;


@Overridepublic void onStop() {super.onStop();VideoPlayerManager.instance().releaseVideoPlayer();}


public static Fragment newInstant(String url){VideoFragment videoFragment = new VideoFragment();Bundle bundle = new Bundle();bundle.putString("url",url);videoFragment.setArguments(bundle);return videoFragment;}


@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);Bundle arguments = getArguments();if (arguments != null) {url = arguments.getString("url");}}


@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater,@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {View view = inflater.inflate(R.layout.fragment_video, container, false);return view;}


@Overridepublic void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);videoPlayer = view.findViewById(R.id.video_player);}


@Overridepublic void onActivityCreated(@Nullable Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);Log.d("初始化操作","------"+index++);VideoPlayerController controller = new VideoPlayerController(getActivity());videoPlayer.setUp(url,null);videoPlayer.setPlayerType(ConstantKeys.IjkPlayerType.TYPE_IJK);videoPlayer.setController(controller);ImageUtils.loadImgByPicasso(getActivity(),"",R.drawable.image_default,controller.imageView());}}

3.3 修改滑动距离翻页

  • 需求要求必须手动触摸滑动超过 1/2 的时候松开可以滑动下一页,没有超过 1/2 返回原页,首先肯定是重写 viewpager,只能从源码下手。经过分析,源码滑动的逻辑处理在此处,truncator 的属性代表判断的比例值!

  • 这个方法会在切页的时候重定向 Page,比如从第一个页面滑动,结果没有滑动到第二个页面,而是又返回到第一个页面,那个这个 page 会有重定向的功能


private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {int targetPage;if (Math.abs(deltaX) > this.mFlingDistance && Math.abs(velocity) > this.mMinimumVelocity) {targetPage = velocity > 0 ? currentPage : currentPage + 1;} else {float truncator = currentPage >= this.mCurItem ? 0.4F : 0.6F;targetPage = currentPage + (int)(pageOffset + truncator);}


if (this.mItems.size() > 0) {ViewPager.ItemInfo firstItem = (ViewPager.ItemInfo)this.mItems.get(0);ViewPager.ItemInfo lastItem = (ViewPager.ItemInfo)this.mItems.get(this.mItems.size() - 1);targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));}


return targetPage;}


  • determineTargetPage 这个方法就是计算接下来要滑到哪一页。这个方法调用是在 MotionEvent.ACTION_UP 这个事件下,先说下参数意思:

  • currentPage:当前 ViewPager 显示的页面

  • pageOffset:用户滑动的页面偏移量

  • velocity: 滑动速率

  • deltaX: X 方向移动的距离

  • 进行 debug 调试之后,发现问题就在 0.4f 和 0.6f 这个参数上。分析得出:0.6f 表示用户滑动能够翻页的偏移量,所以不难理解,为啥要滑动半屏或者以上了。

  • 也可以修改 Touch 事件

  • 控制 ViewPager 的 Touch 事件,这个基本是万能的,毕竟是从根源上入手的。你可以在 onTouchEvent 和 onInterceptTouchEvent 中做逻辑的判断。但是比较麻烦。

3.4 修改滑动速度

  • 使用 viewPager 进行滑动时,如果通过手指滑动来进行的话,可以根据手指滑动的距离来实现,但是如果通过 setCurrentItem 函数来实现的话,则会发现直接闪过去的,会出现一下刷屏。想要通过使用 setCurrentItem 函数来进行 viewpager 的滑动,并且需要有过度滑动的动画,那么,该如何做呢?

  • 具体可以分析 setCurrentItem 源码的逻辑,然后会看到 scrollToItem 方法,这个特别重要,主要是处理滚动过程中的逻辑。最主要关心的也是 smoothScrollTo 函数,这个函数中,可以看到具体执行滑动的其实就一句话,就是 mScroller.startScroll(sx,sy,dx,dy,duration),则可以看到,是 mScroller 这个对象进行滑动的。那么想要改变它的属性,则可以通过反射来实现。

  • 代码如下所示,如果是手指触摸滑动,则可以加快一点滑动速率,当然滑动持续时间你可以自己设置。通过自己自定义滑动的时间,就可以控制滑动的速度。


@TargetApi(Build.VERSION_CODES.KITKAT)public void setAnimationDuration(final int during){try {// viewPager 平移动画事件 Field mField = ViewPager.class.getDeclaredField("mScroller");mField.setAccessible(true);// 动画效果与 ViewPager 的一致 Interpolator interpolator = new Interpolator() {@Overridepublic float getInterpolation(float t) {t -= 1.0f;return t * t * t * t * t + 1.0f;}};Scroller mScroller = new Scroller(getContext(),interpolator){final int time = 2000;@Overridepublic void startScroll(int x, int y, int dx, int dy, int duration) {// 如果手工滚动,则加速滚动 if (System.currentTimeMillis() - mRecentTouchTime > time) {duration = during;} else {duration /= 2;}super.startScroll(x, y, dx, dy, duration);}


@Overridepublic void startScroll(int x, int y, int dx, int dy) {super.startScroll(x, y, dx, dy,during);}};mField.set(this, mScroller);} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {e.printStackTrace();}}

04.用 RecyclerView 实现

4.1 自定义 LayoutManager

  • 自定义 LayoutManager,并且继承 LinearLayoutManager,这样就得到一个可以水平排向或者竖向排向的布局策略。如果你接触过 SnapHelper 应该了解一下 LinearSnapHelper 和 PagerSnapHelper 这两个子类类,LinearSnapHelper 可以实现让列表的 Item 居中显示的效果,PagerSnapHelper 就可以做到一次滚动一个 item 显示的效果。

  • 重写 onChildViewAttachedToWindow 方法,在 RecyclerView 中,当 Item 添加进来了调用这个方法。这个方法相当于是把 view 添加到 window 时候调用的,也就是说它比 draw 方法先执行,可以做一些初始化相关的操作。


/**


  • 该方法必须调用

  • @param recyclerView recyclerView*/@Overridepublic void onAttachedToWindow(RecyclerView recyclerView) {if (recyclerView == null) {throw new IllegalArgumentException("The attach RecycleView must not null!!");}super.onAttachedToWindow(recyclerView);this.mRecyclerView = recyclerView;if (mPagerSnapHelper==null){init();}mPagerSnapHelper.attachToRecyclerView(mRecyclerView);mRecyclerView.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener);}

4.2 添加滑动监听

  • 涉及到一次滑动一页视频,那么肯定会有视频初始化和释放的功能。那么思考一下哪里来开始播放视频和在哪里释放视频?不要着急,要监听滑动到哪页,需要我们重写 onScrollStateChanged()函数,这里面有三种状态:SCROLL_STATE_IDLE(空闲),SCROLL_STATE_DRAGGING(拖动),SCROLL_STATE_SETTLING(要移动到最后位置时)。

  • 我们需要的就是 RecyclerView 停止时的状态,我们就可以拿到这个 View 的 Position,注意这里还有一个问题,当你通过这个 position 去拿 Item 会报错,这里涉及到 RecyclerView 的缓存机制,自己去脑补~~。打印 Log,你会发现 RecyclerView.getChildCount()一直为 1 或者会出现为 2 的情况。来实现一个接口然后通过接口把状态传递出去。

  • 自定义监听 listener 事件


public interface OnPagerListener {


/**


  • 初始化完成*/void onInitComplete();


/**


  • 释放的监听

  • @param isNext 是否下一个

  • @param position 索引*/void onPageRelease(boolean isNext,int position);


/***


  • 选中的监听以及判断是否滑动到底部

  • @param position 索引

  • @param isBottom 是否到了底部*/void onPageSelected(int position,boolean isBottom);}

  • 获取到 RecyclerView 空闲时选中的 Item,重写 LinearLayoutManager 的 onScrollStateChanged 方法


/**


  • 滑动状态的改变

  • 缓慢拖拽-> SCROLL_STATE_DRAGGING

  • 快速滚动-> SCROLL_STATE_SETTLING

  • 空闲状态-> SCROLL_STATE_IDLE

  • @param state 状态*/@Overridepublic void onScrollStateChanged(int state) {switch (state) {case RecyclerView.SCROLL_STATE_IDLE:View viewIdle = mPagerSnapHelper.findSnapView(this);int positionIdle = 0;if (viewIdle != null) {positionIdle = getPosition(viewIdle);}if (mOnViewPagerListener != null && getChildCount() == 1) {mOnViewPagerListener.onPageSelected(positionIdle,positionIdle == getItemCount() - 1);}break;case RecyclerView.SCROLL_STATE_DRAGGING:View viewDrag = mPagerSnapHelper.findSnapView(this);if (viewDrag != null) {int positionDrag = getPosition(viewDrag);}break;case RecyclerView.SCROLL_STATE_SETTLING:View viewSettling = mPagerSnapHelper.findSnapView(this);if (viewSettling != null) {int positionSettling = getPosition(viewSettling);}break;default:break;}}

4.3 监听页面是否滚动

  • 这里有两个方法 scrollHorizontallyBy()和 scrollVerticallyBy()可以拿到滑动偏移量,可以判断滑动方向。


/**


  • 监听竖直方向的相对偏移量

  • @param dy y 轴滚动值

  • @param recycler recycler

  • @param state state 滚动状态

  • @return int 值*/@Overridepublic int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {if (getChildCount() == 0 || dy == 0) {return 0;}this.mDrift = dy;return super.scrollVerticallyBy(dy, recycler, state);}


/**


  • 监听水平方向的相对偏移量

  • @param dx x 轴滚动值

  • @param recycler recycler

  • @param state state 滚动状态

  • @return int 值*/@Overridepublic int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {if (getChildCount() == 0 || dx == 0) {return 0;}this.mDrift = dx;return super.scrollHorizontallyBy(dx, recycler, state);}

4.4 attach 和 Detached

  • 列表的选中监听好了,我们就看看什么时候释放视频的资源,第二步中的三种状态,去打印 getChildCount()的日志,你会发现 getChildCount()在 SCROLL_STATE_DRAGGING 会为 1,SCROLL_STATE_SETTLING 为 2,SCROLL_STATE_IDLE 有时为 1,有时为 2,还是 RecyclerView 的缓存机制 O(∩∩)O,这里不会去赘述缓存机制,要做的是要知道在什么时候去做释放视频的操作,还要分清是释放上一页还是下一页。


private RecyclerView.OnChildAttachStateChangeListener mChildAttachStateChangeListener =new RecyclerView.OnChildAttachStateChangeListener() {/**


  • 第一次进入界面的监听,可以做初始化方面的操作

  • @param view view*/@Overridepublic void onChildViewAttachedToWindow(@NonNull View view) {if (mOnViewPagerListener != null && getChildCount() == 1) {mOnViewPagerListener.onInitComplete();}}


/**


  • 页面销毁的时候调用该方法,可以做销毁方面的操作

  • @param view view*/@Overridepublic void onChildViewDetachedFromWindow(@NonNull View view) {if (mDrift >= 0){if (mOnViewPagerListener != null) {mOnViewPagerListener.onPageRelease(true , getPosition(view));}}else {if (mOnViewPagerListener != null) {mOnViewPagerListener.onPageRelease(false , getPosition(view));}}}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android进阶:高仿抖音上下滑动分页视频,要求页面流畅