欧同学

一枚切尔西死忠和安卓开发者


  • Home

  • Archives

  • Tags
欧同学

高仿音悦台播放页面交互效果

Posted on 2017-01-10

新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,这个页面透明渐变,然后到底部可以左右拖动关闭播放器,然后点击视频列表有个页面弹出来的效果,十分炫酷,于是我自己动手实现了这个交互炫酷的播放器页面。

1.废话不多说,直接演示实现效果

1.1.点击某个视频,然后手指上下拖动,播放器做尺寸比例的渐变,视频相关信息做透明度渐变
这里写图片描述

1.2.播放器只有在底部的时候,才能左右拖动,此时播放器做透明度渐变,拖动一定范围可以关闭播放器;然后它只有在原始位置的一小段距离内可以往上拖动
这里写图片描述

1.3.点击视频列表的时候,若是上次视频是左右拖动关闭的话,会有个弹起播放页面的效果;若是返回键和返回箭头则无效果
这里写图片描述

2.实现的思路讲解

  • 毫无疑问,需要自定义一个容器,然后处理它的触摸事件,对它的子 View 进行不同的处理。触摸事件的处理使用 ViewDragHelper 是再适合不过了,然后你需要实现容器 onMeasure 和 onLayout,由于使用了 ViewDragHelper,有些坑在代码解析的时候就会讲解。
  • 播放页面是用新的 Activity 还仅仅是当前 Activity 的View的问题,由于播放器缩小到底部的时候,用户是可以滑动视频列表的,所以我个人认为就是在当前 Activity 放置一个自定义容器即可,因此为了效率考虑你可以用 ViewStub 来懒加载处理,这里方便演示我就直接 View 的形式了。

3.代码解析

3.1.需要的变量

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
/**
* Created by Oubowu on 201612/26 13:58.<p>
* 实现了布局交互的容器
*/
public class YytLayout extends ViewGroup {
private static final int MIN_FLING_VELOCITY = 400;
private ViewDragHelper mDragHelper;
// 拖动的宽度
private int mDragWidth;
// 拖动的高度
private int mDragHeight;
// 响应拖动做缩放的View
private View mFlexView;
// 与mFlexView联动做透明度渐变的View
private View mFollowView;
// 响应拖动做缩放的View保存的位置
private ChildLayoutPosition mFlexLayoutPosition;
// 与mFlexView联动的View保存的位置
private ChildLayoutPosition mFollowLayoutPosition;
// 水平拖动与否的标志位
private boolean mHorizontalDragEnable;
public boolean isHorizontalDragEnable() {
return mHorizontalDragEnable;
}
// 垂直拖动与否的标志位
private boolean mVerticalDragEnable = true;
// 是否正在关闭页面的标志位
private boolean mIsClosing;
// 监听布局是否水平拖动关闭了
private OnLayoutStateListener mOnLayoutStateListener;
// 做拖放缩放的子View的宽度
private int mFlexWidth;
// 做拖放缩放的子View的高度
private int mFlexHeight;
// mFlexView缩放的比率
private float mFlexScaleRatio = 1;
// mFlexView缩放的基准点的偏移值
private int mFlexScaleOffset;
// 触摸事件是否发生在mFlexView的区域
private boolean mInFlexViewTouchRange;

3.2.初始化做 ViewDragHelper 的初始化,然后 post 拿到两个子 View,这里强制规定只能有两个子元素

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
public YytLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
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() {
// 需要添加的两个子View,其中mFlexView作为拖动的响应View,mLinkView作为跟随View
mFlexView = getChildAt(0);
mFollowView = getChildAt(1);
mDragHeight = getMeasuredHeight() - mFlexView.getMeasuredHeight();
mFlexWidth = mFlexView.getMeasuredWidth();
mFlexHeight = mFlexView.getMeasuredHeight();
}
});
}

