先看实现的效果


GestureDetector相关
GestureDetector 提供的MotionEvents检测各种手势和事件。当发生特定的运动事件时,GestureDetector.OnGestureListener回调将通知用户。
GestureDetector 的内部实现其实就是将onTouchEvent(MotionEvent ev) 将一系列的touch事件处理然后回调给用户。然而GestureDetector extends Object ,它本身是没有touch事件的。 所以,需要重写view的onTouch(MotionEvents event) 将event传递给GestureDetector.onTouchEvent(MotionEvent ev),说白了 GestureDetector 就是对一系列的touch事件的一个封装。
如何使用GestureLayout
使用GestureLayout 就很简单了
mGestureDetector = new GestureDetector(context, gestureListener);
/** 手势监听 */
GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//滑动事件
}
@Override
public boolean onDoubleTap(MotionEvent e) {//双击事件
}
@Override
public boolean onSingleTapUp(MotionEvent e) {//点击事件
}
};
@Override
public boolean onTouch(View v, MotionEvent event) {
boolean touchEvent = mGestureDetector.onTouchEvent(event);
return touchEvent;
}
实践GestureLayout在视频的重要作用,改变音量、亮度和进度
这块感觉没什么可讲的,主要就是拿到手势回调 然后换算一下,然后通过监听回调给视频的控制器,让控制器继承ControllerGestureLayout就可以了,直接上代码了
public abstract class ControllerGestureLayout extends FrameLayout implements View.OnTouchListener {
public static final int NONE = 0, VOLUME = 1, BRIGHTNESS = 2, POSITION = 3;
private @ScrollMode
int mScrollMode = NONE;
@IntDef({NONE, VOLUME, BRIGHTNESS, POSITION})
@Retention(RetentionPolicy.SOURCE)
private @interface ScrollMode {
}
protected GestureDetector mGestureDetector;
public VideoGestureListener mVideoGestureListener;
private int offX = 1;
private int offInX = 10;
private int oldVolume = 0;
private int maxVolume = 0;
private float brightness = 1;
private static final String TAG = "ControllerGestureLayout";
private BrightnessHelper brightnessHelper;
private AudioHelper audioHelper;
private long currentPosition, duration;
private float oldPosition = 0;
private boolean isGesture = true;
public ControllerGestureLayout(@NonNull Context context) {
super(context);
init(context);
}
public ControllerGestureLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ControllerGestureLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
setOnTouchListener(this);
mGestureDetector = new GestureDetector(context, gestureListener);
brightnessHelper = new BrightnessHelper(context);
audioHelper = new AudioHelper(context);
}
/** 手势监听 */
GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (builder().isGesture()) {
down();
return true;
} else {
return false;
}
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//滑动事件
if (builder().isGesture()) {
return gestureMove(e1, e2, distanceX, distanceY);
} else {
return false;
}
}
@Override
public boolean onDoubleTap(MotionEvent e) {//双击事件
if (builder().isGesture()) {
onDouble();
return true;
} else {
return false;
}
}
@Override
public boolean onSingleTapUp(MotionEvent e) {//点击事件
if (builder().isGesture()) {
if (null != mVideoGestureListener) {
mVideoGestureListener.onSingleTapUp();
}
return true;
} else {
return false;
}
}
};
/** 双击事件 */
private void onDouble() {
if (!builder().isDouble()) {
return;
}
if (null != mVideoGestureListener) {
mVideoGestureListener.onDoubleTap();
}
}
/** 手指按下 */
private void down() {
brightness = brightnessHelper.getAppBrightness(getContext());
if (brightness == -1) {
brightness = brightnessHelper.getSystemBrightness() / 255;
}
mScrollMode = NONE;
oldVolume = audioHelper.getCurrentVolume();
maxVolume = audioHelper.getMaxVolume();
currentPosition = getVideoCurrentPosition();
duration = getVideoDuration();
oldPosition = 1000L * currentPosition / duration;
}
public abstract long getVideoCurrentPosition();
public abstract long getVideoDuration();
/** 手势移动 */
private boolean gestureMove(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!builder().isMove()) {
return false;
}
switch (mScrollMode) {
case NONE:
return noneChange(e1, distanceX, distanceY);
case VOLUME:
return changeVolume(e1, e2);
case BRIGHTNESS:
return changeBrightness(e1, e2);
case POSITION:
return changePosition(e1, e2);
default:
return false;
}
}
/** 改变进度 */
private boolean changePosition(MotionEvent e1, MotionEvent e2) {
if (!builder().isPosition()) {
if (mVideoGestureListener != null) {
mVideoGestureListener.onGesture(false);
}
return false;
}
if (mVideoGestureListener != null) {
mVideoGestureListener.onGesture(true);
}
float offX = e2.getX() - e1.getX();
float movePercent = (1000L * (offX / getWidth()) * duration) / duration;
float value = movePercent + oldPosition;
if (value < 0) {
value = 0;
} else if (value > 1000) {
value = 1000;
}
if (mVideoGestureListener != null) {
mVideoGestureListener.onMovePosition((long) (duration * value / 1000), value);
}
return true;
}
/** 改变亮度 */
private boolean changeBrightness(MotionEvent e1, MotionEvent e2) {
if (!builder().isBrightness()) {
if (mVideoGestureListener != null) {
mVideoGestureListener.onGesture(false);
}
return false;
}
if (mVideoGestureListener != null) {
mVideoGestureListener.onGesture(true);
}
float newBrightness = (e1.getY() - e2.getY()) / getHeight();
newBrightness += brightness;
if (newBrightness < 0) {
newBrightness = 0;
} else if (newBrightness > 1) {
newBrightness = 1;
}
Window window = Utils.scanForActivity(getContext()).getWindow();
WindowManager.LayoutParams attributes = window.getAttributes();
attributes.screenBrightness = newBrightness;
window.setAttributes(attributes);
if (mVideoGestureListener != null) {
mVideoGestureListener.showBrightness((int) (newBrightness * 100));
}
return true;
}
/** 改变声音 */
private boolean changeVolume(MotionEvent e1, MotionEvent e2) {
if (!builder().isVolume()) {
return false;
}
getParent().requestDisallowInterceptTouchEvent(false);//touch hand over parent view handle
int value = getHeight() / maxVolume;
int newVolume = (int) ((e1.getY() - e2.getY()) / value + oldVolume);
int volumeProgress = (int) (newVolume / Float.valueOf(maxVolume) * 100);
audioHelper.setVolume(newVolume);
if (mVideoGestureListener != null) {
mVideoGestureListener.showVolume(volumeProgress);
}
return true;
}
/** 没有改变 */
private boolean noneChange(MotionEvent e1, float distanceX, float distanceY) {
if (mVideoGestureListener != null) {
mVideoGestureListener.onGesture(true);
}
if ((Math.abs(distanceX) - Math.abs(distanceY)) > offX) {
mScrollMode = POSITION;
} else {
if (e1.getX() < getWidth() / 2) {
mScrollMode = BRIGHTNESS;
} else {
mScrollMode = VOLUME;
}
}
return false;
}
private ViewParent getControllerParent() {
try {
return getParent().getParent();
} catch (NullPointerException e) {
return null;
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
boolean touchEvent = mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (mVideoGestureListener != null) {
mVideoGestureListener.onTouchCancle(mScrollMode);
}
break;
}
return touchEvent;
}
protected void actionUpGesture() {
if (mVideoGestureListener != null) {
mVideoGestureListener.onTouchCancle(mScrollMode);
}
}
public void hideChangePosition() {
if (mVideoGestureListener != null) {
mVideoGestureListener.onTouchCancle(POSITION);
}
}
public void doDown() {
down();
}
public boolean doChangePosition(float offX) {
if (!builder().isPosition()) {
if (mVideoGestureListener != null) {
mVideoGestureListener.onGesture(false);
}
return false;
}
if (mVideoGestureListener != null) {
mVideoGestureListener.onGesture(true);
}
float movePercent = (1000L * (offX / getWidth()) * duration) / duration;
float value = movePercent + oldPosition;
if (value < 0) {
value = 0;
} else if (value > 1000) {
value = 1000;
}
if (mVideoGestureListener != null) {
mVideoGestureListener.onMovePosition((long) (duration * value / 1000), value);
}
return true;
}
public void setVideoGestureListener(VideoGestureListener mVideoGestureListener) {
this.mVideoGestureListener = mVideoGestureListener;
}
public interface VideoGestureListener {
void onDoubleTap();
void onSingleTapUp();
void onMovePosition(long position, float value);
void showBrightness(int position);
void showVolume(int position);
void onTouchCancle(int scrollMode);
void onGesture(boolean gesture);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
public Builder builder = null;
public Builder builder() {
if (builder == null) {
builder = new Builder();
}
return builder;
}
public static class Builder {
Builder() {
}
/** 手势监听默认开启 */
private boolean isGesture = true;
/** 手势移动默认开启 */
private boolean isMove = true;
/** 手势移动音量默认开启 */
private boolean isVolume = true;
/** 手势移动亮度默认开启 */
private boolean isBrightness = true;
/** 手势移动进度默认开启 */
private boolean isPosition = true;
private boolean isDouble = true;
boolean isGesture() {
return isGesture;
}
boolean isMove() {
return isMove;
}
boolean isVolume() {
return isVolume;
}
boolean isBrightness() {
return isBrightness;
}
public boolean isPosition() {
return isPosition;
}
public boolean isDouble() {
return isDouble;
}
public Builder setGestureDetector(boolean isGesture) {
this.isGesture = isGesture;
return this;
}
public Builder setGestureMove(boolean isMove) {
this.isMove = isMove;
return this;
}
public Builder setGestureVolume(boolean isVolume) {
this.isVolume = isVolume;
return this;
}
public Builder setGestureBrightness(boolean isBrightness) {
this.isBrightness = isBrightness;
return this;
}
public Builder setGesturePosition(boolean isPosition) {
this.isPosition = isPosition;
return this;
}
public Builder setDoublePlayOrPuse(boolean isDouble) {
this.isDouble = isDouble;
return this;
}
}
}
调节屏幕亮度的辅助类
public class BrightnessHelper {
private ContentResolver resolver;
private int maxBrightness = 255;
private static final String TAG = "BrightnessHelper";
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 getSystemBrightness() {
return Settings.System.getInt(resolver, Settings.System.SCREEN_BRIGHTNESS, 255);
}
/** 获取APP内的亮度 */
public float getAppBrightness(Context context) {
float screenBrightness = Utils.scanForActivity(context).getWindow().getAttributes().screenBrightness;
Log.e(TAG, "getAppBrightness: " + screenBrightness);
if (screenBrightness == -1) {
//一开始是默认亮度的时候,获取系统亮度,计算比例值
screenBrightness = getSystemBrightness() / 255f;
}
return screenBrightness;
}
}
调节音量的辅助类
public class AudioHelper {
protected AudioManager mAM;
public AudioHelper(Context context) {
mAM = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
public int getCurrentVolume() {
return mAM.getStreamVolume(AudioManager.STREAM_MUSIC);
}
public int getMaxVolume() {
return mAM.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
}
public void setVolume(int volume) {
mAM.setStreamVolume(AudioManager.STREAM_MUSIC, volume, AudioManager.FLAG_PLAY_SOUND);
}
}
实现效果如下:

RecyclerView的item中播放视频View使用GestureLayout横向滑动改变进度存在的问题
如果item中要进行GestureLayout 手势操作,也许你会想这很简单吗,在播放视频的view中实现GestureLayout监听就行了?
但是,如果item消费了onTouch事件,那么RecyclerView就不能滑动了,因为由子View消费掉了。
还有一种想法,在播放视频的view中实现GestureLayout监听中横向滑动就消费掉onTouch事件竖向滑动就拦截掉?
但是,经过一系列测试,还是无法实现,原因是什么呢?
GestureLayout 监听的 onDown() 要想监听滑动事件 也就是MoationEvent.ACTION_MOVE,就必须返回true,也就相当于MoationEvent.ACTION_DOWN必须返回true,否则无法检测到滑动事件,简单来说,父view和子view必须有一个去消费掉onTouch事件,这样不管如何拦截都不能达成你想要的效果,横向滑动改变进度,竖向滑动交给RecyclerView。
那么如何在列表中实现以下效果呢?

如何解决上述问题呢?
查找了大量的资料看到了RecyclerView有一个这样的方法:
RecyclerView.addOnItemTouchListener(itemTouchListener);
这个方法就是监听item的触摸事件的,那么我们是否可以在这上面进行修改呢?
先看一下,这个监听方法:
public static interface OnItemTouchListener {
//这个方法中设有拦截处理
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);
//当某个事件被拦截时,会走这个方法
public void onTouchEvent(RecyclerView rv, MotionEvent e);
// 当RecyclerView的一个孩子不想要RecyclerView及其祖先时调用
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}
看到这个监听方法我已经有了思路了:
上述问题无法处理的原因的是,子View和父view无法解决的冲突问题,那么我们就用一个view来进行滑动判断,这样就没有冲突了,这个view当然是RecyclerView,那么我们如何通过触摸来拿到正在播放的item的视频的view呢?
当然很简单RecyclerView 是很强大的存在,它有一个这样的方法:
通过触摸的x,y的坐标可以拿到触摸的itemView
View childViewUnder = rv.findChildViewUnder(e.getX(), e.getY());
然后我们再根据,childViewUnder 拿到当前的位置position和已知正在播放的item的位置进行判断。
if (childViewUnder != null) {
//get item view position in the list
int childAdapterPosition = rv.getChildAdapterPosition(childViewUnder);
//if touch item view position equals record the position of the video being played in the list
if (itemPosition == childAdapterPosition) {
RecyclerView.ViewHolder childViewHolder = rv.getChildViewHolder(childViewUnder);
if (childViewHolder instanceof VideoFeedHolder) {//get ViewHolder
VideoFeedHolder feedHolder = (VideoFeedHolder) childViewHolder;
FrameLayout ll_video = feedHolder.ll_video;
//Judging whether the touch is a video playback view
if (isTouchPointInView(ll_video, e.getX(), e.getY())) {
intercept = true;
}
}
}
}
然后在通过ViewHolder拿到视频的view,通过x,y坐标来判断是否触摸在视频的view上面,如果在上面就拦截开始滑动改变播放进度
private boolean isTouchPointInView(View view, float x, float y) {
if (view == null) {
return false;
}
int[] location = new int[2];
view.getLocationOnScreen(location);
int left = location[0];
int top = location[1];
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
//view.isClickable() &&
if (y >= top && y <= bottom && x >= left
&& x <= right) {
return true;
}
return false;
}
具体如何改变播放进度,要根据你具体的实现的播放器view代码来实现
下面是完整的代码:
public class RecyclerItemTouchListener extends RecyclerView.SimpleOnItemTouchListener {
/**
* whether to intercept touch event
*/
private boolean intercept = false;
/**
* record touch down x , y
*/
private float downX, downY;
/**
* video view
*/
private VideoPlayerView videoPlayerView;
/**
* record the position of the video being played in the list
*/
private int itemPosition = 0;
/**
* touch move offset
*/
private static final int MOVE_OFFSET = 20;
public RecyclerItemTouchListener(VideoPlayerView videoPlayerView) {
this.videoPlayerView = videoPlayerView;
}
/**
* set position of the video being played
*
* @param itemPosition
* int - position
*/
public void setItemPosition(int itemPosition) {
this.itemPosition = itemPosition;
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = e.getX();
downY = e.getY();
if (videoPlayerView != null) {
videoPlayerView.doDown();
}
break;
case MotionEvent.ACTION_MOVE:
float dX = downX - e.getX();
float dY = downY - e.getY();
if (Math.abs(dX) > MOVE_OFFSET && Math.abs(dY) > MOVE_OFFSET) {//correct sliding operation
if (Math.abs(dX) - Math.abs(dY) > MOVE_OFFSET) {//if horizontal slide
//According to coordinates get recyclerView touch item View
View childViewUnder = rv.findChildViewUnder(e.getX(), e.getY());
if (childViewUnder != null) {
//get item view position in the list
int childAdapterPosition = rv.getChildAdapterPosition(childViewUnder);
//if touch item view position equals record the position of the video being played in the list
if (itemPosition == childAdapterPosition) {
RecyclerView.ViewHolder childViewHolder = rv.getChildViewHolder(childViewUnder);
if (childViewHolder instanceof VideoFeedHolder) {//get ViewHolder
VideoFeedHolder feedHolder = (VideoFeedHolder) childViewHolder;
FrameLayout ll_video = feedHolder.ll_video;
//Judging whether the touch is a video playback view
if (isTouchPointInView(ll_video, e.getX(), e.getY())) {
intercept = true;
}
}
}
}
} else {
intercept = false;
}
} else {
intercept = false;
}
break;
}
return intercept;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dX = downX - e.getX();
float dY = downY - e.getY();
if (Math.abs(dX) > MOVE_OFFSET && Math.abs(dY) > MOVE_OFFSET) {
if (Math.abs(dX) - Math.abs(dY) > MOVE_OFFSET) {
if (videoPlayerView != null) {
videoPlayerView.doChangePosition(-dX);
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
intercept = false;
if (videoPlayerView != null) {
videoPlayerView.hideChangePosition();
}
break;
}
}
/**
* Judging whether the touch view
*
* @param view
* View
* @param x
* float
* @param y
* float
*
* @return boolean
*/
private boolean isTouchPointInView(View view, float x, float y) {
if (view == null) {
return false;
}
int[] location = new int[2];
view.getLocationOnScreen(location);
int left = location[0];
int top = location[1];
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
//view.isClickable() &&
if (y >= top && y <= bottom && x >= left
&& x <= right) {
return true;
}
return false;
}
}
思考View的事件分发机制
所谓的事件分发就是MoationEvent事件的分发过程,当MoationEvent 产生后,系统就需要把这个事件传递给一个具体的view,这个传递的过程就是分发过程。
完成分发过程主要有以下三个重要的方法完成:
方法 | 描述 |
---|---|
dispatchTouchEvent | 用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会调用,返回的结果受当前view的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗了当前事件。 |
onInterceptTouchEvent | 用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一事件序列中,此方法不会再调用,返回结果表示是否拦截当前事件。 |
onTouchEvent | 在dispatchTouchEvent方法中调用,用来处理触摸事件,返回结果表示是否消耗当前事件。 |
可以用一段伪代码来表示事件的分发机制:
public boolean dispatchTouchEvent(MoationEvent event){
boolean coustom = false;
if(onInterceptTouchEvent(event)){
coustom = onTouchEvent(event);
}else{
coustom = cilde.dispatchTouchEvent(event);
}
return coustom;
}
不管多么复杂发滑动冲突都是遵循事件的分发机制来进行的,可以用许多思路来解决问题。
注意调节亮度后需要恢复亮度
/**
* What is changed is the brightness of the activity of the current window.
* The brightness of other activities does not change,and it need to be reset
* to the previous brightness when exiting.
*/
public void recoverBrightness() {
if (isRecordBrightness) {
brightnessHelper.setAppBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE, getContext());
}
}
网友评论