引言
一切的自定义都是来自于需求,而在项目开发中由于界面条目太多,所以自然而然的使用到了ScrollerView,当把效果给产品经理的时候呢,ios和Android的效果完全不一样,ios自带的上下拉伸回弹的效果,而Android没有,所以自定义一个具有拉伸效果的ScrollerView迫在眉睫啊.
首先来看一下效果图,妹子很漂亮,但是注意重点!!!
好了,下面进入正题,通过继承ScrollerView来进行相关滑动回弹的效果实现,先来定义几个变量:
private View childView;// 子View(ScrollerView的唯一子类)
private int y;// 点击时y坐标
private Rect rect = new Rect();// 矩形(用来保存inner的初始状态,判断是够需要动画回弹效果)
注释打的也很清楚,然后我们先在该ScrollerView的xml布局加载完成后获取ScrollerView的唯一子布局赋值给上面定义的childView:
/**
* 在xml布局绘制为界面完成时调用,
* 获取ScrollerView中唯一的直系子布局(ScrollerView中不许包含一层ViewGroup,有且只有一个)
*/
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
childView = getChildAt(0);
}
super.onFinishInflate();
}
下面就是处理Touch事件了:
/**
* touch 事件处理
**/
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (childView != null) {
handleTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
/***
* 触摸事件
*
* @param ev
*/
public void handleTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
y = (int) ev.getY();//按下的时候获取到y坐标
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int) ev.getY(); // 移动时的实时y坐标
int delayY = y - nowY; // 移动时的间隔
y = nowY; // 将本次移动结束时的y坐标赋值给下次移动的起始坐标(也就是nowY)
if (isNeedMove()) {
if (rect.isEmpty()) {
//rect保存childView的初始位置信息
rect.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
}
//移动布局(屏幕移动的距离等于手指滑动距离的一般)
childView.layout(childView.getLeft(), childView.getTop() - delayY / 2, childView.getRight(), childView.getBottom() - delayY / 2);
}
break;
case MotionEvent.ACTION_UP:
if (isNeedAnimation()) {// 判断rect是否为空,也就是是否被重置了
startAnim();//开始回弹动画
}
break;
default:
break;
}
}
对于Touch事件的处理,我注释说的应该很清楚,但是里面有需要调用的四个方法:
/**
* 判断布局是否需要移动
* @return
*/
private boolean isNeedMove() {
int offset = childView.getMeasuredHeight() - getHeight();
int scrollY = getScrollY();
// 0是顶部,后面那个是底部(需要仔细想一下这个过程)
if (scrollY == 0 || scrollY == offset) {
return true;
}
return false;
}
其中childView.getMeasuredHeight()为获取到该布局的实际高度,getHeight是该布局在屏幕中显示的高度,getScrollY()是滑动的时候相对于起始位置的距离。
在加载布局的时候rect进行了初始化,当确定需要滑动时,再判断一下rect是否为空,因为该rect在布局执行动画回弹之后就会被置空,如果当Scroller顶部对其或者底部对其,未在回弹过程就会将该时刻Scroller的位置信息传入到rect,方便回弹的时候根据rect保存的scrollerview的位置信息完成回弹作用。
public boolean isNeedAnimation() {
return !rect.isEmpty();
}
private void startAnim() {
TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop(), rect.top);
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator());//加速器
childView.startAnimation(anim);
// 将inner布局重新回到起始位置
childView.layout(rect.left, rect.top, rect.right, rect.bottom);
rect.setEmpty();
}
在执行完动画回弹后即ScrollerView回归了原始状态,于是rect也就置空,方便下一次继续记录,好了,下面直接贴一份完整代码吧:
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
/**
* Author : luweicheng on 2017/7/20 0020 15:15
* E-mail :[email protected]
* GitHub : https://github.com/luweicheng24
* funcation: 具有拉伸效果的ScrollerView
*/
public class CustomScroller extends ScrollView {
private View childView;// 子View(ScrollerView的唯一子类)
private int y;// 点击时y坐标
private Rect rect = new Rect();// 矩形(用来保存inner的初始状态,判断是够需要动画回弹效果)
public CustomScroller(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 在xml布局绘制为界面完成时调用,
* 获取ScrollerView中唯一的直系子布局(ScrollerView中不许包含一层ViewGroup,有且只有一个)
*/
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
childView = getChildAt(0);
}
super.onFinishInflate();
}
/**
* touch 事件处理
**/
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (childView != null) {
handleTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
/***
* 触摸事件
*
* @param ev
*/
public void handleTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
y = (int) ev.getY();//按下的时候获取到y坐标
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int) ev.getY(); // 移动时的实时y坐标
int delayY = y - nowY; // 移动时的间隔
y = nowY; // 将本次移动结束时的y坐标赋值给下次移动的起始坐标(也就是nowY)
if (isNeedMove()) {
if (rect.isEmpty()) {
//rect保存childView的初始位置信息
rect.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
}
//移动布局(屏幕移动的距离等于手指滑动距离的一般)
childView.layout(childView.getLeft(), childView.getTop() - delayY / 2, childView.getRight(), childView.getBottom() - delayY / 2);
}
break;
case MotionEvent.ACTION_UP:
if (isNeedAnimation()) {// 判断rect是否为空,也就是是否被重置了
startAnim();//开始回弹动画
}
break;
default:
break;
}
}
private void startAnim() {
TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop(), rect.top);
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator());//加速器
childView.startAnimation(anim);
// 将inner布局重新回到起始位置
childView.layout(rect.left, rect.top, rect.right, rect.bottom);
rect.setEmpty();
}
/**
* 判断布局是否需要移动
* @return
*/
private boolean isNeedMove() {
int offset = childView.getMeasuredHeight() - getHeight();
int scrollY = getScrollY();
// 0是顶部,后面那个是底部(需要仔细想一下这个过程)
if (scrollY == 0 || scrollY == offset) {
return true;
}
return false;
}
public boolean isNeedAnimation() {
return !rect.isEmpty();
}
}
如果有问题,在下面留言,共同探讨。