概述
新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,然后在底部悬浮一个小框,还可以左右拖动,然后回弹的时候也会有相应的效果,这种交互效果在头条视频和一些专注于视频的app也是很常见的。
前几天看网友有仿这个 效果,觉得不错,现在分享出来,代码可以再优化,这里的播放器使用的是B站的ijkplayer,先上两张动图。
当图片到达底部后,左右拖动
实现的思路
首先,要是拖动视图缩小的效果,我们肯定需要自定义一个View,而根据我们项目的场景我们这里需要两个View,一个是拖动的View,另一个是浮动上下的View(可以缩小的View),为了实现拖动,我们知道必定会用到ViewDragHelper这个类,这个类专门为了拖动而设计的。
然后,对于拖动到底部的View,我们需要实现左右拖动的效果,这个其实也是比较容易实现的,我们通过ViewDragHelper的onViewPositionChanged方法来判断当前视图的状况,就可以做View进行缩放和渐变了。
代码分析
首先我们会自定义一个容器,容器的init方法会初始化两个View:mFlexView (到底拖动的View)和mFollowView (跟随触摸缩放的View)
private void init(Context context, AttributeSet attrs) {
final float density = getResources().getDisplayMetrics().density;
final float minVel = MIN_FLING_VELOCITY * density;
ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
FlexCallback flexCallback = new FlexCallback();
mDragHelper = ViewDragHelper.create(this, 1.0f, flexCallback);
mDragHelper.setMinVelocity(minVel);
post(new Runnable() {
@Override
public void run() {
mFlexView = getChildAt(0);
mFollowView = getChildAt(1);
mDragHeight = getMeasuredHeight() - mFlexView.getMeasuredHeight();
mFlexWidth = mFlexView.getMeasuredWidth();
mFlexHeight = mFlexView.getMeasuredHeight();
}
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
ViewDragHelper 的回调需要做的事情比较多,在 mFlexView 拖动的时候需要同时设置 mFlexView 和 mFollowView 的相应变化效果,在 mFlexView 释放的时候需要处理关闭或收起等效果。所以这里我们需要对ViewDragHelper个各种回调事件进行监听。这也是本功能最核心的:
private class FlexCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mFlexView == child;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return Math.max(Math.min(mDragWidth, left), -mDragWidth);
}
@Override
public int getViewHorizontalDragRange(View child) {
return mDragWidth * 2;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
if (!mVerticalDragEnable) {
return mDragHeight;
}
return Math.max(Math.min(mDragHeight, top), 0);
}
@Override
public int getViewVerticalDragRange(View child) {
return mDragHeight;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (mHorizontalDragEnable) {
if (xvel > 1500) {
mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight);
mIsClosing = true;
} else if (xvel < -1500) {
mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight);
mIsClosing = true;
} else {
float alpha = releasedChild.getAlpha();
if (releasedChild.getLeft() < 0 && alpha <= 0.4f) {
mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight);
mIsClosing = true;
} else if (releasedChild.getLeft() > 0 && alpha <= 0.4f) {
mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight);
mIsClosing = true;
} else {
mDragHelper.settleCapturedViewAt(0, mDragHeight);
}
}
} else {
if (yvel > 1500) {
mDragHelper.settleCapturedViewAt(0, mDragHeight);
} else if (yvel < -1500) {
mDragHelper.settleCapturedViewAt(0, 0);
} else {
if (releasedChild.getTop() <= mDragHeight / 2) {
mDragHelper.settleCapturedViewAt(0, 0);
} else {
mDragHelper.settleCapturedViewAt(0, mDragHeight);
}
}
}
invalidate();
}
@Override
public void onViewPositionChanged(final View changedView, int left, int top, int dx, int dy) {
float fraction = top * 1.0f / mDragHeight;
mFlexScaleRatio = 1 - 0.5f * fraction;
mFlexScaleOffset = changedView.getWidth() / 20;
changedView.setPivotX(changedView.getWidth() - mFlexScaleOffset);
changedView.setPivotY(changedView.getHeight() - mFlexScaleOffset);
changedView.setScaleX(mFlexScaleRatio);
changedView.setScaleY(mFlexScaleRatio);
float alphaRatio = 1 - fraction;
mFollowView.setAlpha(alphaRatio);
mFollowView.setTop(mFollowView.getTop() + dy);
mHorizontalDragEnable = top == mDragHeight;
if (mHorizontalDragEnable) {
mDragWidth = (int) (changedView.getMeasuredWidth() * 0.5f);
changedView.setAlpha(1 - Math.abs(left) * 1.0f / mDragWidth);
mVerticalDragEnable = left < 0 && left >= -mDragWidth * 0.05;
} else {
changedView.setAlpha(1);
mDragWidth = 0;
mVerticalDragEnable = true;
}
if (mFlexLayoutPosition == null) {
mFlexLayoutPosition = new ChildLayoutPosition();
mFollowLayoutPosition = new ChildLayoutPosition();
}
mFlexLayoutPosition.setPosition(mFlexView.getLeft(), mFlexView.getRight(), mFlexView.getTop(), mFlexView.getBottom());
mFollowLayoutPosition.setPosition(mFollowView.getLeft(), mFollowView.getRight(), mFollowView.getTop(), mFollowView.getBottom());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
接下来是处理测量和定位,我们实现的排列效果类似 LinearLayout 垂直排列的效果,这里需要对 measureChildWithMargins 的 heightUse 重新设置;onLayout 的时候在位置缓存不为空的时候直接定位是因为 ViewDragHelper 在处理触摸事件子元素在做一些平移之类的,若是有元素更新了 UI 会导致重新 Layout,因此在 FlexCallback 的 onViewPositionChanged 方法记录位置,然后在回弹的时候需要通过Layout 恢复之前的视图。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desireHeight = 0;
int desireWidth = 0;
int tmpHeight = 0;
if (getChildCount() != 2) {
throw new IllegalArgumentException("只允许容器添加两个子View!");
}
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, tmpHeight);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
desireWidth = Math.max(desireWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
tmpHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
desireHeight += tmpHeight;
}
desireWidth += getPaddingLeft() + getPaddingRight();
desireHeight += getPaddingTop() + getPaddingBottom();
desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
}
setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mFlexLayoutPosition != null) {
mFlexView.layout(mFlexLayoutPosition.getLeft(), mFlexLayoutPosition.getTop(), mFlexLayoutPosition.getRight(), mFlexLayoutPosition.getBottom());
mFollowView.layout(mFollowLayoutPosition.getLeft(), mFollowLayoutPosition.getTop(), mFollowLayoutPosition.getRight(), mFollowLayoutPosition.getBottom());
return;
}
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
int multiHeight = 0;
int count = getChildCount();
if (count != 2) {
throw new IllegalArgumentException("此容器的子元素个数必须为2!");
}
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int left = paddingLeft + lp.leftMargin;
int right = child.getMeasuredWidth() + left;
int top = (i == 0 ? paddingTop : 0) + lp.topMargin + multiHeight;
int bottom = child.getMeasuredHeight() + top;
child.layout(left, top, right, bottom);
multiHeight += (child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
触摸事件的处理,由于缩放不会影响 mFlexView 真实宽高,ViewDragHelper 仍然会阻断 mFlexView 的真实宽高的区域,所以这里判断手指是否落在 mFlexView 视觉上的范围内,在才去调 ViewDragHelper 的 shouldInterceptTouchEvent 方法。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float left = mFlexView.getLeft() + mFlexWidth * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);
float top = mFlexView.getTop() + mFlexHeight * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);
mInFlexViewTouchRange = ev.getX() >= left && ev.getY() >= top;
if (mInFlexViewTouchRange) {
return mDragHelper.shouldInterceptTouchEvent(ev);
} else {
return super.onInterceptTouchEvent(ev);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mInFlexViewTouchRange) {
mDragHelper.processTouchEvent(event);
return true;
} else {
return false;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
同时我们需要对滚动事件进行监听,我们需要在此关闭的整个平移执行事件。
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
invalidate();
} else if (mIsClosing && mOnLayoutStateListener != null) {
mOnLayoutStateListener.onClose();
mIsClosing = false;
}
}
/**
* 监听布局是否水平拖动关闭了
*/
public interface OnLayoutStateListener {
void onClose();
}
public void setOnLayoutStateListener(OnLayoutStateListener onLayoutStateListener) {
mOnLayoutStateListener = onLayoutStateListener;
}
/**
* 展开布局
*/
public void expand() {
mDragHelper.smoothSlideViewTo(mFlexView, 0, 0);
invalidate();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
而在实际的应用中要实现回弹后详情页面的效果,我们需要自己实现一个组合View,这个大家可以自己看源码音悦台源码
附:ViewDragHelper及移动事件处理