今天要写的这个效果属于刷新类,比较实用,像很多流行的 app 都是用了这种效果,大家熟知的QQ空间、微博个人主页等,这个效果在 github 上也有别人实现好的源码,点击查看。这里也参考了上面的源码;还是那句话,看 blog主要是学习其中的原理和思路。
动态效果图
图片放大的原理是什么呢?
通过改变图片显示控件 ImageView 的父控件的高度,比如这里的头部 View 是一个 FrameLayout,FrameLayout 中再 通过 add 方法把图片 View 添加进去,addView(ImageView),ImageView有几个属性是要特别注意的,ImageView 的放缩类型为从中间截取
1
|
setScaleType(ImageView.ScaleType.CENTER_CROP);
|
并且宽高设为匹配父控件;所以想要图片有放大效果,只需设置 FrameLayout 的 LayoutParams 中的 height值,通过改变 height 的值从而改变 ImageView 的显示高度。讲的有点混乱,可以结合下面的代码来理解。
如果你是对手势事件处理很了解的朋友,对这个效果的实现应该没有什么难度,唯一的一点就是判断何时该放大图片,何时该滚动ListView。
这里就直接贴代码:
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
|
/**
* Created by gyzhong on 15/3/22.
*/
public
class
PullZoomListView
extends
ListView {
/*头部View 的容器*/
private
FrameLayout mHeaderContainer;
/*头部View 的图片*/
private
ImageView mHeaderImg;
/*屏幕的高度*/
private
int
mScreenHeight;
/*屏幕的宽度*/
private
int
mScreenWidth;
private
int
mHeaderHeight;
/*无效的点*/
private
static
final
int
INVALID_POINTER = -
1
;
/*滑动动画执行的时间*/
private
static
final
int
MIN_SETTLE_DURATION =
200
;
// ms
/*定义了一个时间插值器,根据ViewPage控件来定义的*/
private
static
final
Interpolator sInterpolator =
new
Interpolator() {
public
float
getInterpolation(
float
t) {
t -=
1
.0f;
return
t * t * t * t * t +
1
.0f;
}
};
/*记录上一次手指触摸的点*/
private
float
mLastMotionX;
private
float
mLastMotionY;
/*当前活动的点Id,有效的点的Id*/
protected
int
mActivePointerId = INVALID_POINTER;
/*开始滑动的标志距离*/
private
int
mTouchSlop;
/*放大的倍数*/
private
float
mScale;
/*上一次放大的倍数*/
private
float
mLastScale;
/*最大放大的倍数*/
private
final
float
mMaxScale =
2
.0f;
/*是否需要禁止ListView 的事件响应*/
private
boolean
isNeedCancelParent;
/*这个不解释*/
private
OnScrollListener mScrollListener ;
/*下拉刷新的阈值*/
private
final
float
REFRESH_SCALE =
1
.20F;
/*下拉刷新监听*/
private
OnRefreshListener mRefreshListener ;
public
PullZoomListView(Context context) {
super
(context);
init(context);
}
public
PullZoomListView(Context context, AttributeSet attrs) {
super
(context, attrs);
init(context);
}
public
PullZoomListView(Context context, AttributeSet attrs,
int
defStyleAttr) {
super
(context, attrs, defStyleAttr);
init(context);
}
private
void
init(Context context) {
/*这里获取的是一个无用值,可忽略*/
final
ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
/*创建头部View 的容器*/
mHeaderContainer =
new
FrameLayout(context);
/*获取屏幕的像素值*/
DisplayMetrics metrics =
new
DisplayMetrics();
((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(metrics);
mScreenHeight = metrics.heightPixels;
mScreenWidth = metrics.widthPixels;
/*设置头部View 的初始大小*/
mHeaderHeight = (
int
) ((
9
*
1
.0f /
16
) * mScreenWidth);
LayoutParams absLayoutParams =
new
LayoutParams(mScreenWidth, mHeaderHeight);
mHeaderContainer.setLayoutParams(absLayoutParams);
/*创建图片显示的View*/
mHeaderImg =
new
ImageView(context);
FrameLayout.LayoutParams imgLayoutParams =
new
FrameLayout.LayoutParams
(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mHeaderImg.setScaleType(ImageView.ScaleType.CENTER_CROP);
mHeaderImg.setLayoutParams(imgLayoutParams);
mHeaderContainer.addView(mHeaderImg);
/*增加头部View*/
addHeaderView(mHeaderContainer);
/*设置监听事件*/
super
.setOnScrollListener(
new
InternalScrollerListener() );
}
/*处理事件用*/
@Override
public
boolean
onTouchEvent(MotionEvent ev) {
final
int
action = ev.getAction() & MotionEventCompat.ACTION_MASK;
switch
(action) {
case
MotionEvent.ACTION_DOWN:
/*计算 x,y 的距离*/
int
index = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
if
(mActivePointerId == INVALID_POINTER)
break
;
mLastMotionX = MotionEventCompat.getX(ev, index);
mLastMotionY = MotionEventCompat.getY(ev, index);
// 结束动画,目前没做处理,可忽略
abortAnimation();
/*计算算一次放缩的比例*/
mLastScale = (
this
.mHeaderContainer.getBottom() /
this
.mHeaderHeight);
/*当按下的时候把这个标志为设为有效*/
isNeedCancelParent =
true
;
break
;
case
MotionEvent.ACTION_MOVE:
int
indexMove = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, indexMove);
if
(mActivePointerId == INVALID_POINTER) {
/*这里相当于松手*/
finishPull();
isNeedCancelParent =
true
;
}
else
{
/*这是一个关键值,通过这个值来判断是否需要放大图片*/
if
(mHeaderContainer.getBottom() >= mHeaderHeight) {
ViewGroup.LayoutParams params =
this
.mHeaderContainer.getLayoutParams();
final
float
y = MotionEventCompat.getY(ev, indexMove);
float
dy = y - mLastMotionY;
float
f = ((y -
this
.mLastMotionY +
this
.mHeaderContainer
.getBottom()) /
this
.mHeaderHeight -
this
.mLastScale)
/
2
.0F +
this
.mLastScale;
if
((
this
.mLastScale <=
1
.0D) && (f <=
this
.mLastScale)) {
params.height =
this
.mHeaderHeight;
this
.mHeaderContainer.setLayoutParams(params);
return
super
.onTouchEvent(ev);
}
/*这里设置紧凑度*/
dy = dy *
0
.5f * (mHeaderHeight *
1
.0f / params.height);
mLastScale = (dy + params.height) *
1
.0f / mHeaderHeight;
mScale = clamp(mLastScale,
1
.0f, mMaxScale);
// Log.v(“zgy”, “=======mScale=====” + mLastScale+”,f = “+f);
params.height = (
int
) (mHeaderHeight * mScale);
mHeaderContainer.setLayoutParams(params);
mLastMotionY = y;
/*这里,如果图片有放大,则屏蔽ListView 的其他事件响应*/
if
(isNeedCancelParent ){
isNeedCancelParent =
false
;
MotionEvent motionEvent = MotionEvent.obtain(ev);
motionEvent.setAction(MotionEvent.ACTION_CANCEL);
super
.onTouchEvent(motionEvent);
}
return
true
;
}
mLastMotionY = MotionEventCompat.getY(ev, indexMove);
}
break
;
case
MotionEvent.ACTION_UP:
/*结束事件响应,做相应的操作*/
finishPull();
break
;
case
MotionEvent.ACTION_POINTER_UP:
/*这里需要注意,多点处理,这里的处理方式是:如果有两个手指按下,抬起的是后按下的手指,则不做处理
* 如果抬起的是最先按下的手指,则复原图片效果。
* */
int
pointUpIndex = MotionEventCompat.getActionIndex(ev);
int
pointId = MotionEventCompat.getPointerId(ev, pointUpIndex);
if
(pointId == mActivePointerId) {
/*松手执行结束拖拽操作*/
/*结束事件响应,做相应的操作*/
finishPull();
}
break
;
}
return
super
.onTouchEvent(ev);
}
@Override
public
void
setOnScrollListener(OnScrollListener l) {
mScrollListener = l ;
}
private
void
abortAnimation() {
/*啥都没做,暂时没做而已*/
}
private
void
finishPull() {
mActivePointerId = INVALID_POINTER;
/*这是一个阈值,如果成立,则表示图片已经放大了,在手指抬起的时候需要复原图片*/
if
(mHeaderContainer.getBottom() > mHeaderHeight){
// Log.v(“zgy”, “===super====onTouchEvent========”);
/<em>这里是下拉刷新的阈值,当达到了,则表示需要刷新,可以添加刷新动画</em>/
if
(mScale > REFRESH_SCALE){
if
(mRefreshListener !=
null
){
mRefreshListener.onRefresh();
}
}
//图片复原动画
pullBackAnimation();
}
}
/**
* 这是属性动画的知识,不懂的可以去看看属性动画的知识
*/
private
void
pullBackAnimation(){
ValueAnimator pullBack = ValueAnimator.ofFloat(mScale ,
1
.0f);
pullBack.setInterpolator(sInterpolator);
pullBack.addUpdateListener(
new
ValueAnimator.AnimatorUpdateListener() {
@Override
public
void
onAnimationUpdate(ValueAnimator animation) {
float
value = (
float
) animation.getAnimatedValue();
LayoutParams params = (LayoutParams) mHeaderContainer.getLayoutParams();
params.height = (
int
) (mHeaderHeight * value);
mHeaderContainer.setLayoutParams(params);
}
});
pullBack.setDuration((
long
) (MIN_SETTLE_DURATION*mScale));
pullBack.start();
}
/**
* 通过事件和点的 id 来获取点的索引
*
* @param ev
* @param id
* @return
*/
private
int
getPointerIndex(MotionEvent ev,
int
id) {
int
activePointerIndex = MotionEventCompat.findPointerIndex(ev, id);
if
(activePointerIndex == -
1
)
mActivePointerId = INVALID_POINTER;
return
activePointerIndex;
}
public
void
setOnRefreshListener(OnRefreshListener l){
mRefreshListener = l ;
}
public
ImageView getHeaderImageView() {
return
this
.mHeaderImg;
}
private
float
clamp(
float
value,
float
min,
float
max) {
return
Math.min(Math.max(value, min), max);
}
private
class
InternalScrollerListener
implements
OnScrollListener{
@Override
public
void
onScrollStateChanged(AbsListView view,
int
scrollState) {
if
(mScrollListener !=
null
){
mScrollListener.onScrollStateChanged(view,scrollState);
}
}
@Override
public
void
onScroll(AbsListView view,
int
firstVisibleItem,
int
visibleItemCount,
int
totalItemCount) {
float
diff = mHeaderHeight - mHeaderContainer.getBottom();
if
((diff >
0
.0F) && (diff < mHeaderHeight)) {
int
i = (
int
) (
0
.3D * diff);
mHeaderImg.scrollTo(
0
, -i);
}
else
if
(mHeaderImg.getScrollY() !=
0
) {
mHeaderImg.scrollTo(
0
,
0
);
}
Log.v(
"zgy"
,
"=========height==="
+getScrolledY());
if
(mScrollListener !=
null
){
mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}
}
public
int
getScrolledY() {
View c = getChildAt(
0
);
if
(c ==
null
) {
return
0
;
}
int
firstVisiblePosition = getFirstVisiblePosition();
int
top = c.getTop();
int
headerHeight =
0
;
if
(firstVisiblePosition >=
1
) {
headerHeight = mHeaderHeight;
}
return
-top + firstVisiblePosition * c.getHeight() + headerHeight;
}
public
interface
OnRefreshListener {
void
onRefresh() ;
}
public
void
computeRefresh(){
if
(mActivePointerId != INVALID_POINTER){
}
}
}
|
比较难理解的地方都做了注释,所以。。。应该还是很好理解的。
总结:
今天 blog的一个难点就是,手势的处理,下拉放大的条件判断;针对这种问题,我也没有很好的解决方案,我的经验就是,直接通过 打印 Log 的方式来寻找规律,因为有的时候想的挺烦的,而且逻辑很容易混乱。
接着是图片放大的原理,知道了就很好实现此功能。