3.3. ViewDragHelper 的回调需要做的事情比较多,在 mFlexView 拖动的时候需要同时设置 mFlexView 和 mFollowView 的相应变化效果,在 mFlexView 释放的时候需要处理关闭或收起等效果

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
private class FlexCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
// mFlexView来响应触摸事件
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) {
// 不允许垂直拖动的时候是mFlexView在底部水平拖动一定距离时设置的,返回mDragHeight就不能再垂直做拖动了
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 {
// 速度没到关闭页面的要求,根据透明度来决定关闭页面,方向根据releasedChild.getLeft()正负决定
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 {
// 根据releasedChild.getTop()决定布局的展示方式
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;
// mFlexView缩放的比率
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);
// mFollowView透明度的比率
float alphaRatio = 1 - fraction;
// 设置透明度
mFollowView.setAlpha(alphaRatio);
// 根据垂直方向的dy设置top,产生跟随mFlexView的效果
mFollowView.setTop(mFollowView.getTop() + dy);
// 到底部的时候,changedView的top刚好等于mDragHeight,以此作为水平拖动的基准
mHorizontalDragEnable = top == mDragHeight;
if (mHorizontalDragEnable) {
// 如果水平拖动允许的话,由于设置缩放不会影响mFlexView的宽高(比如getWidth),所以水平拖动距离为mFlexView宽度一半
mDragWidth = (int) (changedView.getMeasuredWidth() * 0.5f);
// 设置mFlexView的透明度,这里向左右水平拖动透明度都随之变化
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());
// Log.e("FlexCallback", "225行-onViewPositionChanged(): 【" + mFlexView.getLeft() + ":" + mFlexView.getRight() + ":" + mFlexView.getTop() + ":" + mFlexView
// .getBottom() + "】 【" + mFollowView.getLeft() + ":" + mFollowView.getRight() + ":" + mFollowView.getTop() + ":" + mFollowView.getBottom() + "】");
}
}

3.4.接下来是处理测量和定位,我们实现的排列效果类似 LinearLayout 垂直排列的效果,这里被 measureChildWithMargins 的 heightUse 摆了一道;onLayout 的时候在位置缓存不为空的时候直接定位是因为 ViewDragHelper 在处理触摸事件子元素在做一些平移之类的,若是有元素更新了 UI 会导致重新 Layout,例如我的播放器在更新时间的 TextView 时就会如此,因此在 FlexCallback 的 onViewPositionChanged 方法记录位置,在重新 Layout 时恢复位置即可,这个也坑了好久

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
@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);
// 测量子元素并考虑外边距
// 参数heightUse:父容器竖直已经被占用的空间,比如被父容器的其他子 view 所占用的空间;这里我们需要的是子View垂直排列,所以需要设置这个值
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) {
// 因为在用到ViewDragHelper处理布局交互的时候,若是有子View的UI更新导致重新Layout的话,需要我们自己处理ViewDragHelper拖动时子View的位置,否则会导致位置错误
// Log.e("YytLayout1", "292行-onLayout(): " + "自己处理布局位置");
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);
}
}

3.5.触摸事件的处理,由于缩放不会影响 mFlexView 真实宽高,ViewDragHelper 仍然会阻断 mFlexView 的真实宽高的区域,所以这里判断手指是否落在 mFlexView 视觉上的范围内,在才去调 ViewDragHelper 的 shouldInterceptTouchEvent 方法

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 boolean onInterceptTouchEvent(MotionEvent ev) {
// Log.e("YytLayout", mFlexView.getLeft() + ";" + mFlexView.getTop() + " --- " + ev.getX() + ":" + ev.getY());
// 由于缩放不会影响mFlexView真实宽高,这里手动计算视觉上的范围
float left = mFlexView.getLeft() + mFlexWidth * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);
float top = mFlexView.getTop() + mFlexHeight * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio);
// 这里所做的是判断手指是否落在mFlexView视觉上的范围内
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) {
// 这里还要做判断是因为,即使我不阻断事件,但是此Layout的子View不消费的话,事件还是给回此Layout
mDragHelper.processTouchEvent(event);
return true;
} else {
// 不在mFlexView触摸范围内,并且子View没有消费,返回false,把事件传递回去
return false;
}
}

3.6.在 computeScroll 中,若是 mIsClosing 为 true,即关闭的整个平移执行完毕了,通知回调事件

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
@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();
}

