Android視頻播放器的手勢控制實現(xiàn)

出處
炎之鎧郵箱:yanzhikai_yjk@qq.com
博客地址:http://blog.csdn.net/totond
本文原創(chuàng),轉(zhuǎn)載請注明本出處!
本項目GitHub地址:https://github.com/totond/GestureTest

前言

現(xiàn)在很多全屏的視頻播放器現(xiàn)在都有這樣的功能:左邊上下滑動調(diào)節(jié)亮度,右邊上下滑動調(diào)節(jié)音量,左右滑動調(diào)節(jié)快進快退,雙擊控制暫停播放。實現(xiàn)這樣的功能并不難,本文分享一下實現(xiàn)經(jīng)驗。


實現(xiàn)

本實現(xiàn)采用GestureDetector來處理輸入的手勢,它的介紹可以看我的GestureDetector全面分析,在這里就不詳細講它的用法了。對于GestureDetector的回調(diào),我們還要把它封裝才能區(qū)分出那些上下左右的手勢,所以這里繼承一個RelativeLayout來封裝它們。下面只介紹了具體實現(xiàn)思路,想開具體細節(jié)的可以進入demo查看。

對GestureDetector的封裝

主要是把onScroll()滑動回調(diào)分成3個部分:音量、手勢和快進快退,所以最后開放給外部的接口是這樣的:

    /**
     * 用于提供給外部實現(xiàn)的視頻手勢處理接口
     */
    public interface VideoGestureListener {
        //亮度手勢,手指在Layout左半部上下滑動時候調(diào)用
        public void onBrightnessGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        //音量手勢,手指在Layout右半部上下滑動時候調(diào)用
        public void onVolumeGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        //快進快退手勢,手指在Layout左右滑動的時候調(diào)用
        public void onFF_REWGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        //單擊手勢,確認是單擊的時候調(diào)用
        public void onSingleTapGesture(MotionEvent e);
        //雙擊手勢,確認是雙擊的時候調(diào)用
        public void onDoubleTapGesture(MotionEvent e);
        //按下手勢,第一根手指按下時候調(diào)用
        public void onDown(MotionEvent e);
        //快進快退執(zhí)行后的松開時候調(diào)用
        public void onEndFF_REW(MotionEvent e);
    }

為了給onScroll()分成3個部分,這里采用一個小小的狀態(tài)模式,給它定義4個狀態(tài):NONE,VOLUME,BRIGHTNESS,FF_REW。只有NONE狀態(tài)才能進入其他狀態(tài),其它狀態(tài)一旦進入了不可切換,這樣就保證了用戶劃著音量的時候不會突然就平移就改變了進度:

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接口的回調(diào)了。

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) {
                //實現(xiàn)快進快退松開時候的回調(diào)
                if (event.getAction() == MotionEvent.ACTION_UP){
                    if (hasFF_REW){
                        if (mVideoGestureListener != null) {
                            mVideoGestureListener.onEndFF_REW(event);
                        }
                        hasFF_REW = false;
                    }
                }
                //監(jiān)聽觸摸事件
                return mGestureDetector.onTouchEvent(event);
            }
        });
    }
//...
}

由于GestureDetector沒有滑動之后松開的回調(diào),這里在onTouch()方補一個回調(diào)。

具體控制實現(xiàn)

來到這里,接口已經(jīng)做好了,用戶的手勢我們都收到相應(yīng)的回調(diào)了,然后我們要做的是定義收到這些回調(diào)的時候的操作。

中間顯示框

在這里我做了一個比較丑的中間顯示框,里面包含著一個ImageView和一個ProgressBar,默認延時一秒后消失:

