推荐
http://android-ultra-ptr.liaohuqiu.net/cn/
因为用的都是 LinearLayout 和 FrameLayout,所以习惯于认为 Parent View 应该包含 Child View。
但这里的 PullLayout 不是。
public class PullLayout extends ViewGroup { public PullLayout(Context context) { super(context); } public PullLayout(Context context, AttributeSet attrs) { super(context, attrs); } public PullLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } // 总偏移量 int offset = 0; View header; View content; @Override protected void onFinishInflate() { // 获取 header 和 content header = getChildAt(0); content = getChildAt(1); super.onFinishInflate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 默认处理 super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); Log.e("result", "onMeasure"); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { layoutHeaderView(); layoutContentView(); Log.e("result", "PullLayout: " + l + " " + t + " " + r + " " + b + " "); } private void layoutHeaderView() { final int left = 0; final int top = offset - header.getMeasuredHeight(); final int right = left + header.getMeasuredWidth(); final int bottom = top + header.getMeasuredHeight(); header.layout(left, top, right, bottom); Log.e("result", "header: " + left + " " + top + " " + right + " " + bottom + " "); } private void layoutContentView() { final int left = 0; final int top = offset; final int right = left + content.getMeasuredWidth(); final int bottom = top + content.getMeasuredHeight(); content.layout(left, top, right, bottom); Log.e("result", "content: " + left + " " + top + " " + right + " " + bottom + " "); } }ContentView 贴着 PullLayout 的左上角,HeaderView 则隐藏在上方,没有被绘制。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="50px"> <ivolianer.pulllayout.PullLayout android:id="@+id/pullLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_red_light"> <TextView android:layout_width="match_parent" android:layout_height="300px" android:background="@android:color/holo_blue_dark" android:gravity="center" android:text="HeaderView" android:textColor="@android:color/white" android:textSize="20dp" /> <TextView android:layout_width="match_parent" android:layout_height="1200px" android:background="@android:color/holo_blue_light" android:gravity="center" android:text="ContentView" android:textColor="@android:color/white" android:textSize="20dp" /> </ivolianer.pulllayout.PullLayout> </FrameLayout>
如果调整下 mScrollY,可以看到 HeaderView 是确实存在的。
PullLayout pullLayout = (PullLayout)findViewById(R.id.pullLayout); pullLayout.setScrollY(-200);
日志:
04-11 11:28:12.601 28676-28676/ivolianer.pulllayout E/result: onMeasure04-11 11:28:12.601 28676-28676/ivolianer.pulllayout E/result: PullLayout: 50 50 1030 1651
04-11 11:28:12.601 28676-28676/ivolianer.pulllayout E/result: header: 0 -300 980 0
04-11 11:28:12.601 28676-28676/ivolianer.pulllayout E/result: content: 0 0 980 1200
float lastY; @Override public boolean dispatchTouchEvent(MotionEvent e) { switch (e.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: // 滑动距离 float dy = e.getY() - lastY; // 新的偏移量 int newOffset = (int) (offset + dy); changeOffset(newOffset); break; case MotionEvent.ACTION_UP: changeOffset(0); break; } lastY = e.getY(); return true; } private void changeOffset(int offset) { this.offset = offset; // 会导致 onLayout 的调用 requestLayout(); }
已经能实现拖拽了,再来优化下。
比如 ContentView 不应被向上拖拽, HeaderView 不该拖拽离开 PullLayout 。
就是给 offset 加个大小的限制。
继续优化。
比如,松手后加上回弹动画。
比如,加上下拉的阻力。
比如,根据下拉距离选择执行加载动画,还是回弹动画。
比如,执行动画的过程,不应该接受到任何事件。
float lastY; @Override public boolean dispatchTouchEvent(MotionEvent e) { // 执行动画过程,屏蔽所有事件 if (animating) { return false; } switch (e.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: // 滑动距离 float dy = e.getY() - lastY; // 阻力 dy = dy / 2; // 新的偏移量 int newOffset = (int) (offset + dy); newOffset = checkOffsetRange(newOffset); changeOffset(newOffset); break; case MotionEvent.ACTION_UP: if (offset > 280) { doYourLoadingAnimation(); } else { clearOffset(); } break; } lastY = e.getY(); return true; } private void changeOffset(int offset) { this.offset = offset; // 会导致 onLayout 的调用 requestLayout(); } private int checkOffsetRange(int newOffset) { newOffset = Math.min(300, newOffset); newOffset = Math.max(0, newOffset); return newOffset; } // boolean animating = false; private void doYourLoadingAnimation() { // 缩放动画 ValueAnimator animator = ValueAnimator.ofFloat(0.9f, 1.1f, 0.9f, 1.1f, 0.9f, 1.1f, 0.9f, 1.1f, 1); animator.setDuration(2000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { float scale = (Float) animator.getAnimatedValue(); header.setScaleX(scale); if (1 == animator.getAnimatedFraction()) { animating = false; clearOffset(); } } }); animator.start(); animating = true; } private void clearOffset() { ValueAnimator animator = ValueAnimator.ofInt(offset, 0); animator.setDuration(300); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { int currentOffset = (Integer) animator.getAnimatedValue(); changeOffset(currentOffset); if (1 == animator.getAnimatedFraction()) { animating = false; } } }); animator.start(); animating = true; }
之前的 Content 是个 TextView ,现在换成 ScrollView。
ScrollView 失去了它一切的特性,不能滚动 ,不能 fling,因为所有的事件都被 PullLayout 消费拦截,没有进一步分发下去。
@Override public boolean dispatchTouchEvent(MotionEvent e) { // 执行动画过程,屏蔽所有事件 if (animating) { return false; } boolean result = true; switch (e.getAction()) { case MotionEvent.ACTION_DOWN: // 把事件分发下去,但始终消费 DOWN 事件 super.dispatchTouchEvent(e); break; case MotionEvent.ACTION_MOVE: // 滑动距离 float dy = e.getY() - lastY; Log.e("result","" + dy); // 阻力 dy = dy / 2; // 最难的地方,谁来处理滑动事件 if (offset > 0 || offset == 0 && dy > 0 && content.getScrollY() == 0) { selfHandleMoveEvent(dy); } else { // 坑,千万不要用 return ,lastY 的每次赋值都很重要... 否则会突然滑动一段距离什么的... result = contentHandleMoveEvent(e); } break; case MotionEvent.ACTION_UP: // 把事件分发下去,但始终消费 UP 事件 super.dispatchTouchEvent(e); if (offset > 280) { doYourLoadingAnimation(); } else { clearOffset(); } break; } lastY = e.getY(); return result; } private void selfHandleMoveEvent(float dy) { int newOffset = (int) (offset + dy); newOffset = checkOffsetRange(newOffset); changeOffset(newOffset); } private boolean contentHandleMoveEvent(MotionEvent e) { return super.dispatchTouchEvent(e); }
难点在在于,何时让 PullLayout 响应移动事件,何时让 Content 响应移动事件。
if (offset > 0 || offset == 0 && dy > 0 && content.getScrollY() == 0) {offset > 0,说明是一个拖拽 Header 的过程,无论向上向下。
offset == 0 && dy > 0 && mContent.getScrollY() == 0offset == 0 header 完全隐藏, dy > 0 下拉拖拽。
mContent.get ScrollY() == 0,滚动条已经滚动到头部了,不需要再消费 MOVE 事件了。