3.7.容器实现了,接下来我们继承 YytLayout 实现播放器页面的组合控件即可,再封装一些常用的方法,这里使用的是大名鼎鼎的 Ijkplayer 实现的播放器,屏蔽了 IjkVideoView 的触摸事件自己处理了;顺带一提,为了实现播放器 Controller 跟随拖动缩放的效果,放弃了常用的 PopupWindow 实现的思路,IjkController 直接是添加到 IjkVideoView 中的,要不弹窗实现跟随播放器太麻烦了

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/**
* Created by Oubowu on 2016/12/27 17:32.<p>
* 仿音悦台播放页面的具体实现,组合控件的形式
*/
public class YytPlayer extends YytLayout {
private IjkController mIjkController;
private IjkVideoView mIjkVideoView;
private ImageView mIvAvatar;
private TextView mTvName;
private TextView mTvTime;
private TextView mTvTitle;
private TextView mTvDesc;
private RecyclerView mYytRecyclerView;
private VideoListAdapter mVideoListAdapter;
public YytPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 继承YytLayout并且通过merge标签减少层级来实现组合控件
LayoutInflater.from(context).inflate(R.layout.yyt_player, this, true);
setOnLayoutStateListener(new OnLayoutStateListener() {
@Override
public void onClose() {
setVisibility(View.INVISIBLE);
mIjkVideoView.release(true);
}
});
mIjkVideoView = (IjkVideoView) findViewById(R.id.ijk_player_view);
final int scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mIjkVideoView.setOnTouchListener(new OnTouchListener() {
float mDownX = 0;
float mDownY = 0;
boolean mClickCancel;
@Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = x;
mDownY = y;
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(mDownX - x) > scaledTouchSlop || Math.abs(mDownY - y) > scaledTouchSlop) {
mClickCancel = true;
}
break;
case MotionEvent.ACTION_UP:
if (!mClickCancel && Math.abs(mDownX - x) <= scaledTouchSlop && Math.abs(mDownY - y) <= scaledTouchSlop) {
// 点击事件偶尔失效,只好这里自己解决了
if (isHorizontalDragEnable()) {
expand();
} else {
mIjkVideoView.toggleMediaControlsVisibility();
}
}
mClickCancel = false;
break;
case MotionEvent.ACTION_CANCEL:
mClickCancel = false;
break;
}
return true;
}
});
mIvAvatar = (ImageView) findViewById(R.id.iv_avatar);
mTvName = (TextView) findViewById(R.id.tv_name);
mTvTime = (TextView) findViewById(R.id.tv_time);
mTvTitle = (TextView) findViewById(R.id.tv_title);
mTvDesc = (TextView) findViewById(R.id.tv_desc);
mVideoListAdapter = new VideoListAdapter();
mVideoListAdapter.setOnItemClickCallback(new OnItemClickCallback() {
@Override
public void onClick(View view, int position) {
int pos = (Integer) view.getTag();
VideoSummary summary = mVideoListAdapter.getData().get(pos);
playVideo(mVideoListAdapter.getData(), summary);
}
});
mYytRecyclerView = (RecyclerView) findViewById(R.id.yyt_recycler_view);
GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false);
mYytRecyclerView.setLayoutManager(gridLayoutManager);
mYytRecyclerView.setNestedScrollingEnabled(false);
mYytRecyclerView.addItemDecoration(new VideoListItemDecoration(context));
mYytRecyclerView.setAdapter(mVideoListAdapter);
}
// 播放视频
private void playVideo(String path, String name) {
try {
if (mIjkController == null) {
IjkMediaPlayer.loadLibrariesOnce(null);
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
mIjkController = new IjkController(mIjkVideoView, name);
mIjkController.setOnViewStateListener(new IjkController.OnViewStateListener() {
@Override
public void onBackPress() {
stop();
}
});
mIjkVideoView.setMediaController(mIjkController);
mIjkVideoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(IMediaPlayer mp) {
mIjkVideoView.start();
}
});
mIjkVideoView.setOnErrorListener(new IMediaPlayer.OnErrorListener() {
@Override
public boolean onError(IMediaPlayer mp, int what, int extra) {
Toast.makeText(getContext(), "视频播放出错了╮(╯Д╰)╭", Toast.LENGTH_SHORT).show();
return true;
}
});
} else {
// 重新设置视频名字
mIjkController.setVideoName(name);
}
// 设置这个TextureView播放器缩放就正常了
mIjkVideoView.setRender(IjkVideoView.RENDER_TEXTURE_VIEW);
// 因为每次setRender都会移除view再添加,为了缩放效果这里控制器是添加到IjkVideoView中的,所以这里也要重新添加才能在IjkVideoView的最上面
mIjkController.updateControlView();
// 显示加载条
mIjkController.showProgress();
// 播放视频
mIjkVideoView.setVideoURI(Uri.parse(path));
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
Toast.makeText(getContext(), "你的CPU是" + Build.CPU_ABI + ",当前播放器使用的编译版本" + BuildConfig.FLAVOR + "不匹配!", Toast.LENGTH_LONG).show();
}
}
/**
* 显示布局,并且播放视频
*
* @param data 视频列表,用于播放页面下面的列表布局
* @param summary 播放的视频信息
*/
public void playVideo(List<VideoSummary> data, VideoSummary summary) {
// 拿到数据,设置到播放的布局的相关信息
Glide.with(getContext()).load(summary.mTopicImg).transform(new GlideCircleTransform(getContext())).into(mIvAvatar);
mTvName.setText(summary.mTopicName);
mTvTime.setText(summary.mPtime);
mTvTitle.setText(Html.fromHtml(summary.mTitle));
if (summary.mDescription.isEmpty()) {
mTvDesc.setText(summary.mTopicDesc);
} else {
mTvDesc.setText(Html.fromHtml(summary.mDescription));
}
// 设置YytLayout可见,并且展开
setVisibility(View.VISIBLE);
expand();
mVideoListAdapter.setData(data);
mVideoListAdapter.setItemWidth(mYytRecyclerView.getWidth() / 2);
mVideoListAdapter.notifyDataSetChanged();
// 播放视频
playVideo(summary.mMp4HdUrl == null ? summary.mMp4Url : summary.mMp4HdUrl, summary.mTitle);
}
// 开始播放
public void start() {
if (mIjkVideoView != null && !mIjkVideoView.isPlaying()) {
mIjkVideoView.start();
}
}
// 暂停播放
public void pause() {
if (mIjkVideoView != null && mIjkVideoView.isPlaying()) {
mIjkVideoView.pause();
}
}
// 停止播放
public void stop() {
setVisibility(View.INVISIBLE);
if (mIjkVideoView != null) {
mIjkVideoView.release(true);
}
}
public boolean isShowing() {
return getVisibility() == VISIBLE;
}
}

