me.xiaopan.android.widget.PullRefreshLayout.java Source code

Java tutorial

Introduction

Here is the source code for me.xiaopan.android.widget.PullRefreshLayout.java

Source

/*
 * Copyright (C) 2014 Peng fei Pan <sky@xiaopan.me>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package me.xiaopan.android.widget;

import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.Scroller;

/**
 * 
 * @author xiaopan
 * @version 1.0.0 Home https://github.com/xiaopansky/PullRefreshLayout
 */
public class PullRefreshLayout extends ViewGroup implements ScrollManager.Bridge, ScrollManager.ScrollListener {
    private static final String NAME = PullRefreshLayout.class.getSimpleName();
    private static final int INVALID_POINTER = -1;

    private View targetView;
    private View headerView;
    private PullRefreshHeader headerInterface;
    private Class<? extends PullRefreshHeader> pullRefreshHeaderClass;

    private int touchSlop; // ???????
    private int currentOffset; // ????
    private int originalOffset; // ???
    private int activePointerId = INVALID_POINTER;
    private int targetViewHeightDecrement; // TargetView???Target
    private float downMotionY; // Y?
    private float lastMotionY; // Y???Y??
    private float elasticForce = 0.5f; //?
    private boolean mIsBeingDragged; // ?

    private ScrollManager scrollManager; // TargetViewHeaderView
    private OnRefreshListener onRefreshListener; // ?

    private boolean ready; // PullRefreshLayout???
    private boolean waitRefresh; // ?

    public PullRefreshLayout(Context context) {
        this(context, null);
    }

    public PullRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        originalOffset = getPaddingTop();
        scrollManager = new ScrollManager(getContext(), this, this);
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // ????1
        if (getChildCount() < 1) {
            throw new IllegalStateException(NAME + " can be only one directly child");
        }

        // ????2
        if (getChildCount() > 2) {
            throw new IllegalStateException(NAME + " can host only two direct child");
        }

