美文网首页UI效果仿写Android进阶之旅程序员
Android仿YouTube拖拽视频效果的实现

Android仿YouTube拖拽视频效果的实现

作者: 劳达斯 | 来源:发表于2017-04-23 23:58 被阅读3666次

    Android仿YouTube拖拽视频效果的实现

    youtube-like-drag-video-view

    代码已经开源到GitHub

    https://github.com/Lyzon/youtube-like-drag-video-view

    可以给个star支持一下我!谢谢!

    实现的效果图

    demo.gif

    实现思路

    在YouTube APP看到这个效果的时候,就觉得挺有意思的,然后就想着去实现这个效果。想了好久,想到了以下实现方案:

    • 首先播放视频的View我选择了TextureView,关于TextureView可以参考一下这篇文章: TextureView简易教程
    • 自定义我们的YouTubeVideoView继承一个LinearLayout,里面包裹着TextureView与下方的详情页面。
    • 根据手指在屏幕上的滑动距离计算并改变TextureView当前的宽、高。
    • TextureView的滑动效果我选择通过LayoutParams动态地设置marginTop属性达到上下滑动的效果。
    • 在最小化的时候,先判断用户意图,如果是横向滑动的话,改marginRight/Left属性来实现横向的滑动,滑动到一定距离则隐藏整个View。
    • 手指抬起后剩下的滑动效果使用属性动画来实现。
    • 剩下的一些细节比如说透明度的改变,最小化时的悬浮效果,以及距离屏幕边界的距离等等,也是根据手指的滑动距离得到的。
    • 所有的滑动事件的处理都在给TextureView设置的OnTouchListener里完成。

    其他的实现思路

    • TextureView的拖动效果也可以使用Android中一个帮助拖动的类ViewDragHelper来完成,在ViewDragHelper的回调中实现与其他View的联动。
    • 可以试试CoordinatorLayout,协调与联动。~

    Let's Code!

    这里我就不把代码全部贴上了,主要讲一下我这个实现思路中需要注意的一些点。先看一下我们要使用到的全局变量吧:

    // 可拖动的videoView 和下方的详情View
    private View mVideoView;
    private View mDetailView;
    // video类的包装类,用于属性动画
    private VideoViewWrapper mVideoWrapper;
    
    //滑动区间,取值为是videoView最小化时距离屏幕顶端的高度
    private float allScrollY;
    
    //1f为初始状态,0.5f或0.25f(横屏时)为最小状态
    private float nowStateScale;
    //最小的缩放比例
    private float MIN_RATIO = 0.5f;
    private static final float VIDEO_RATIO = 16f / 9f;
    
    //是否是第一次Measure,用于获取播放器初始宽高
    private boolean isFirstMeasure = true;
    
    //VideoView初始宽高
    private int originalWidth;
    private int originalHeight;
    
    //最小时距离屏幕右边以及下边的 DP值 初始化时会转化为PX
    private static final int MARGIN_DP = 12;
    private int marginPx;
    
    //是否可以横滑删除
    private boolean canHide;
    

    接下来重写onFinishInflate()方法,获取到两个子View,一个播放视频,一个展示详情。

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() != 2)
            throw new RuntimeException("YouTubeVideoView only need 2 child views");
    
        mVideoView = getChildAt(0);
        mDetailView = getChildAt(1);
    
        init();
    }
    

    再看一下init()方法,主要做一下初始化:

    private void init() {
        //设置触摸监听器
        mVideoView.setOnTouchListener(new VideoTouchListener());
        //初始化包装类
        mVideoWrapper = new VideoViewWrapper();
        //DP To PX
        marginPx = MARGIN_DP * (getContext().getResources().getDisplayMetrics().densityDpi / 160);
    
        //当前缩放比例
        nowStateScale = 1f;
    
        //如果是横屏则最小化比例为0.25f
        if (mVideoView.getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
            MIN_RATIO = 0.25f;
    
        originalWidth = mVideoView.getContext().getResources().getDisplayMetrics().widthPixels;
        originalHeight = (int) (originalWidth / VIDEO_RATIO);
    
        ViewGroup.LayoutParams lp = mVideoView.getLayoutParams();
        lp.width = originalWidth;
        lp.height = originalHeight;
        mVideoView.setLayoutParams(lp);
    }
    
    • 首先我们先为播放视频的View注册一个监听器,接下来在这个监听器里处理触摸事件。

    • 然后我们初始化了一个包装类,这个包装类用于属性动画,稍后分析。

    • px(像素) = dp * (dpi / 160)。

    • 原始宽度 = 屏幕宽度,高度由比例16 : 9算出。

    • 然后把初始宽高通过LayoutParams设置给播放视频的View。

    • 包装类:

        private class VideoViewWrapper {
        private LinearLayout.LayoutParams params;
        private LinearLayout.LayoutParams detailParams;
      
        VideoViewWrapper() {
            params = (LinearLayout.LayoutParams) mVideoView.getLayoutParams();
            detailParams = (LinearLayout.LayoutParams) mDetailView.getLayoutParams();
            params.gravity = Gravity.END;
        }
      
        int getWidth() {
            return params.width < 0 ? originalWidth : params.width;
        }
      
        int getHeight() {
            return params.height < 0 ? originalHeight : params.height;
        }
      
        void setWidth(float width) {
            if (width == originalWidth) {
                params.width = -1;
                params.setMargins(0, 0, 0, 0);
            } else
                params.width = (int) width;
      
            mVideoView.setLayoutParams(params);
        }
      
        void setHeight(float height) {
            params.height = (int) height;
            mVideoView.setLayoutParams(params);
        }
      

    分析一下这个包装类的作用,我们知道,要改变一个View的宽高,你可以在View的onMeasure或者onLayout中做文章,也可以通过给View设置LayoutParams来改变宽高。然后我们在使用属性动画的时候,要改变某个对象的某个属性的值,那么这个属性要有相对应的set/get方法,然而View里并没有setWidth/getWidth方法,有些实现类有setWidth/getWidth方法,可是改变的并不是控件的宽高。这个时候,使用包装类可以完美的解决这个问题,我们通过这个类为播放视频的View间接地提供了get/set宽高的方法,方法内的实现是为View设置LayoutParams。这样使用不仅可读性高,而且很安全,拓展性也高。

    接下来重写onMeasure()方法,如果是第一次onMeasure的话,初始化竖直方向的滑动区间,也就是视频View从最大到最小整个过程中手指需要滑动的竖直方向上的距离,也就是最小化时视频View的MarginTop的值,通过this.getMeasuredHeight()获取我们整个View的测量高度,不用屏幕高度的原因是因为虚拟按键的影响。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (isFirstMeasure) {
            //滑动区间,取值为是videoView最小化时距离屏幕顶端的高度 也就是最小化时的marginTop
            allScrollY = this.getMeasuredHeight() - MIN_RATIO * originalHeight - marginPx;
            isFirstMeasure = false;
        }
    }
    

    接下来就到给视频View设置的OnTouchListener里了,这个类里的代码有点多,在ACTION_DOWN里,做一下初始化,在ACTION_UP和CANCEL里,确定View的状态,最大化或最小化。这里主要看一下ACTION_MOVE里的代码:

    case MotionEvent.ACTION_MOVE:
        tracker.addMovement(ev);
        dy = y - mLastY; //和上一次滑动的差值
        int dx = x - mLastX;
        int newMarY = mVideoWrapper.getMargin() + dy; //新的marginTop值
        int newMarX = mVideoWrapper.getMarginRight() - dx;//新的marginRight值
        int dDownY = y - mDownY;
        int dDownX = x - mDownX; // 从点击点开始产生的的差值
    
        //如果滑动达到一定距离
        if (Math.abs(dDownX) > touchSlop || Math.abs(dDownY) > touchSlop) {
            isClick = false;
            if (Math.abs(dDownX) > Math.abs(dDownY) && canHide) {//如果X>Y 且能滑动关闭
                mVideoWrapper.setMarginRight(newMarX);
                } else
                  updateVideoView(newMarY); //否则通过新的marginTop的值更新大小
                }
          break;
    
    • touchSlop是一个int值,跟随不同的分辨率有变化,一般滑动差值大于这个值,才能认为用户在进行滑动操作。使用 ViewConfiguration.get(getContext()).getScaledTouchSlop() 获取到。

    • 横滑隐藏的时候动态地设置marginRight属性就可以了。

    • 竖直滑动改变大小的时候,调用的 updateVideoView(int marginTop)方法我们看一下:

        private void updateVideoView(int m) {
        //如果当前状态是最小化,先把我们的的布局宽高设置为MATCH_PARENT
        if (nowStateScale == MIN_RATIO) {
            ViewGroup.LayoutParams params = getLayoutParams();
            params.width = -1;
            params.height = -1;
            setLayoutParams(params);
        }
      
        canHide = false;
      
        //marginTop的值最大为allScrollY,最小为0
        if (m > allScrollY)
            m = (int) allScrollY;
        if (m < 0)
            m = 0;
      
        //视频View高度的百分比100% - 0%
        float marginPercent = (allScrollY - m) / allScrollY;
        //视频View对应的大小的百分比 100% - 50%或25%
        float videoPercent = MIN_RATIO + (1f - MIN_RATIO) * marginPercent;
      
        //设置宽高
        mVideoWrapper.setWidth(originalWidth * videoPercent);
        mVideoWrapper.setHeight(originalHeight * videoPercent);
      
        mDetailView.setAlpha(marginPercent);//设置下方详情View的透明度
        this.getBackground().setAlpha((int) (marginPercent * 255));
      
        int mr = (int) ((1f - marginPercent) * marginPx); //VideoView右边和详情View 上方的margin
        mVideoWrapper.setZ(mr / 2);//这个是Z轴的值,悬浮效果
      
        mVideoWrapper.setMarginTop(m);
        mVideoWrapper.setMarginRight(mr);
        mVideoWrapper.setDetailMargin(mr);
      

      }

    顺着注释看,主要就是通过marginTop值算出百分比,通过百分比得到当前宽高,并通过包装类设置给视频View。

    看一下UP里的处理吧:

                    case MotionEvent.ACTION_UP:
    
                    if (isClick) {
                        if (nowStateScale == 1f && mCallback !=null) {
                                //单击事件回调
                                mCallback.onVideoClick();
                        } else {
                            goMax();
                        }
                        break;
                    }
    
                    tracker.computeCurrentVelocity(100);
                    float yVelocity = Math.abs(tracker.getYVelocity());
                    tracker.clear();
                    tracker.recycle();
    
                    if (canHide) {
                        //速度大于一定值或者滑动的距离超过了最小化时的宽度,则进行隐藏,否则保持最小状态。
                        if (yVelocity > touchSlop || Math.abs(mVideoWrapper.getMarginRight()) > MIN_RATIO * originalWidth)
                            dismissView();
                        else
                            goMin();
                    } else
                        confirmState(yVelocity, dy);//确定状态。
                    break;
    

    首先,如果在MOVE里移动的距离小于touchSlop的话,UP里isClick就为真,这个时候就进行单击事件的处理,并且break,如果不是单击事件,就可以根据移动的速度或者移动的距离来确定状态,看一下用于手指抬起后确定状态的函数:

    private void confirmState(float v, int dy) { //dy用于判断是否反方向滑动了
    
        //如果手指抬起时宽度达到一定值 或者 速度达到一定值 则改变状态
        if (nowStateScale == 1f) {
            if (mVideoView.getWidth() <= originalWidth * 0.75f || (v > 15 && dy > 0)) {
                goMin();
            } else
                goMax();
        } else {
            if (mVideoView.getWidth() >= originalWidth * 0.75f || (v > 15 && dy < 0)) {
                goMax();
            } else
                goMin();
        }
    }
    

    非常简单。
    最后看一下goMax()函数:

    public void goMax() {
       
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                ObjectAnimator.ofFloat(mVideoWrapper, "width", mVideoWrapper.getWidth(), originalWidth),
                ObjectAnimator.ofFloat(mVideoWrapper, "height", mVideoWrapper.getHeight(), originalHeight),
                ObjectAnimator.ofInt(mVideoWrapper, "marginTop", mVideoWrapper.getMarginTop(), 0),
                ObjectAnimator.ofInt(mVideoWrapper, "marginRight", mVideoWrapper.getMarginRight(), 0),
                ObjectAnimator.ofInt(mVideoWrapper, "detailMargin", mVideoWrapper.getDetailMargin(), 0),
                ObjectAnimator.ofFloat(mVideoWrapper, "z", mVideoWrapper.getZ(), 0),
                ObjectAnimator.ofFloat(mDetailView, "alpha", mDetailView.getAlpha(), 1f),
                ObjectAnimator.ofInt(this.getBackground(), "alpha", this.getBackground().getAlpha(), 255)
        );
        set.setDuration(200).start();
        nowStateScale = 1.0f;
        canHide = false;
    }
    

    使用属性动画把所有要更改的对象的所有值都设置为最大化时候的状态就可以了。goMin()方法差不多,反着设置属性就是了。

    最后在MainActivity中做一些常规工作,播放一下视频就可以了!

    总结一下

    这个自定义ViewGroup的的代码还有许多可以优化的地方,可是本人水平有限,做得不够好。另外,这个效果不能封装成库来使用,因为局限性还是比较多的。写这个效果,从一开始的完全没有思路,到后来一步步慢慢地实现出来。其实是非常有成就感的一件事情。这次是我第一次写博客,有不好的地方请批评指正。非常感谢,代码已经开源到github,希望能够给个star,再次感谢。

    相关文章

      网友评论

      • EchoYuq:棒棒滴
      • 控阁:不错哈:+1:
      • e6a56e3205c8:许多外贸出口推广方法已过时,目前谷歌系三剑客gofair最管用。
        劳达斯:@benxia3639 嗯嗯嗯嗯
      • 6e752a59c8bf:让自定义的view覆盖在那个ListView 之上是怎么实现的啊.....
        6e752a59c8bf:哦,知道了.......
      • 6e752a59c8bf:第一次点击的时候不能出现从小到大这个效果
        劳达斯: @wayhow 你自己该一下那,代码很简单 😄
        6e752a59c8bf:@劳达斯 就是说我第一次点击listview的item的切到那个自定义view的时候宽高之类的就是originalWith什么的嘛,就导致goMax就没有从小变到大这个效果.也不是啥大问题
        劳达斯:啊= =,最近太忙了。
        是什么问题?我没懂,详细说明一下?
      • 113e0b1b553e:UC也有。刚遇到的时候贼好奇。拖下去拖上来的玩。
      • trayliu_小马过河:一直以为这个东西是画中画啊
        劳达斯:@依然范特稀西 谢谢谢谢谢谢:relaxed:
        劳达斯:啊 我也不知道youtube是如何做的 这个只是按照自己的想法实现的效果

        另外 谢谢回复:relaxed:

      本文标题:Android仿YouTube拖拽视频效果的实现

      本文链接:https://www.haomeiwen.com/subject/qcruzttx.html