4.总结

说难也不难,就是各种抠细节需要脑洞,各位不妨看到好玩的交互自己打开脑洞一下,接下来可能要实现下 UC 浏览器播放器的效果,感觉也是非常有意思。

#扔个Demo地址吧:Github:YinyuetaiPlayer,觉得还不错的话给个Star哦!

欧同学

一种无痕过渡下拉刷新控件的实现思路

Posted on 2016-11-22

相信大家已经对下拉刷新熟悉得不能再熟悉了,市面上的下拉刷新琳琅满目,然而有很多在我看来略有缺陷,接下来我将说明一下存在的缺陷问题,然后提供一种思路来解决这一缺陷,废话不多说!往下看嘞!


1.市面一些下拉刷新控件普遍缺陷演示

以直播吧APP为例:

第1种情况:
滑动控件在初始的0位置时,手势往下滑动然后再往上滑动,可以看到滑动到初始位置时滑动控件不能滑动。

原因:
下拉刷新控件响应了触摸事件,后续的一系列事件都由它来处理,当滑动控件到顶端的时候,滑动事件都被下拉刷新控件消费掉了,传递不到它的子控件即滑动控件,因此滑动控件不能滑动。

这里写图片描述

第2种情况:
滑动控件滑动到某个非0位置时,这时下拉回0位置时,可以看到下拉刷新头部没有被拉出来。

原因:
滑动控件响应了触摸事件,后续的一系列事件都由它来处理,当滑动控件到顶端的时候,滑动事件都被滑动控件消费掉了,父控件即下拉刷新控件消费不了滑动事件,因此下拉刷新头部没有被拉出来。

这里写图片描述

可能大部分人觉得无关痛痒,把手指抬起再下拉就可以了,but对于强迫症的我而言,能提供一个无痕过渡才是最符合操作逻辑的,因此接下来我来讲解下实现的思路。


2.实现的思路讲解

2.1.事件分发机制简介(来源于Android开发艺术探索)

dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent方法的关系伪代码

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}

1.由代码可知若当前View拦截事件,就交给自己的onTouchEvent去处理,否则就丢给子View继续走相同的流程。
2.事件传递顺序:Activity -> Window -> View,如果View都不处理,最终将由Activity的onTouchEvent
处理,是一种责任链模式的实现。
3.正常情况,一个事件序列只能被一个View拦截且消耗。
4.某个View一旦决定拦截,这一个事件序列只能由它处理,并且它的onInterceptTouchEvent不会再被调用
5.不消耗ACTION_DOWN,则事件序列都会由其父元素处理。

