前言:
相信很多安卓开发者都很头痛的一件事就是公司要求做的app和IOS端的风格保持一致,很多IOS端中具有弹性的ScrollView在android开发中不失为一个难题(相应的效果见IOS的微信端等等...)对应的软件可以查看。为了实现这样的效果,毫无疑问的一件事:可以考虑重写我们的ScrollView,然后在监听事件中做相应的处理。开题话说的太多了,直接开始我们的教程把。
正文:
首先贴上我们今天要完成的效果图:
看到这边我想大家应该都清楚今天要完成的是什么了吧。其实这个效果实现起来并不困难,重要的是对touch事件的处理,接下来我会给大家提供两套实现方案。一个是用基本的dispatchTouchEvent和TouchEvent去处理事件,另外一个则是用ViewDragHelper去处理view的事件。说实话,个人还是比较推崇第二种方法的,处理事件比较简单,而且还能做出各种想不到的特效。(Tips:对ViewDragHelper不熟悉的可以去百度,在以后的blog中会推出关于ViewDragHelper的讲解).
首先,我还是从难到易来实现吧,这样你才会体会到ViewDragHelper的方便之处。
Step1:继承ScrollVIew,重写ScrollView
public class FlexibleScrollView extends ScrollView {
}
Step2:重写构造器(3中构造,当然何时需要复写哪种构造请自行百度,常识性问题我觉得还是自己去查才能记住

public FlexibleScrollView(Context context) {
this(context, null);
}
public FlexibleScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlexibleScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
Step3:因为ScrollView是特殊的Viewgroup,在于它只能有一个childView,一般为LinearLayout来组成它的子view,所以我们需要复写onLayout方法,同时获取我们需要操作的子view。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (contentView == null)
return;
// scrollview唯一的一个子view的位置信息,这个位置信息在整个生命周期中保持不变
originalRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
}
这里的originalRect是我们view的正常位置Rect范围,在整个生命周期中都是保持不变的。
我们的子view是在onFinishInflate() 中获取的,至于onFinishInflate() 什么时候加载,嘿嘿准备说百度的,算了,提一下吧:
onFinishInflate当View中所有的子控件均被映射成xml后触发,太深奥了?
比如你 自定义一个view叫myView ,路径是,com.test.view.MyView,此view是继承LinearLayout,定义的布局文件是my_view.xml
里面内容是:
<com.test.view.MyView>
<xxxx />
</com.test.view.MyView>
当你在使用的时候,可以这样使用
MyView mv = (MyView)View.inflate (context,R.layout.my_view,null);
当加载完成xml后,就会执行那个方法。
这段解释也是当时初学安卓的时候百度到的,然后就记下来了。自己太笨了,只能靠记笔记了,当然是在电脑上。
好了,看看代码吧:
/**
* 在加载完xml后获取唯一的一个childview
*/
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
// 获取第一个childview
contentView = getChildAt(0);
}
}
好吧,一切准备工作就绪了,就差我们的事件处理了:
// 在触摸事件中处理上拉和下拉的逻辑
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (contentView == null) {
return super.dispatchTouchEvent(ev);
}
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 判断是否可以上拉或者下拉
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
// 记录按下时的Y值
startY = ev.getY();
break;
case MotionEvent.ACTION_UP:
if (!isMoved)
break; // 如果没有移动布局,则跳过执行
// 开启动画
TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(), originalRect.top);
// 设置动画时间
anim.setDuration(ANIM_TIME);
// 给view设置动画
contentView.setAnimation(anim);
// 设置回到正常的布局位置
contentView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);
// 将标志位重置
canPullDown = false;
canPullUp = false;
isMoved = false;
break;
case MotionEvent.ACTION_MOVE:
// 在移动过程既没有达到上拉的程度,又没有达到下拉的程度
if (!canPullDown && !canPullUp) {
startY = ev.getY();
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
break;
}
// 计算手指移动的距离
float nowY = ev.getY();
int deltaY = (int) (nowY - startY);
// 是否应该移动布局
// 1.可以下拉,并且手指向下移动
// 2.可以上拉,并且手指向上移动
// 3.既可以上拉也可以下拉,当ScrollView包裹的控件比scrollView还小
boolean shouldMove = (canPullDown && deltaY > 0) || (canPullUp && deltaY < 0) || (canPullDown && canPullUp);
if (shouldMove) {
// 计算偏移量
int offset = (int) (deltaY * MOVE_FACTOR);
contentView.layout(originalRect.left, originalRect.top + offset, originalRect.right, originalRect.bottom + offset);
isMoved = true;
}
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 判断是否滚动到顶部
*
* @return
*/
private boolean isCanPullDown() {
return getScrollY() == 0 || contentView.getHeight() < getHeight() + getScrollY();
}
/**
* 判断是否滚动到底部
*/
private boolean isCanPullUp() {
return contentView.getHeight() <= getScrollY() + getHeight();
}
这段代码有点长,分别是判断我们手指按下,手指移动和手指抬起时view的位置变化来重新onLayout。上面都有注解,相信不用花太多时间就能看懂。好了,整体代码我就贴上去吧。
activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.beyole.view.FlexibleScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:layout_width="match_parent"
android:layout_height="200.0dip"
android:scaleType="fitXY"
android:src="@drawable/img1" />
<ImageView
android:layout_width="match_parent"
android:layout_height="200.0dip"
android:scaleType="fitXY"
android:src="@drawable/img2" />
<ImageView
android:layout_width="match_parent"
android:layout_height="200.0dip"
android:scaleType="fitXY"
android:src="@drawable/img3" />
</LinearLayout>
</com.beyole.view.FlexibleScrollView>
</RelativeLayout>
就是简单的调用,和ScrollView一样。
FlexibleScrollView.java:
package com.beyole.view;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
/**
* 有弹性的scrollView
*
* @date 2016/02/22
* @author Iceberg
*
*/
public class FlexibleScrollView extends ScrollView {
private static final String TAG = "FLEXIBLESCROLLVIEW";
// 移动因子,是一个百分比,比如手指移动了100px,那么view只移动50px,目的是达到一个延迟的效果。
private static final float MOVE_FACTOR = 0.5f;
// 手指松开时,界面回到原始位置动画所需的时间
private static final int ANIM_TIME = 300;
// ScrollView唯一的一个子view
private View contentView;
// 手指按下时的Y值,用于计算移动中的移动距离
// 如果按下时不能上拉或者下拉,会在手指移动时更新为当前手指的Y值。
private float startY;
// 用于记录正常的布局位置
private Rect originalRect = new Rect();
// 记录手指按下时是否可以下拉
private boolean canPullDown = false;
// 记录手指按下时是否可以上拉
private boolean canPullUp = false;
// 在手指滑动时的过程中记录是否移动了布局
private boolean isMoved = false;
public FlexibleScrollView(Context context) {
this(context, null);
}
public FlexibleScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlexibleScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 在加载完xml后获取唯一的一个childview
*/
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
// 获取第一个childview
contentView = getChildAt(0);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (contentView == null)
return;
// scrollview唯一的一个子view的位置信息,这个位置信息在整个生命周期中保持不变
originalRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
}
// 在触摸事件中处理上拉和下拉的逻辑
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (contentView == null) {
return super.dispatchTouchEvent(ev);
}
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 判断是否可以上拉或者下拉
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
// 记录按下时的Y值
startY = ev.getY();
break;
case MotionEvent.ACTION_UP:
if (!isMoved)
break; // 如果没有移动布局,则跳过执行
// 开启动画
TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(), originalRect.top);
// 设置动画时间
anim.setDuration(ANIM_TIME);
// 给view设置动画
contentView.setAnimation(anim);
// 设置回到正常的布局位置
contentView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);
// 将标志位重置
canPullDown = false;
canPullUp = false;
isMoved = false;
break;
case MotionEvent.ACTION_MOVE:
// 在移动过程既没有达到上拉的程度,又没有达到下拉的程度
if (!canPullDown && !canPullUp) {
startY = ev.getY();
canPullDown = isCanPullDown();
canPullUp = isCanPullUp();
break;
}
// 计算手指移动的距离
float nowY = ev.getY();
int deltaY = (int) (nowY - startY);
// 是否应该移动布局
// 1.可以下拉,并且手指向下移动
// 2.可以上拉,并且手指向上移动
// 3.既可以上拉也可以下拉,当ScrollView包裹的控件比scrollView还小
boolean shouldMove = (canPullDown && deltaY > 0) || (canPullUp && deltaY < 0) || (canPullDown && canPullUp);
if (shouldMove) {
// 计算偏移量
int offset = (int) (deltaY * MOVE_FACTOR);
contentView.layout(originalRect.left, originalRect.top + offset, originalRect.right, originalRect.bottom + offset);
isMoved = true;
}
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 判断是否滚动到顶部
*
* @return
*/
private boolean isCanPullDown() {
return getScrollY() == 0 || contentView.getHeight() < getHeight() + getScrollY();
}
/**
* 判断是否滚动到底部
*/
private boolean isCanPullUp() {
return contentView.getHeight() <= getScrollY() + getHeight();
}
}
MainActivity.java:
package com.beyole.flexiblescrollview;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
由于篇幅问题,第一种方法到此就结束了,其实整个逻辑不难理解。第二种用ViewDragHelper实现的,请移步:
http://blog.youkuaiyun.com/smarticeberg/article/details/50717963
下载地址:http://download.youkuaiyun.com/detail/smarticeberg/9439380
Github地址:https://github.com/xuejiawei/beyole_FlexibleScrollView,欢迎fork or star
题外话:
android交流群:279031247(广告勿入)
新浪微博:SmartIceberg