自定义控件之重写ScrollView实现图片下拉放大

前言

因为公司项目要实现一个效果,在ScrollView没有向下滚动时,下拉(未重写前下拉是没有任何效果的)放大顶部的图片,当时去网上找了,记得以前见过很多这样的控件的,现在却找半天也很难找到一个,好不容易找到了2个,发现效果都和需求上面的效果有偏差,最后没有办法只能是自己写了,花费了半天时间研究出来了,同时为了记录实现思路,所以就有了此文章

效果

效果图

实现思路

拦截ScrollView的触摸滑动事件(ACTION_MOVE),记录下当前事件y轴坐标,判断当前ScrollView的Y轴滚动进度(getScrollY)是否等于0,等于0就与上次事件记录的位置进行对比,如果为正数就放大(X轴是从左往右,Y轴是从上往下,所以下拉时本次事件的Y轴会大于上次事件的Y轴),每次事件都通过设置ImageView的高度来放大图片控件(本来想用属性动画的,但是因为每个事件放大的比例非常小,所以最后就没使用,直接通过修改属性来实现),同时记录从开始到现在事件位置一共偏移了多少,当偏移量大于最大值的,就停止放大并将偏移量设置为最大值,当偏移量小于0时,则将偏移量设置为0,同时不再继续拦截事件。注意被放大的图片需要设置scaleType为centerCrop,这样当图片高度发生变化时,图片内容才会跟着大,当然其他几种模式有些模式也能跟着放大,但是具体可以自己去测试,我就不去测试了,毕竟我已经达到我要的效果了

好了,废话少说,先贴代码,再对代码进行说明

代码

COPY
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
package wang.raye.library;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;


/**
* 重写让ScrollView有滚动监听(23以前是没有滚动监听的)
* 拦截touch事件,让其支持下拉放大图片
* Created by Raye on 2016/6/11.
*/
public class ZoomScrollView extends ScrollView {

private View zoomView;
/** 记录上次事件的Y轴*/
private float mLastMotionY;
/** 记录一个滚动了多少距离,通过这个来设置缩放*/
private int allScroll = -1;
/** 控件原本的高度*/
private int height = 0;
/** 被放大的控件id*/
private int zoomId;
/** 最大放大多少像素*/
private int maxZoom;
/** 滚动监听*/
private ScrollViewListener scrollViewListener = null;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
allScroll -= 25;
if(allScroll < 0){
allScroll = 0;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
lp.height = (int) (height + allScroll/2);
zoomView.setLayoutParams(lp);
if(allScroll != 0){
handler.sendEmptyMessageDelayed(1,10);
}else{
allScroll = -1;
}
}
};
public ZoomScrollView(Context context) {
super(context);
}

public ZoomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}

public ZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}

@Override
protected void onFinishInflate() {
super.onFinishInflate();
zoomView = findViewById(zoomId);
}

private void init(AttributeSet attrs){
TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.ObservableScrollView);
zoomId = t.getResourceId(R.styleable.ObservableScrollView_zoomId,-1);
maxZoom = t.getDimensionPixelOffset(R.styleable.ObservableScrollView_maxZoom,0);
}

public void setScrollViewListener(ScrollViewListener scrollViewListener) {
this.scrollViewListener = scrollViewListener;
}


@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if(zoomView == null || maxZoom == 0){
return super.dispatchTouchEvent(event);
}

final int action = event.getAction();

if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
if(allScroll != -1){
handler.sendEmptyMessageDelayed(1,10);
}
return super.dispatchTouchEvent(event);
}

switch (action) {
case MotionEvent.ACTION_MOVE: {
final float y = event.getY();
final float diff, absDiff;
diff = y - mLastMotionY;
mLastMotionY = y;
absDiff = Math.abs(diff);
if(allScroll >= 0 && absDiff > 1){
allScroll += diff;

if(allScroll < 0){
allScroll = 0;
}else if(allScroll > maxZoom){
allScroll = maxZoom;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
lp.height = (int) (height + allScroll/2);
zoomView.setLayoutParams(lp);
if(allScroll == 0){
allScroll = -1;
}
return false;
}
if (isReadyForPullStart()) {
if (absDiff > 0 ) {
if (diff >= 1f && isReadyForPullStart()) {
mLastMotionY = y;
allScroll = 0;
height = zoomView.getHeight();
return true;
}
}
}
break;
}


}

return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
if(allScroll != -1){
Log.i("ScrollView","onTouchEvent");
return false;
}
return super.onTouchEvent(ev);
}



/**
* 返回是否可以开始放大
* @return
*/
protected boolean isReadyForPullStart() {
return getScrollY() == 0;
}


@Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
super.onScrollChanged(x, y, oldx, oldy);
if (scrollViewListener != null) {
scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);
}
}
public interface ScrollViewListener {

void onScrollChanged(ZoomScrollView scrollView, int x, int y, int oldx, int oldy);

}
}

重要点,从上往下

COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
allScroll -= 25;
if(allScroll < 0){
allScroll = 0;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
lp.height = (int) (height + allScroll/2);
zoomView.setLayoutParams(lp);
if(allScroll != 0){
handler.sendEmptyMessageDelayed(1,10);
}else{
allScroll = -1;
}
}
};