2.2.一般下拉刷新的实现思路猜想
首先,下拉刷新控件作为一个容器,需要重写onInterceptTouchEvent和onTouchEvent这两个方法,
然后在onInterceptTouchEvent中判断ACTION_DOWN事件,根据子控件的滑动距离做出判断,若还没滑动过,则onInterceptTouchEvent返回true表示其拦截事件,然后在onTouchEvent中进行下拉刷新的头部显示隐藏的逻辑处理;
若子控件滑动过了,不拦截事件,onInterceptTouchEvent返回false,后续其下拉刷新的头部显示隐藏的逻辑处理就无法被调用了。

2.3.无痕过渡下拉刷新控件的实现思路
从2.2中可以看出,要想无痕过渡,下拉刷新控件不能拦截事件,这时候你可能会问,既然把事件给了子控件,后续拉刷新头部逻辑怎么实现呢?
这时候就要用到一般都忽略的事件分发方法dispatchTouchEvent了,此方法在ViewGroup默认返回true表示分发事件,即使子控件拦截了事件,父布局的dispatchTouchEvent仍然会被调用,因为事件是传递下来的,这个方法必定被调用。
所以我们可以在dispatchTouchEvent时对子控件的滑动距离做出判断,在这里把下拉刷新的头部的逻辑处理掉,同时在函数调用return super.dispatchTouchEvent(event) 前把event的action设置为ACTION_CANCEL,这样子子控件就不会响应滑动的操作。


3.代码实现

3.1.确定需求

  • 需要适配任意控件,例如RecyclerView、ListView、ViewPager、WebView以及普通的不能滑动的View
  • 不能影响子控件原来的事件逻辑
  • 暴露方法提供手动调用刷新功能
  • 可以设置禁止下拉刷新功能

3.2.代码讲解

需要的变量

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
public class RefreshLayout extends LinearLayout {
// 隐藏的状态
private static final int HIDE = 0;
// 下拉刷新的状态
private static final int PULL_TO_REFRESH = 1;
// 松开刷新的状态
private static final int RELEASE_TO_REFRESH = 2;
// 正在刷新的状态
private static final int REFRESHING = 3;
// 正在隐藏的状态
private static final int HIDING = 4;
// 当前状态
private int mCurrentState = HIDE;
// 头部动画的默认时间(单位:毫秒)
public static final int DEFAULT_DURATION = 200;
// 头部高度
private int mHeaderHeight;
// 内容控件的滑动距离
private int mContentViewOffset;
// 最小滑动响应距离
private int mScaledTouchSlop;
// 记录上次的Y坐标
private float mLastMotionY;
// 记录一开始的Y坐标
private float mInitDownY;
// 响应的手指
private int mActivePointerId;
// 是否在处理头部
private boolean mIsHeaderHandling;
// 是否可以下拉刷新
private boolean mIsRefreshable = true;
// 内容控件是否可以滑动,不能滑动的控件会做触摸事件的优化
private boolean mContentViewScrollable = true;
// 头部,为了方便演示选取了TextView
private TextView mHeader;
// 容器要承载的内容控件,在XML里面要放置好
private View mContentView;
// 值动画,由于头部显示隐藏
private ValueAnimator mHeaderAnimator;
// 刷新的监听器
private OnRefreshListener mOnRefreshListener;

初始化时创建头部执行显示隐藏的值动画,添加头部到布局中,并且通过设置paddingTop隐藏头部

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
public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
addHeader(context);
}
private void init() {
mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mHeaderAnimator = ValueAnimator.ofInt(0).setDuration(DEFAULT_DURATION);
mHeaderAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
if (getContext() == null) {
// 若是退出Activity了,动画结束不必执行头部动作
return;
}
// 通过设置paddingTop实现显示或者隐藏头部
int offset = (Integer) valueAnimator.getAnimatedValue();
mHeader.setPadding(0, offset, 0, 0);
}
});
mHeaderAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (getContext() == null) {
// 若是退出Activity了,动画结束不必执行头部动作
return;
}
if (mCurrentState == RELEASE_TO_REFRESH) {
// 释放刷新状态执行的动画结束,意味接下来就是刷新了,改状态并且调用刷新的监听
mHeader.setText("正在刷新...");
mCurrentState = REFRESHING;
if (mOnRefreshListener != null) {
mOnRefreshListener.onRefresh();
}
} else if (mCurrentState == HIDING) {
// 下拉状态执行的动画结束,隐藏头部,改状态
mHeader.setText("我是头部");
mCurrentState = HIDE;
}
}
});
}
// 头部的创建
private void addHeader(Context context) {
// 强制垂直方法
setOrientation(LinearLayout.VERTICAL);
mHeader = new TextView(context);
mHeader.setBackgroundColor(Color.GRAY);
mHeader.setTextColor(Color.WHITE);
mHeader.setText("我是头部");
mHeader.setTextSize(TypedValue.COMPLEX_UNIT_SP, 25);
mHeader.setGravity(Gravity.CENTER);
addView(mHeader, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
mHeader.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 算出头部高度
mHeaderHeight = mHeader.getMeasuredHeight();
// 移除监听
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mHeader.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mHeader.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// 设置paddingTop为-mHeaderHeight,刚好把头部隐藏掉了
mHeader.setPadding(0, -mHeaderHeight, 0, 0);
}
});
}