        // ????PullRefreshLayout
        int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        getChildAt(0).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));

        if (getChildCount() < 2) {
            if (pullRefreshHeaderClass != null) {
                try {
                    addView((View) pullRefreshHeaderClass.getConstructor(Context.class).newInstance(getContext()));
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
            } else {
                return;
            }
        }

        // ?HeaderViewRefreshHeader?
        if (!(getChildAt(1) instanceof PullRefreshHeader)) {
            throw new IllegalStateException(NAME + " the second view must implement "
                    + PullRefreshHeader.class.getSimpleName() + " interface");
        }

        // ???UNSPECIFIED
        getChildAt(1).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = getPaddingLeft();
        int childTop;
        int childRight;
        int childBottom;
        View childView;

        // ?
        if (getChildCount() < 1)
            return;
        childView = getChildAt(0);
        childTop = currentOffset;
        childRight = childLeft + childView.getMeasuredWidth();
        childBottom = childTop + childView.getMeasuredHeight() - targetViewHeightDecrement;
        childView.layout(childLeft, childTop, childRight, childBottom);
        targetView = childView;

        // ?
        if (getChildCount() < 2)
            return;
        childView = getChildAt(1);
        childTop = currentOffset - childView.getMeasuredHeight();
        childRight = childLeft + childView.getMeasuredWidth();
        childBottom = childTop + childView.getMeasuredHeight();
        childView.layout(childLeft, childTop, childRight, childBottom);
        headerView = childView;
        headerInterface = (PullRefreshHeader) headerView;

        // ?
        if (!ready) {
            ready = true;
            if (waitRefresh) {
                delayedStartRefresh();
            }
        }
    }

    /**
     * ?
     */
    private void delayedStartRefresh() {
        postDelayed(new Runnable() {
            @Override
            public void run() {
                startRefresh();
            }
        }, getResources().getInteger(android.R.integer.config_mediumAnimTime));
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        // Nope.
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isEnabled() || targetView == null || headerView == null || headerInterface == null
                || scrollManager == null || canChildScrollUp()) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (MotionEventCompat.getActionMasked(ev)) {
        case MotionEvent.ACTION_DOWN:
            downMotionY = lastMotionY = ev.getY();
            activePointerId = MotionEventCompat.getPointerId(ev, 0);
            mIsBeingDragged = false;
            break;

        case MotionEvent.ACTION_MOVE:
            if (activePointerId == INVALID_POINTER) {
                Log.e(NAME, "Got ACTION_MOVE event but don't have an active pointer id.");
                return false;
            }

            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            if (pointerIndex < 0) {
                Log.e(NAME, "Got ACTION_MOVE event but have an invalid active pointer id.");
                return false;
            }

            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = y - downMotionY;
            if (yDiff > touchSlop) {
                lastMotionY = y;
                downMotionY = y;
                mIsBeingDragged = true;
                scrollManager.abort(); // ?
            }
            break;

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            activePointerId = INVALID_POINTER;
            mIsBeingDragged = false;
            break;
        }

        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!isEnabled() || targetView == null || headerView == null || headerInterface == null
                || scrollManager == null || canChildScrollUp()) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (MotionEventCompat.getActionMasked(ev)) {
        case MotionEvent.ACTION_DOWN: {
            downMotionY = ev.getY();
            lastMotionY = ev.getY();
            activePointerId = MotionEventCompat.getPointerId(ev, 0);
            mIsBeingDragged = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            if (pointerIndex < 0) {
                Log.e(NAME, "Got ACTION_MOVE event but have an invalid active pointer id.");
                return false;
            }

            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = y - downMotionY;

            if (!mIsBeingDragged && yDiff > touchSlop) {
                lastMotionY = y;
                downMotionY = y;
                mIsBeingDragged = true;
                scrollManager.abort(); // ?
            }
            int addOffset = (int) ((y - lastMotionY) * elasticForce);
            updateOffset(currentOffset + addOffset, true, true);
            lastMotionY = y;
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN: {
            final int index = MotionEventCompat.getActionIndex(ev);
            lastMotionY = MotionEventCompat.getY(ev, index);
            activePointerId = MotionEventCompat.getPointerId(ev, index);
            break;
        }
        case MotionEventCompat.ACTION_POINTER_UP: {
            onSecondaryPointerUp(ev);
            break;
        }

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            mIsBeingDragged = false;
            activePointerId = INVALID_POINTER;

            // ???
            if (headerInterface != null && headerInterface.getStatus() == PullRefreshHeader.Status.WAIT_REFRESH) {
                startRefresh();
            } else {
                scrollManager.startScroll(-1, true);
            }

            return false;
        }
        }

        return true;
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == activePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            lastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
            activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
    }

    /**
     * @return Whether it is possible for the child view of this layout to
     *         scroll up. Override this if the child view is a custom view.
     */
    private boolean canChildScrollUp() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (targetView instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) targetView;
                return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0
                        || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
            } else {
                return targetView.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(targetView, -1);
        }
    }

    @Override
    public int getCurrentOffset() {
        return currentOffset;
    }

    @Override
    public int getOriginalOffset() {
        return originalOffset;
    }

    @Override
    public void setOriginalOffset(int newOriginalOffset) {
        this.originalOffset = newOriginalOffset;
    }

    @Override
    public void setTargetViewHeightDecrement(int newTargetViewHeightDecrement) {
        this.targetViewHeightDecrement = newTargetViewHeightDecrement;
    }

    /**
     * ???
     * @param newOffset ?
     * @param preventOverOriginalOffset true??false?
     * @param manualSliding ?????????
     */
    @Override
    public void updateOffset(int newOffset, boolean preventOverOriginalOffset, boolean manualSliding) {
        // ??
        if (preventOverOriginalOffset) {
            if (newOffset < originalOffset) {
                newOffset = originalOffset;
            }
        } else {
            if (newOffset > originalOffset) {
                newOffset = originalOffset;
            }
        }

        // ???
        currentOffset = newOffset;
        requestLayout();

        // ???HeaderView??
        // ??????
        if (headerInterface != null && onRefreshListener != null) {
            // HeaderView?
            int distance = Math.abs(currentOffset - getPaddingTop());
            headerInterface.onScroll(distance);

            // ??????
            if (headerInterface.getStatus() != PullRefreshHeader.Status.REFRESHING && manualSliding) {
                if (distance >= headerInterface.getTriggerHeight()) {
                    headerInterface.setStatus(PullRefreshHeader.Status.WAIT_REFRESH);
                    headerInterface.onToWaitRefresh();
                } else {
                    headerInterface.setStatus(PullRefreshHeader.Status.NORMAL);
                    headerInterface.onToNormal();
                }
            }
        }
    }

    @Override
    public View getView() {
        return this;
    }

    /**
     * Class??Class
     */
    public void setPullRefreshHeaderClass(Class<? extends PullRefreshHeader> pullRefreshHeaderClass) {
        this.pullRefreshHeaderClass = pullRefreshHeaderClass;
    }

    /**
     * ?
     */
    public void setAnimationDuration(int animationDuration) {
        scrollManager.setAnimationDuration(animationDuration);
    }

    /**
     * ?
     */
    public void setOnRefreshListener(OnRefreshListener listener) {
        onRefreshListener = listener;
    }

    /**
     * 
     * @param elasticForce ?[0.0f-1.0f]??0.5f
     */
    public void setElasticForce(float elasticForce) {
        this.elasticForce = elasticForce;
    }

    /**
     * ?
     */
    public boolean isRefreshing() {
        return headerInterface != null && headerInterface.getStatus() == PullRefreshHeader.Status.REFRESHING;
    }

    /**
     * 
     * @return true?false???
     */
    public boolean startRefresh() {
        if (!ready) {
            waitRefresh = true;
            return true;
        }

        if (headerInterface == null || onRefreshListener == null
                || headerInterface.getStatus() == PullRefreshHeader.Status.REFRESHING) {
            return false;
        }

        headerInterface.setStatus(PullRefreshHeader.Status.WAIT_REFRESH);
        scrollManager.startScroll(getPaddingTop() + headerInterface.getTriggerHeight(), true);

        return true;
    }

    /**
     * ?
     * @return true?false?
     */
    public boolean stopRefresh() {
        if (headerInterface == null || headerInterface.getStatus() != PullRefreshHeader.Status.REFRESHING) {
            return false;
        }

        headerInterface.setStatus(PullRefreshHeader.Status.NORMAL);
        headerInterface.onToNormal();
        scrollManager.startScroll(getPaddingTop(), false);

        return true;
    }

    @Override
    public void onScrollEnd() {
        if (headerInterface == null) {
            return;
        }

        if (headerInterface.getStatus() == PullRefreshHeader.Status.WAIT_REFRESH) {
            headerInterface.setStatus(PullRefreshHeader.Status.REFRESHING);
            headerInterface.onToRefreshing();
            onRefreshListener.onRefresh();
        }
    }

    public interface OnRefreshListener {
        public void onRefresh();
    }

    public interface PullRefreshHeader {
        public void onScroll(int distance);

        public void onToRefreshing();

        public void onToNormal();

        public void onToWaitRefresh();

        public int getTriggerHeight();

        public Status getStatus();

        public void setStatus(Status status);

        public enum Status {
            /**
             * 
             */
            NORMAL,

            /**
             * 
             */
            WAIT_REFRESH,

            /**
             * 
             */
            REFRESHING,
        }
    }
}

