学学下拉刷新

推荐

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>
学学下拉刷新_第1张图片

如果调整下 mScrollY,可以看到 HeaderView 是确实存在的。

     PullLayout pullLayout = (PullLayout)findViewById(R.id.pullLayout);
        pullLayout.setScrollY(-200);
学学下拉刷新_第2张图片

日志:

04-11 11:28:12.601 28676-28676/ivolianer.pulllayout E/result: onMeasure

04-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 



第二步 拖拽和完善

假设暂时由 PullLayout 处理所有的事件。
   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;
    }


第三步 和 ContentView 分享事件

之前的 Content 是个 TextView ,现在换成 ScrollView。

学学下拉刷新_第3张图片

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() == 0
offset == 0 header 完全隐藏, dy > 0 下拉拖拽。

mContent.get ScrollY() == 0,滚动条已经滚动到头部了,不需要再消费 MOVE 事件了。












你可能感兴趣的:(学学下拉刷新)