在填充完布局后取出内容控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 设置长点击或者短点击都能消耗事件,要不这样做,若孩子都不消耗,最终点击事件会被它的上级消耗掉,后面一系列的事件都只给它的上级处理了
setLongClickable(true);
// 获取内容控件
mContentView = getChildAt(1);
if (mContentView == null) {
// 为空抛异常,强制要求在XML设置内容控件
throw new IllegalArgumentException("You must add a content view!");
}
if (!(mContentView instanceof ScrollingView
|| mContentView instanceof WebView
|| mContentView instanceof ScrollView
|| mContentView instanceof AbsListView)) {
// 不是具有滚动的控件,这里设置标志位
mContentViewScrollable = false;
}
}

重头戏来了,分发对于下拉刷新的特殊处理:
1.mContentViewOffset用于判别内容页的滑动距离,在无偏移值时才去处理下拉刷新的操作;
2.在mContentViewOffset!=0即内容页滑动的第一个瞬间,强制把MOVE事件改为DOWN,是因为之前MOVE都被拦截掉了,若不给个DOWN让内容页重新定下滑动起点,会有一瞬间滑动一大段距离的坑爹效果。

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
@Override
public boolean dispatchTouchEvent(final MotionEvent event) {
if (!mIsRefreshable) {
// 禁止下拉刷新,直接把事件分发
return super.dispatchTouchEvent(event);
}
if ((mCurrentState == REFRESHING
|| mCurrentState == RELEASE_TO_REFRESH
|| mCurrentState == HIDING)
&& mHeaderAnimator.isRunning()) {
// 正在刷新,正在释放,正在隐藏头部都不处理事件,并且不分发下去
return true;
}
// 支持多指触控
int actionMasked = MotionEventCompat.getActionMasked(event);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
// 记录响应的手指
mActivePointerId = event.getPointerId(0);
// 记录初始Y坐标
mInitDownY = mLastMotionY = event.getY(0);
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
// 另外一根手指按下,切换到这个手指响应
int pointerDownIndex = MotionEventCompat.getActionIndex(event);
if (pointerDownIndex < 0) {
Log.e("RefreshLayout", "296行-dispatchTouchEvent(): " + "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return dispatchTouchEvent(event);
}
mActivePointerId = event.getPointerId(pointerDownIndex);
mLastMotionY = event.getY(pointerDownIndex);
}
break;
case MotionEvent.ACTION_POINTER_UP: {
// 另外一根手指抬起,切换回其他手指响应
final int pointerUpIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = event.getPointerId(pointerUpIndex);
if (pointerId == mActivePointerId) {
// 抬起手指就是之前控制滑动手指,切换其他手指响应
final int newPointerIndex = pointerUpIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
}
mLastMotionY = event.getY(event.findPointerIndex(mActivePointerId));
}
break;
case MotionEvent.ACTION_MOVE: {
// 移动事件
if (mActivePointerId == INVALID_POINTER) {
Log.e("RefreshLayout", "235行-dispatchTouchEvent(): " + "Got ACTION_MOVE event but don't have an active pointer id.");
return dispatchTouchEvent(event);
}
float y = event.getY(event.findPointerIndex(mActivePointerId));
// 移动的偏移量
float yDiff = y - mLastMotionY;
mLastMotionY = y;
if (mContentViewOffset == 0 && (yDiff > 0 || (yDiff < 0 && isHeaderShowing()))) {
// 内容控件还没滚动时,下拉或者在头部还在显示的时候上滑时,交由自己处理滑动事件
// 滑动的总距离
float totalDistanceY = mLastMotionY - mInitDownY;
if (totalDistanceY > 0 && totalDistanceY <= mScaledTouchSlop && yDiff > 0) {
// 下拉时,优化滑动逻辑,不要稍微一点位移就响应
return super.dispatchTouchEvent(event);
}
// 正在处理事件
mIsHeaderHandling = true;
if (mCurrentState == REFRESHING) {
// 正在刷新,不让contentView响应滑动
event.setAction(MotionEvent.ACTION_CANCEL);
}
// 处理下拉头部
scrollHeader(yDiff);
break;
} else if (mIsHeaderHandling) {
// 在头部隐藏的那一瞬间的事件特殊处理
if (mContentViewScrollable) {
// 1.可滑动的View,由于之前处理头部,之前的MOVE事件没有传递到内容页,这里需要要ACTION_DOWN来重新告知滑动的起点,不然会瞬间滑动一段距离
// 2.对于不滑动的View设置了点击事件,若这里给它一个ACTION_DOWN事件,在手指抬起时ACTION_UP事件会触发点击,因此这里做了处理
event.setAction(MotionEvent.ACTION_DOWN);
}
mIsHeaderHandling = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
// 处理手指抬起或取消事件
mActivePointerId = INVALID_POINTER;
if (isHeaderShowing()) {
// 头部显示情况下
if (actionMasked == MotionEvent.ACTION_CANCEL) {
// 取消的话强制不能刷新,状态改为下拉刷新,接下来autoScrollHeader就会隐藏头部
mCurrentState = PULL_TO_REFRESH;
}
autoScrollHeader();
}
}
break;
default:
break;
}
if (mCurrentState != REFRESHING
&& isHeaderShowing()
&& actionMasked != MotionEvent.ACTION_UP
&& actionMasked != MotionEvent.ACTION_POINTER_UP) {
// 不是在刷新的时候,并且头部在显示, 某些情况下不让contentView响应事件
event.setAction(MotionEvent.ACTION_CANCEL);
}
return super.dispatchTouchEvent(event);
}