/**
 * Author: yanzhikai
 * Description: 中間用于顯示狀態(tài)的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);
    }

    //設(shè)置進度
    public void setProgress(int progress){
        pb.setProgress(progress);
        Log.d(TAG, "setProgress: " +progress);
    }

    //設(shè)置持續(xù)時間
    public void setDuration(int duration) {
        this.duration = duration;
    }

    //設(shè)置顯示圖片
    public void setImageResource(int resource){
        iv_center.setImageResource(resource);
    }

    //隱藏自己的Runnable
    private class HideRunnable implements Runnable{
        @Override
        public void run() {
            ShowChangeLayout.this.setVisibility(GONE);
        }
    }
}

亮度、音量、進度調(diào)節(jié)

首先是進行初始化:

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);

        //初始化亮度調(diào)節(jié)
        mBrightnessHelper = new BrightnessHelper(this);

        //下面這是設(shè)置當前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){
            //一開始是默認亮度的時候,獲取系統(tǒng)亮度,計算比例值
            brightness = mBrightnessHelper.getBrightness() / 255f;
        }
    }
//...
}

每次onDown()都更新3個值的原因:onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)是每次Move事件位移大于1像素都會執(zhí)行的(原因可看上一篇源碼分析),distanceXdistanceY是兩個MOVE之間的距離決定的,如果手指移動得比較慢,它們就會比較小,float轉(zhuǎn)化成int很可能會被舍去小數(shù)點后的然后變成0,讓用戶不能慢慢通過滑動精準調(diào)控參數(shù),所以要使用e2和e1的位移差來決定亮度等參數(shù)的變化大小,從而就要在onDown()獲取舊數(shù)值來作為起始點了,詳細邏輯請看代碼:

//...
    @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);
        //要強行轉(zhuǎn)Float類型才能算出小數(shù)點,不然結(jié)果一直為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) {

        //下面這是設(shè)置當前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("設(shè)置進度為" + newProgress);
    }

    @Override
    public void onFF_REWGesture(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        float offset = e2.getX() - e1.getX();
        //根據(jù)移動的正負決定快進還是快退
        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();
    }
//...

這里沒有處理多點觸控,所以實際效果是觸控手指只看第一根,后面落下的手指動作都忽視。
  最后貼出調(diào)節(jié)系統(tǒng)亮度的輔助類:

/**
 * Author: yanzhikai
 * Description: 用于輔助調(diào)節(jié)亮度的類
 * Email: yanzhikai_yjk@qq.com
 */

public class BrightnessHelper {
    private ContentResolver resolver;
    private int maxBrightness = 255;

    public BrightnessHelper(Context context){
        resolver = context.getContentResolver();
    }

    /*
     * 調(diào)整亮度范圍
     */
    private int adjustBrightnessNumber(int brightness){
        if (brightness < 0) {
            brightness = 0;
        } else if (brightness > 255) {
            brightness = 255;
        }
        return brightness;
    }

    /*
     * 關(guān)閉自動調(diào)節(jié)亮度
     */
    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();
        }
    }

    /*
     * 獲取系統(tǒng)亮度
     */
    public int getBrightness(){
        return Settings.System.getInt(resolver, Settings.System.SCREEN_BRIGHTNESS, 255);
    }

    /*
     * 設(shè)置系統(tǒng)亮度,如果有設(shè)置了自動調(diào)節(jié),請先調(diào)用offAutoBrightness()方法關(guān)閉自動調(diào)節(jié),否則會設(shè)置失敗
     */
    public void setSystemBrightness(int newBrightness){
        Settings.System.putInt(resolver, Settings.System.SCREEN_BRIGHTNESS
                ,adjustBrightnessNumber(newBrightness));
    }

    public int getMaxBrightness() {
        return maxBrightness;
    }


    /*
     * 設(shè)置當前APP的亮度
     */
    public void setAppBrightness(float brightnessPercent, Activity activity){
        Window window = activity.getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        layoutParams.screenBrightness = brightnessPercent;
        window.setAttributes(layoutParams);
    }
}

上面調(diào)節(jié)亮度選擇的是控制當前APP的亮度,不改變系統(tǒng)亮度。其實這些控制的實現(xiàn)是多種多樣的,我這里只是給出一種封裝GestureDetector的思路和實現(xiàn)方法,各個步驟都說得挺清楚了,具體的細節(jié)大家可以根據(jù)自己的需求改動。

后話

這篇博客是上一篇GestureDetector全面分析的后續(xù),是GestureDetector的實踐,原本是想合在一起的,結(jié)果發(fā)現(xiàn)太長了。經(jīng)過GestureDetector的洗禮之后,感覺我對Android觸摸事件輸入的處理更加熟悉了,在這里分享出來我的經(jīng)驗,水平有限,如有錯漏,敬請指正。
  最后貼一下demo地址,喜歡的可以給個Star!
  https://github.com/totond/GestureTest

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容