IOS的UI和用户体验是它的优势, 与IOS相比, Android的UI和用户体验可能要差一些。 虽然Android版本已经到了4.4, 对系统的各个方法进行了大量实质性的优化, 但他的显示效果和交互体验依然不及IOS。 例如IOS上的很多控件都是带弹性的, 也就是拖拽一个控件到了该控件的边界, 但是控件依然可以随着手指的移动而移动一段距离。 这样的话, 给用户的感觉就不那么生硬,能在一定程度上提升用户体验。现在有很多Android App都采用了这中弹性控件,例如最常见的QQ, 多个界面都采用了弹性ScrollView。 我不知道腾讯是如何实现的, 在本文中我会给出自己的实现方式。
我之前的文章 Android上实现仿IOS弹性ListView , 在Android上实现了弹性的ListView, 主要的实现原理是为该ListView增加一个初始高度为0的HeaderView, 如果滚动到第一个条目的位置,用户依然向下拖拽, 那么就增加HeaderView的高度,实现拉伸效果, 当用户松开手指, 就将HeaderView的高度组件递减到0, 实现回弹效果。感兴趣的同学可以看一下实现细节, 文章的链接已经在上面给出。
本文讨论弹性ScrollView的实现。弹性ScrollView实现原理和ListView不同, 因为ScrollView只能有一个子View, 不能为他添加额外的HeaderView。 弹性ScrollView的实现原理是移动这个唯一的子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
* 实现下拉弹回和上拉弹回
* @author zhangjg
* @date Feb 13, 2014 6:11:33 PM
*/
public class ReboundScrollView extends ScrollView {
private static final String TAG = "ElasticScrollView";
//移动因子, 是一个百分比, 比如手指移动了100px, 那么View就只移动50px
//目的是达到一个延迟的效果
private static final float MOVE_FACTOR = 0.5f;
//松开手指后, 界面回到正常位置需要的动画时间
private static final int ANIM_TIME = 300;
//ScrollView的子View, 也是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 ReboundScrollView(Context context) {
super(context);
}
public ReboundScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
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中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变
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);
contentView.startAnimation(anim);
// 设置回到正常的布局位置
contentView.layout(originalRect.left, originalRect.top,
originalRect.right, originalRect.bottom);
//将标志位设回false
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);
//是否应该移动布局
boolean shouldMove =
(canPullDown && deltaY > 0) //可以下拉, 并且手指向下移动
|| (canPullUp && deltaY< 0) //可以上拉, 并且手指向上移动
|| (canPullUp && canPullDown); //既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小)
if(shouldMove){
//计算偏移量
int offset = (int)(deltaY * MOVE_FACTOR);
//随着手指的移动而移动布局
contentView.layout(originalRect.left, originalRect.top + offset,
originalRect.right, originalRect.bottom + offset);
isMoved = true; //记录移动了布局
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 判断是否滚动到顶部
*/
private boolean isCanPullDown() {
return getScrollY() == 0 ||
contentView.getHeight() < getHeight() + getScrollY();
}
/**
* 判断是否滚动到底部
*/
private boolean isCanPullUp() {
return contentView.getHeight() <= getHeight() + getScrollY();
}
}
当contentView.height() = scrollView.height + mScrollY时, 就说明滚动到了底部, 如下图所示:
还有一种情况既可以认为滚动到了底部,也可以认为滚动到顶部, 那就是contentView的高度本身就小于ScrollView的高度, 不需要滑动, 这时满足条件contentView.height() < scrollView.height + mScrollY。 如下图所示:
判断滚动到顶部和滚动到底部,分别由isCanPullDown方法和isCanPullUp方法实现。理解了上面图示的内容,
就可以很容易的理解这两个方法的原理。 这两个方法的代码如下所示:
/**
* 判断是否滚动到顶部
*/
private boolean isCanPullDown() {
return getScrollY() == 0 ||
contentView.getHeight() < getHeight() + getScrollY();
}
/**
* 判断是否滚动到底部
*/
private boolean isCanPullUp() {
return contentView.getHeight() <= getHeight() + getScrollY();
}在用户按下手指时(也就是ACTION_DOWN事件), 调用上面的两个方法判断是否滚动到了顶部或底部, 如果滚动到了顶部或底部, 就说明在移动手指时需要移动contentView的布局。 这时就用标志位canPullDown和canPullUp 记住这两个个状态, 并且也记住ACTION_DOWN发生时, 手指触摸点的Y值, 就是startY成员变量。具体实现代码如下:
case MotionEvent.ACTION_DOWN: //判断是否可以上拉和下拉 canPullDown = isCanPullDown(); canPullUp = isCanPullUp(); //记录按下时的Y值 startY = ev.getY(); 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);
//是否应该移动布局
boolean shouldMove =
(canPullDown && deltaY > 0) //可以下拉, 并且手指向下移动
|| (canPullUp && deltaY< 0) //可以上拉, 并且手指向上移动
|| (canPullUp && canPullDown); //既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小)
if(shouldMove){
//计算偏移量
int offset = (int)(deltaY * MOVE_FACTOR);
//随着手指的移动而移动布局
contentView.layout(originalRect.left, originalRect.top + offset,
originalRect.right, originalRect.bottom + offset);
isMoved = true; //记录移动了布局
}
break; @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中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变
originalRect.set(contentView.getLeft(), contentView.getTop(), contentView
.getRight(), contentView.getBottom());
}case MotionEvent.ACTION_UP: if(!isMoved) break; //如果没有移动布局, 则跳过执行 // 开启动画 TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(), originalRect.top); anim.setDuration(ANIM_TIME); contentView.startAnimation(anim); // 设置回到正常的布局位置 contentView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom); //将标志位设回false canPullDown = false; canPullUp = false; isMoved = false; break;
public class ReboundScrollView extends ScrollView {
private static final int MAX_Y_OVERSCROLL_DISTANCE = 500;
private Context mContext;
private int mMaxYOverscrollDistance;
public ReboundScrollView(Context context){
super(context);
mContext = context;
initBounceScrollView();
}
public ReboundScrollView(Context context, AttributeSet attrs){
super(context, attrs);
mContext = context;
initBounceScrollView();
}
public ReboundScrollView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
mContext = context;
initBounceScrollView();
}
private void initBounceScrollView(){
final DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
final float density = metrics.density;
mMaxYOverscrollDistance = (int) (density * MAX_Y_OVERSCROLL_DISTANCE);
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent){
//这块是关键性代码
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxYOverscrollDistance, isTouchEvent);
}
}
原文:http://blog.csdn.net/zhangjg_blog/article/details/19193671