头部的处理逻辑:拿到下拉偏移量,然后动态去设置头部的paddingTop值,即可实现显示隐藏;手指抬起时根据状态决定是显示刷新还是直接隐藏头部

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
/**
* 拉动头部
*
* @param diff 拉动距离
*/
private void scrollHeader(float diff) {
// 除以3相当于阻尼值
diff /= 3;
// 计算出移动后的头部位置
int top = (int) (diff + mHeader.getPaddingTop());
// 控制头部位置最小不超过-mHeaderHeight,最大不超过mHeaderHeight * 3
mHeader.setPadding(0, Math.min(Math.max(top, -mHeaderHeight), mHeaderHeight * 3), 0, 0);
if (mCurrentState == REFRESHING) {
// 之前还在刷新状态,继续维持刷新状态
mHeader.setText("正在刷新...");
return;
}
if (mHeader.getPaddingTop() > mHeaderHeight / 2) {
// 大于mHeaderHeight / 2时可以刷新了
mHeader.setText("可以释放刷新...");
mCurrentState = RELEASE_TO_REFRESH;
} else {
// 下拉状态
mHeader.setText("正在下拉...");
mCurrentState = PULL_TO_REFRESH;
}
}
/**
* 执行头部显示或隐藏滑动
*/
private void autoScrollHeader() {
// 处理抬起事件
if (mCurrentState == RELEASE_TO_REFRESH) {
// 释放刷新状态,手指抬起,通过动画实现头部回到(0,0)位置
mHeaderAnimator.setIntValues(mHeader.getPaddingTop(), 0);
mHeaderAnimator.setDuration(DEFAULT_DURATION);
mHeaderAnimator.start();
mHeader.setText("正在释放...");
} else if (mCurrentState == PULL_TO_REFRESH || mCurrentState == REFRESHING) {
// 下拉状态或者正在刷新状态,通过动画隐藏头部
mHeaderAnimator.setIntValues(mHeader.getPaddingTop(), -mHeaderHeight);
if (mHeader.getPaddingTop() <= 0) {
mHeaderAnimator.setDuration((long) (DEFAULT_DURATION * 1.0 /
mHeaderHeight * (mHeader.getPaddingTop() + mHeaderHeight)));
} else {
mHeaderAnimator.setDuration(DEFAULT_DURATION);
}
mHeaderAnimator.start();
if (mCurrentState == PULL_TO_REFRESH) {
// 下拉状态的话,把状态改为正在隐藏头部状态
mCurrentState = HIDING;
mHeader.setText("收回头部...");
}
}
}