这里是当ACTION_UP事件发生时,如果图片还在放大状态,就模拟动画效果,吧图片缩放回去,当然是可以用属性动画的,只是我之前没用属性动画,所以这里也直接用这个了

COPY
1
2
3
4
5
@Override
protected void onFinishInflate() {
super.onFinishInflate();
zoomView = findViewById(zoomId);
}

这里是当控件从xml中初始化完成的生命周期方法,在这里我们找到被放大的图片控件

COPY
1
2
3
4
5
private void init(AttributeSet attrs){
TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.ObservableScrollView);
zoomId = t.getResourceId(R.styleable.ObservableScrollView_zoomId,-1);
maxZoom = t.getDimensionPixelOffset(R.styleable.ObservableScrollView_maxZoom,0);
}

这段代码相信很容易看懂,就是获取2个自定义属性,一个是被放大的图片控件id,一个是最大的放大像素

最主要的地方,事件拦截
COPY
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
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if(zoomView == null || maxZoom == 0){
return super.dispatchTouchEvent(event);
}

final int action = event.getAction();

if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
if(allScroll != -1){
handler.sendEmptyMessageDelayed(1,10);
}
return super.dispatchTouchEvent(event);
}

switch (action) {
case MotionEvent.ACTION_MOVE: {
final float y = event.getY();
final float diff, absDiff;
diff = y - mLastMotionY;
mLastMotionY = y;
absDiff = Math.abs(diff);
if(allScroll >= 0 && absDiff > 1){
allScroll += diff;

if(allScroll < 0){
allScroll = 0;
}else if(allScroll > maxZoom){
allScroll = maxZoom;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
lp.height = (int) (height + allScroll/2);
zoomView.setLayoutParams(lp);
if(allScroll == 0){
allScroll = -1;
}
return false;
}
if (isReadyForPullStart()) {
if (absDiff > 0 ) {
if (diff >= 1f && isReadyForPullStart()) {
mLastMotionY = y;
allScroll = 0;
height = zoomView.getHeight();
return true;
}
}
}
break;
}


}

return super.dispatchTouchEvent(event);
}
详细说明
COPY
1
2
3
if(zoomView == null || maxZoom == 0){
return super.dispatchTouchEvent(event);
}

当控件为空和最大放大像素为0 的时候,不进行事件拦截

COPY
1
2
3
4
5
6
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
if(allScroll != -1){
handler.sendEmptyMessageDelayed(1,10);
}
return super.dispatchTouchEvent(event);
}

当事件取消和手指松开时,判断当前偏移量(allScroll )是否回到了最初状态-1,如果没有说明图片没有缩放,要缩放回去

COPY
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
case MotionEvent.ACTION_MOVE: {
final float y = event.getY();
final float diff, oppositeDiff, absDiff;
diff = y - mLastMotionY;
mLastMotionY = y;
absDiff = Math.abs(diff);
if( allScroll >= 0 && absDiff > 1){
allScroll += diff;

if(allScroll < 0){
allScroll = 0;
}else if(allScroll > maxZoom){
allScroll = maxZoom;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
lp.height = (int) (height + allScroll/2);
zoomView.setLayoutParams(lp);
if(allScroll == 0){
allScroll = -1;
}
return false;
}
if (isReadyForPullStart()) {
if (absDiff > 0 ) {
if (diff >= 1f && isReadyForPullStart()) {
mLastMotionY = y;
allScroll = 0;
height = zoomView.getHeight();
return true;
}
}
}
break;
}

拦截移动事件,每次记录下Y轴坐标,当滚动为0的时候,就计算与上次坐标的偏移量,大于0就开始放大,每次放大总偏移值的二分之一,因为每次放大总偏移值的效果不大好看,同时判断总偏移值是否大于最大偏移值,大于就设置总偏移值为最大值,相当于停止放大。如果小于0,就把总偏移值设置为0,并且重置偏移值的为-1,-1的时候,就不会拦截事件

COPY
1
2
3
4
5
6
7
8
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(allScroll != -1){
Log.i("ScrollView","onTouchEvent");
return false;
}
return super.onTouchEvent(ev);
}

重写onTouchEvent,当偏移值不是-1的时候,说明图片在进行放大或缩放,这时候不能让ScrollView滚动,所以需要把onTouchEvent拦截掉

COPY
1
2
3
protected boolean isReadyForPullStart() {
return getScrollY() == 0;
}

获取当前ScrollView的滚动位置,是0的时候才可以开始放大图片

最后说两句

控件中还有个监听,那个不用管,那个是为了获取滚动位置来设置标题栏透明度的,跟本文内容无关,所以就不详细说明了。当然这个自定义控件只是为了实现我项目中需求的效果,很简陋,实现方法也很简单,所以欢迎高手前来指点。需要效果图demo的请点击demo github地址,另外同时也欢迎大家吐槽交流(QQ群:123965382)

Authorship: 作者
Article Link: https://raye.wang/2016/06/13/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6%E4%B9%8B%E9%87%8D%E5%86%99ScrollView%E5%AE%9E%E7%8E%B0%E5%9B%BE%E7%89%87%E4%B8%8B%E6%8B%89%E6%94%BE%E5%A4%A7/
Copyright: All posts on this blog are licensed under the CC BY-NC-SA 4.0 license unless otherwise stated. Please cite Raye Blog !