class ScrollManager implements Runnable {
    private int animationDuration;
    private Bridge bridge;
    private Scroller scroller;
    private ScrollListener scrollListener;
    private boolean rollback;
    private boolean running;

    public ScrollManager(Context context, Bridge bridge, ScrollListener scrollListener) {
        this.animationDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime);
        this.bridge = bridge;
        this.scrollListener = scrollListener;
        this.scroller = new Scroller(context, new DecelerateInterpolator());
    }

    /**
     * 
     * @param newOriginalOffset ???
     * @param diminish ??TargetView?????????????
     */
    public boolean startScroll(int newOriginalOffset, boolean diminish) {
        abort();

        int startX = bridge.getCurrentOffset();
        int endX = bridge.getOriginalOffset();
        int startY = 0;
        int endY = 0;
        if (newOriginalOffset != bridge.getOriginalOffset() && newOriginalOffset >= 0) {
            if (diminish) {
                startY = 0;
                endY = Math.abs(newOriginalOffset - bridge.getOriginalOffset());
            } else {
                startY = Math.abs(newOriginalOffset - bridge.getOriginalOffset());
                endY = 0;
            }
            bridge.setOriginalOffset(newOriginalOffset);
            endX = newOriginalOffset;
        }

        running = true;
        rollback = startX > endX;
        scroller.startScroll(startX, startY, endX - startX, endY - startY, animationDuration);
        bridge.getView().post(this);

        return true;
    }

    @Override
    public void run() {
        if (!running) {
            return;
        }

        // ?
        if (!scroller.computeScrollOffset()) {
            running = false;
            if (scrollListener != null) {
                scrollListener.onScrollEnd();
            }
            return;
        }

        // 
        bridge.setTargetViewHeightDecrement(scroller.getCurrY());
        bridge.updateOffset(scroller.getCurrX(), rollback, false);

        bridge.getView().post(this);
    }

    public void abort() {
        if (!scroller.isFinished()) {
            scroller.abortAnimation();
        }
        bridge.getView().removeCallbacks(this);
        running = false;
    }

    public void setAnimationDuration(int animationDuration) {
        this.animationDuration = animationDuration;
    }

    public interface Bridge {
        public int getCurrentOffset();

        public int getOriginalOffset();

        public void setOriginalOffset(int newOriginalOffset);

        public void setTargetViewHeightDecrement(int newTargetViewHeightDecrement);

        public void updateOffset(int newCurrentOffset, boolean rollback, boolean callbackHeader);

        public View getView();
    }

    public interface ScrollListener {
        public void onScrollEnd();
    }
}