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

出处
炎之铠邮箱: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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容