Android弹性ScrollView
开袋即食
1 import android.content.Context; 2 import android.graphics.Rect; 3 import android.util.AttributeSet; 4 import android.view.MotionEvent; 5 import android.view.View; 6 import android.view.animation.TranslateAnimation; 7 import android.widget.ScrollView; 8 9 /** 10 * 有弹性的ScrollView 11 * 实现下拉弹回和上拉弹回 12 * 13 * @author zhangjg 14 * @date Feb 13, 2014 6:11:33 PM 15 */ 16 public class ReboundScrollView extends ScrollView { 17 18 private static final String TAG = "ElasticScrollView"; 19 20 //移动因子, 是一个百分比, 比如手指移动了100px, 那么View就只移动50px 21 //目的是达到一个延迟的效果 22 private static final float MOVE_FACTOR = 0.5f; 23 24 //松开手指后, 界面回到正常位置需要的动画时间 25 private static final int ANIM_TIME = 300; 26 27 //ScrollView的子View, 也是ScrollView的唯一一个子View 28 private View contentView; 29 30 //手指按下时的Y值, 用于在移动时计算移动距离 31 //如果按下时不能上拉和下拉, 会在手指移动时更新为当前手指的Y值 32 private float startY; 33 34 //用于记录正常的布局位置 35 private Rect originalRect = new Rect(); 36 37 //手指按下时记录是否可以继续下拉 38 private boolean canPullDown = false; 39 40 //手指按下时记录是否可以继续上拉 41 private boolean canPullUp = false; 42 43 //在手指滑动的过程中记录是否移动了布局 44 private boolean isMoved = false; 45 46 public ReboundScrollView(Context context) { 47 super(context); 48 } 49 50 public ReboundScrollView(Context context, AttributeSet attrs) { 51 super(context, attrs); 52 } 53 54 @Override 55 protected void onFinishInflate() { 56 if (getChildCount() > 0) { 57 contentView = getChildAt(0); 58 } 59 } 60 61 @Override 62 protected void onLayout(boolean changed, int l, int t, int r, int b) { 63 super.onLayout(changed, l, t, r, b); 64 65 if (contentView == null) return; 66 67 //ScrollView中的唯一子控件的位置信息, 这个位置信息在整个控件的生命周期中保持不变 68 originalRect.set(contentView.getLeft(), contentView.getTop(), contentView 69 .getRight(), contentView.getBottom()); 70 } 71 72 /** 73 * 在触摸事件中, 处理上拉和下拉的逻辑 74 */ 75 @Override 76 public boolean dispatchTouchEvent(MotionEvent ev) { 77 78 if (contentView == null) { 79 return super.dispatchTouchEvent(ev); 80 } 81 82 int action = ev.getAction(); 83 84 switch (action) { 85 case MotionEvent.ACTION_DOWN: 86 87 //判断是否可以上拉和下拉 88 canPullDown = isCanPullDown(); 89 canPullUp = isCanPullUp(); 90 91 //记录按下时的Y值 92 startY = ev.getY(); 93 break; 94 95 case MotionEvent.ACTION_UP: 96 97 if (!isMoved) break; //如果没有移动布局, 则跳过执行 98 99 // 开启动画 100 TranslateAnimation anim = new TranslateAnimation(0, 0, contentView.getTop(), 101 originalRect.top); 102 anim.setDuration(ANIM_TIME); 103 104 contentView.startAnimation(anim); 105 106 // 设置回到正常的布局位置 107 contentView.layout(originalRect.left, originalRect.top, 108 originalRect.right, originalRect.bottom); 109 110 //将标志位设回false 111 canPullDown = false; 112 canPullUp = false; 113 isMoved = false; 114 115 break; 116 case MotionEvent.ACTION_MOVE: 117 118 //在移动的过程中, 既没有滚动到可以上拉的程度, 也没有滚动到可以下拉的程度 119 if (!canPullDown && !canPullUp) { 120 startY = ev.getY(); 121 canPullDown = isCanPullDown(); 122 canPullUp = isCanPullUp(); 123 124 break; 125 } 126 127 //计算手指移动的距离 128 float nowY = ev.getY(); 129 int deltaY = (int) (nowY - startY); 130 131 //是否应该移动布局 132 boolean shouldMove = 133 (canPullDown && deltaY > 0) //可以下拉, 并且手指向下移动 134 || (canPullUp && deltaY < 0) //可以上拉, 并且手指向上移动 135 || (canPullUp && canPullDown); //既可以上拉也可以下拉(这种情况出现在ScrollView包裹的控件比ScrollView还小) 136 137 if (shouldMove) { 138 //计算偏移量 139 int offset = (int) (deltaY * MOVE_FACTOR); 140 141 //随着手指的移动而移动布局 142 contentView.layout(originalRect.left, originalRect.top + offset, 143 originalRect.right, originalRect.bottom + offset); 144 145 isMoved = true; //记录移动了布局 146 } 147 148 break; 149 default: 150 break; 151 } 152 153 return super.dispatchTouchEvent(ev); 154 } 155 156 157 /** 158 * 判断是否滚动到顶部 159 */ 160 private boolean isCanPullDown() { 161 return getScrollY() == 0 || 162 contentView.getHeight() < getHeight() + getScrollY(); 163 } 164 165 /** 166 * 判断是否滚动到底部 167 */ 168 private boolean isCanPullUp() { 169 return contentView.getHeight() <= getHeight() + getScrollY(); 170 } 171 172 }