你可能会问了,这个mContentViewOffset怎么知道呢?接下来就是处理的方法,我会针对不同的滑动控件,去设置它们的滑动距离的监听,方法各种各样,通过handleTargetOffset去判别View的类型采取不同的策略;然后你可能会觉得要是我那个控件我也要实现监听咋办?这个简单,继承我已经实现的监听器,再补充你想要的功能即可,这个时候就不能再调handleTargetOffset这个方法了呗。

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
// 设置内容页滑动距离
public void setContentViewOffset(int offset) {
mContentViewOffset = offset;
}
/**
* 根据不同类型的View采取不同类型策略去计算滑动距离
*
* @param view 内容View
*/
public void handleTargetOffset(View view) {
if (view instanceof RecyclerView) {
((RecyclerView) view).addOnScrollListener(new RecyclerViewOnScrollListener());
} else if (view instanceof NestedScrollView) {
((NestedScrollView) view).setOnScrollChangeListener(new NestedScrollViewOnScrollChangeListener());
} else if (view instanceof WebView) {
view.setOnTouchListener(new WebViewOnTouchListener());
} else if (view instanceof ScrollView) {
view.setOnTouchListener(new ScrollViewOnTouchListener());
} else if (view instanceof ListView) {
((ListView) view).setOnScrollListener(new ListViewOnScrollListener());
}
}
/**
* 适用于RecyclerView的滑动距离监听
*/
public class RecyclerViewOnScrollListener extends RecyclerView.OnScrollListener {
int offset = 0;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
offset += dy;
setContentViewOffset(offset);
}
}
/**
* 适用于NestedScrollView的滑动距离监听
*/
public class NestedScrollViewOnScrollChangeListener implements NestedScrollView.OnScrollChangeListener {
@Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
setContentViewOffset(scrollY);
}
}
/**
* 适用于WebView的滑动距离监听
*/
public class WebViewOnTouchListener implements View.OnTouchListener {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
setContentViewOffset(view.getScrollY());
return false;
}
}
/**
* 适用于ScrollView的滑动距离监听
*/
public class ScrollViewOnTouchListener extends WebViewOnTouchListener {
}
/**
* 适用于ListView的滑动距离监听
*/
public class ListViewOnScrollListener implements AbsListView.OnScrollListener {
@Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == 0) {
View c = view.getChildAt(0);
if (c == null) {
return;
}
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = c.getTop();
int scrolledY = -top + firstVisiblePosition * c.getHeight();
setContentViewOffset(scrolledY);
} else {
setContentViewOffset(1);
}
}
}

最后参考谷歌大大的SwipeRefreshLayout提供setRefreshing来开启或关闭刷新动画,至于openHeader为啥要post(Runnable)呢?相信用过SwipeRefreshLayout在onCreate的时候直接调用setRefreshing(true)没有小圆圈出来的都知道这个坑!

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
public void setRefreshing(boolean refreshing) {
if (refreshing && mCurrentState != REFRESHING) {
// 强开刷新头部
openHeader();
} else if (!refreshing) {
closeHeader();
}
}
private void openHeader() {
post(new Runnable() {
@Override
public void run() {
mCurrentState = RELEASE_TO_REFRESH;
mHeaderAnimator.setDuration((long) (DEFAULT_DURATION * 2.5));
mHeaderAnimator.setIntValues(mHeader.getPaddingTop(), 0);
mHeaderAnimator.start();
}
});
}
private void closeHeader() {
mHeader.setText("刷新完毕,收回头部...");
mCurrentState = HIDING;
mHeaderAnimator.setIntValues(mHeader.getPaddingTop(), -mHeaderHeight);
// 0~-mHeaderHeight用时DEFAULT_DURATION
mHeaderAnimator.setDuration(DEFAULT_DURATION);
mHeaderAnimator.start();
}

3.3.效果展示

这里写图片描述

这里写图片描述

这里写图片描述

除了以上三个还有在Demo中实现了ListView、ViewPager、ScrollView、NestedScrollView,具体看代码即可

Demo地址:Github:RefreshLayoutDemo,觉得还不错的话给个Star哦。

欧同学

Untitled

Posted on 2016-11-08

个人博客搭成

使用Hexo搭建而成了,有点意思啊!以后在这写点啥分享啦!

图片

Bowu Ou

Bowu Ou

3 posts
1 tags
© 2017 Bowu Ou
Powered by Hexo
Theme - NexT.Muse