美文网首页Android开发半栈工程师Android知识
Android视频播放器的手势控制实现

Android视频播放器的手势控制实现

作者: 炎之铠 | 来源:发表于2017-09-07 15:44 被阅读246次

    出处
    炎之铠邮箱:yanzhikai_yjk@qq.com
    博客地址:http://blog.csdn.net/totond
    本文原创,转载请注明本出处!
    本项目GitHub地址:https://github.com/totond/GestureTest

    前言

    现在很多全屏的视频播放器现在都有这样的功能:左边上下滑动调节亮度,右边上下滑动调节音量,左右滑动调节快进快退,双击控制暂停播放。实现这样的功能并不难,本文分享一下实现经验。


    实现

    本实现采用GestureDetector来处理输入的手势,它的介绍可以看我的GestureDetector全面分析,在这里就不详细讲它的用法了。对于GestureDetector的回调,我们还要把它封装才能区分出那些上下左右的手势,所以这里继承一个RelativeLayout来封装它们。下面只介绍了具体实现思路,想开具体细节的可以进入demo查看。

    对GestureDetector的封装

    主要是把onScroll()滑动回调分成3个部分:音量、手势和快进快退,所以最后开放给外部的接口是这样的:

        /**
         * 用于提供给外部实现的视频手势处理接口
         */
        public interface VideoGestureListener {
            //亮度手势,手指在Layout左半部上下滑动时候调用
            public void onBrightnessGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
            //音量手势,手指在Layout右半部上下滑动时候调用
            public void onVolumeGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
            //快进快退手势,手指在Layout左右滑动的时候调用
            public void onFF_REWGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
            //单击手势,确认是单击的时候调用
            public void onSingleTapGesture(MotionEvent e);
            //双击手势,确认是双击的时候调用
            public void onDoubleTapGesture(MotionEvent e);
            //按下手势,第一根手指按下时候调用
            public void onDown(MotionEvent e);
            //快进快退执行后的松开时候调用
            public void onEndFF_REW(MotionEvent e);
        }
    

    为了给onScroll()分成3个部分,这里采用一个小小的状态模式,给它定义4个状态:NONE,VOLUME,BRIGHTNESS,FF_REW。只有NONE状态才能进入其他状态,其它状态一旦进入了不可切换,这样就保证了用户划着音量的时候不会突然就平移就改变了进度:

    public class VideoPlayerOnGestureListener extends GestureDetector.SimpleOnGestureListener {
    //...
            @Override
            public boolean onDown(MotionEvent e) {
                Log.d(TAG, "onDown: ");
                //每次按下都重置为NONE
                mScrollMode = NONE;
                if (mVideoGestureListener != null) {
                    mVideoGestureListener.onDown(e);
                }
                return true;
            }
    
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                Log.d(TAG, "onScroll: e1:" + e1.getX() + "," + e1.getY());
                Log.d(TAG, "onScroll: e2:" + e2.getX() + "," + e2.getY());
                Log.d(TAG, "onScroll: X:" + distanceX + "  Y:" + distanceY);
                switch (mScrollMode) {
                    case NONE:
                        Log.d(TAG, "NONE: ");
                        //offset是让快进快退不要那么敏感的值
                        if (Math.abs(distanceX) - Math.abs(distanceY) > offsetX) {
                            mScrollMode = FF_REW;
                        } else {
                            if (e1.getX() < getWidth() / 2) {
                                mScrollMode = BRIGHTNESS;
                            } else {
                                mScrollMode = VOLUME;
                            }
                        }
                        break;
                    case VOLUME:
                        if (mVideoGestureListener != null) {
                            mVideoGestureListener.onVolumeGesture(e1, e2, distanceX, distanceY);
                        }
                        Log.d(TAG, "VOLUME: ");
                        break;
                    case BRIGHTNESS:
                        if (mVideoGestureListener != null) {
                            mVideoGestureListener.onBrightnessGesture(e1, e2, distanceX, distanceY);
                        }
                        Log.d(TAG, "BRIGHTNESS: ");
                        break;
                    case FF_REW:
                        if (mVideoGestureListener != null) {
                            mVideoGestureListener.onFF_REWGesture(e1, e2, distanceX, distanceY);
                        }
                        hasFF_REW = true;
                        Log.d(TAG, "FF_REW: ");
                        break;
                }
                return true;
            }
    //...
    }
    

    然后在RelativeLayout里面使用这个VideoPlayerOnGestureListener,就让它们绑定了,只要在Activity里面使用这个RelativeLayout,就可以使用前面的VideoGestureListener接口的回调了。

    public class VideoGestureRelativeLayout extends RelativeLayout {
    //...
        public VideoGestureRelativeLayout(Context context) {
            super(context);
            init(context);
        }
    
        public VideoGestureRelativeLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        private void init(Context context){
            mOnGestureListener = new VideoPlayerOnGestureListener(this);
            mGestureDetector = new GestureDetector(context,mOnGestureListener);
            //取消长按,不然会影响滑动
            mGestureDetector.setIsLongpressEnabled(false);
            setOnTouchListener(new OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    //实现快进快退松开时候的回调
                    if (event.getAction() == MotionEvent.ACTION_UP){
                        if (hasFF_REW){
                            if (mVideoGestureListener != null) {
                                mVideoGestureListener.onEndFF_REW(event);
                            }
                            hasFF_REW = false;
                        }
                    }
                    //监听触摸事件
                    return mGestureDetector.onTouchEvent(event);
                }
            });
        }
    //...
    }
    

    由于GestureDetector没有滑动之后松开的回调,这里在onTouch()方补一个回调。

    具体控制实现

    来到这里,接口已经做好了,用户的手势我们都收到相应的回调了,然后我们要做的是定义收到这些回调的时候的操作。

    中间显示框

    在这里我做了一个比较丑的中间显示框,里面包含着一个ImageView和一个ProgressBar,默认延时一秒后消失:

    /**
     * Author: yanzhikai
     * Description: 中间用于显示状态的Layout
     * Email: yanzhikai_yjk@qq.com
     */
    
    public class ShowChangeLayout extends RelativeLayout {
        private static final String TAG = "gesturetest";
        private ImageView iv_center;
        private ProgressBar pb;
        private HideRunnable mHideRunnable;
        private int duration = 1000;
    
        public ShowChangeLayout(Context context) {
            super(context);
            init(context);
        }
    
        public ShowChangeLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        private void init(Context context){
            LayoutInflater.from(context).inflate(R.layout.show_change_layout,this);
            iv_center = (ImageView) findViewById(R.id.iv_center);
            pb = (ProgressBar) findViewById(R.id.pb);
    
            mHideRunnable = new HideRunnable();
            ShowChangeLayout.this.setVisibility(GONE);
        }
    
        //显示
        public void show(){
            setVisibility(VISIBLE);
            removeCallbacks(mHideRunnable);
            postDelayed(mHideRunnable,duration);
        }
    
        //设置进度
        public void setProgress(int progress){
            pb.setProgress(progress);
            Log.d(TAG, "setProgress: " +progress);
        }
    
        //设置持续时间
        public void setDuration(int duration) {
            this.duration = duration;
        }
    
        //设置显示图片
        public void setImageResource(int resource){
            iv_center.setImageResource(resource);
        }
    
        //隐藏自己的Runnable
        private class HideRunnable implements Runnable{
            @Override
            public void run() {
                ShowChangeLayout.this.setVisibility(GONE);
            }
        }
    }
    
    

    亮度、音量、进度调节

    首先是进行初始化:

    public class MainActivity extends AppCompatActivity implements VideoGestureRelativeLayout.VideoGestureListener {
        private final String TAG = "gesturetestm";
        private VideoGestureRelativeLayout ly_VG;
        private ShowChangeLayout scl;
        private AudioManager mAudioManager;
        private int maxVolume = 0;
        private int oldVolume = 0;
        private int newProgress = 0, oldProgress = 0;
        private BrightnessHelper mBrightnessHelper;
        private float brightness = 1;
        private Window mWindow;
        private WindowManager.LayoutParams mLayoutParams;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ly_VG = (VideoGestureRelativeLayout) findViewById(R.id.ly_VG);
            ly_VG.setVideoGestureListener(this);
    
            scl = (ShowChangeLayout) findViewById(R.id.scl);
    
            //初始化获取音量属性
            mAudioManager = (AudioManager)getSystemService(Service.AUDIO_SERVICE);
            maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
    
            //初始化亮度调节
            mBrightnessHelper = new BrightnessHelper(this);
    
            //下面这是设置当前APP亮度的方法配置
            mWindow = getWindow();
            mLayoutParams = mWindow.getAttributes();
            brightness = mLayoutParams.screenBrightness;
        }
    
    
    
        @Override
        public void onDown(MotionEvent e) {
            //每次按下的时候更新当前亮度和音量,还有进度
            oldProgress = newProgress;
            oldVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
            brightness = mLayoutParams.screenBrightness;
            if (brightness == -1){
                //一开始是默认亮度的时候,获取系统亮度,计算比例值
                brightness = mBrightnessHelper.getBrightness() / 255f;
            }
        }
    //...
    }
    

    每次onDown()都更新3个值的原因:onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)是每次Move事件位移大于1像素都会执行的(原因可看上一篇源码分析),distanceXdistanceY是两个MOVE之间的距离决定的,如果手指移动得比较慢,它们就会比较小,float转化成int很可能会被舍去小数点后的然后变成0,让用户不能慢慢通过滑动精准调控参数,所以要使用e2和e1的位移差来决定亮度等参数的变化大小,从而就要在onDown()获取旧数值来作为起始点了,详细逻辑请看代码:

    //...
        @Override
        public void onVolumeGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    
            int value = ly_VG.getHeight()/maxVolume ;
            int newVolume = (int) ((e1.getY() - e2.getY())/value + oldVolume);
    
            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,newVolume,AudioManager.FLAG_PLAY_SOUND);
            //要强行转Float类型才能算出小数点,不然结果一直为0
            int volumeProgress = (int) (newVolume/Float.valueOf(maxVolume) *100);
            if (volumeProgress >= 50){
                scl.setImageResource(R.drawable.volume_higher_w);
            }else if (volumeProgress > 0){
                scl.setImageResource(R.drawable.volume_lower_w);
            }else {
                scl.setImageResource(R.drawable.volume_off_w);
            }
            scl.setProgress(volumeProgress);
            scl.show();
        }
    
        @Override
        public void onBrightnessGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    
            //下面这是设置当前APP亮度的方法
            newBrightness += brightness;
    
            if (newBrightness < 0){
                newBrightness = 0;
            }else if (newBrightness > 1){
                newBrightness = 1;
            }
            mLayoutParams.screenBrightness = newBrightness;
            mWindow.setAttributes(mLayoutParams);
            scl.setProgress((int) (newBrightness * 100));
            scl.setImageResource(R.drawable.brightness_w);
            scl.show();
        }
    
        @Override
        public void onEndFF_REW(MotionEvent e) {
            makeToast("设置进度为" + newProgress);
        }
    
        @Override
        public void onFF_REWGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            float offset = e2.getX() - e1.getX();
            //根据移动的正负决定快进还是快退
            if (offset > 0) {
                scl.setImageResource(R.drawable.ff);
                newProgress = (int) (oldProgress + offset/ly_VG.getWidth() * 100);
                if (newProgress > 100){
                    newProgress = 100;
                }
            }else {
                scl.setImageResource(R.drawable.fr);
                newProgress = (int) (oldProgress + offset/ly_VG.getWidth() * 100);
                if (newProgress < 0){
                    newProgress = 0;
                }
            }
    
            scl.setProgress(newProgress);
            scl.show();
        }
    
        @Override
        public void onSingleTapGesture(MotionEvent e) {
            makeToast("SingleTap");
        }
    
        @Override
        public void onDoubleTapGesture(MotionEvent e) {
            makeToast("DoubleTap");
        }
    
       private void makeToast(String str){
            Toast.makeText(this,str,Toast.LENGTH_SHORT).show();
        }
    //...
    
    

    这里没有处理多点触控,所以实际效果是触控手指只看第一根,后面落下的手指动作都忽视。
      最后贴出调节系统亮度的辅助类:

    /**
     * Author: yanzhikai
     * Description: 用于辅助调节亮度的类
     * Email: yanzhikai_yjk@qq.com
     */
    
    public class BrightnessHelper {
        private ContentResolver resolver;
        private int maxBrightness = 255;
    
        public BrightnessHelper(Context context){
            resolver = context.getContentResolver();
        }
    
        /*
         * 调整亮度范围
         */
        private int adjustBrightnessNumber(int brightness){
            if (brightness < 0) {
                brightness = 0;
            } else if (brightness > 255) {
                brightness = 255;
            }
            return brightness;
        }
    
        /*
         * 关闭自动调节亮度
         */
        public void offAutoBrightness(){
            try {
                if(Settings.System.getInt(resolver, Settings.System.SCREEN_BRIGHTNESS_MODE) == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC)
                {
                    Settings.System.putInt(resolver,
                            Settings.System.SCREEN_BRIGHTNESS_MODE,
                            Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
                }
            } catch (Settings.SettingNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        /*
         * 获取系统亮度
         */
        public int getBrightness(){
            return Settings.System.getInt(resolver, Settings.System.SCREEN_BRIGHTNESS, 255);
        }
    
        /*
         * 设置系统亮度,如果有设置了自动调节,请先调用offAutoBrightness()方法关闭自动调节,否则会设置失败
         */
        public void setSystemBrightness(int newBrightness){
            Settings.System.putInt(resolver, Settings.System.SCREEN_BRIGHTNESS
                    ,adjustBrightnessNumber(newBrightness));
        }
    
        public int getMaxBrightness() {
            return maxBrightness;
        }
    
    
        /*
         * 设置当前APP的亮度
         */
        public void setAppBrightness(float brightnessPercent, Activity activity){
            Window window = activity.getWindow();
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            layoutParams.screenBrightness = brightnessPercent;
            window.setAttributes(layoutParams);
        }
    }
    

    上面调节亮度选择的是控制当前APP的亮度,不改变系统亮度。其实这些控制的实现是多种多样的,我这里只是给出一种封装GestureDetector的思路和实现方法,各个步骤都说得挺清楚了,具体的细节大家可以根据自己的需求改动。

    后话

    这篇博客是上一篇GestureDetector全面分析的后续,是GestureDetector的实践,原本是想合在一起的,结果发现太长了。经过GestureDetector的洗礼之后,感觉我对Android触摸事件输入的处理更加熟悉了,在这里分享出来我的经验,水平有限,如有错漏,敬请指正。
      最后贴一下demo地址,喜欢的可以给个Star!
      https://github.com/totond/GestureTest

    相关文章

      网友评论

        本文标题:Android视频播放器的手势控制实现

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