下拉刷新Phoenix分析

Phoenix还不知道是什么样的效果的朋友可以先去看我的另一篇文章
比pullTorefresh更好看的下拉刷新
,在本文将要讲解其实现。。。。拖了好久

思路整理

首先我们可以先不去看源码,看下它的使用方法

<com.hankkin.AnimationPullToRefreshDemo.PullToRefreshView
        android:id="@+id/pull_to_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/list_view"
            android:divider="@null"
            android:dividerHeight="0dp"
            android:fadingEdge="none"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </com.hankkin.AnimationPullToRefreshDemo.PullToRefreshView>

由此我们可以知道,在这个下拉刷新并不是重写了listview,而是在listview的外面套了一层布局,也就是说listview被添加到了Phoenix上,那么我们就能知道Phoenix其实就是一个viewgroup,到这里就差不多知道要重写那几个方法了,自定义viewgroup的话也就onlayout、onmeasure、ontouchevent(如果是自定义view的话一般就重写onmeasure、ondraw、ontouchevent),整理到这里我们就可以来看看那源码了。

源码分析

  1. 构造方法
  2. onmeasure
  3. onlayout
  4. 头部动画效果实现

构造方法

首先看他的构造方法

public PullToRefreshView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
        final int type = a.getInteger(R.styleable.RefreshView_type, STYLE_SUN);
        a.recycle();

        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mTotalDragDistance = Utils.convertDpToPixel(context, DRAG_MAX_DISTANCE);

        mRefreshView = new ImageView(context);

        setRefreshStyle(type);

        addView(mRefreshView);
        //保证ondraw会执行,如果是true的话ondraw不会执行
        setWillNotDraw(false);
        ViewCompat.setChildrenDrawingOrderEnabled(this, true);
    }

    public void setRefreshStyle(int type) {
        setRefreshing(false);
        switch (type) {
            case STYLE_SUN:
                mBaseRefreshView = new SunRefreshView(getContext(), this);
                break;
            default:
                throw new InvalidParameterException("Type does not exist");
        }
        mRefreshView.setImageDrawable(mBaseRefreshView);
    }

在这里setRefreshStyle其实就可以直接看成是给头部的imageview设置显示的内容,然后将这个imageview添加到viewgroup中,另外的就是一写参数的初始化。简单的说就是在这个已经包了一个listview的viewgroup中再添加一个imageview。

onMeasure

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

        ensureTarget();
        if (mTarget == null)
            return;

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingRight() - getPaddingLeft(), MeasureSpec.EXACTLY);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
        mTarget.measure(widthMeasureSpec, heightMeasureSpec);
        mRefreshView.measure(widthMeasureSpec, heightMeasureSpec);
    }

    private void ensureTarget() {
        if (mTarget != null)
            return;
        if (getChildCount() > 0) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (child != mRefreshView) {
                    mTarget = child;
                    mTargetPaddingBottom = mTarget.getPaddingBottom();
                    mTargetPaddingLeft = mTarget.getPaddingLeft();
                    mTargetPaddingRight = mTarget.getPaddingRight();
                    mTargetPaddingTop = mTarget.getPaddingTop();
                }
            }
        }
    }

一开始mTarget 是空的,然后到getChildCount方法,想一下这个时候这个viewgroup中也就两个孩子,一个imageview,一个listview,ensureTarget的作用就是把listview的实例赋值给mTarget,以及给几个padding赋值,随后在onmeasure中设置imageview和listview与外层的viewgroup一样大小。

onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        ensureTarget();
        if (mTarget == null)
            return;

        int height = getMeasuredHeight();
        int width = getMeasuredWidth();
        int left = getPaddingLeft();
        int top = getPaddingTop();
        int right = getPaddingRight();
        int bottom = getPaddingBottom();

        mTarget.layout(left, top + mCurrentOffsetTop, left + width - right, top + height - bottom + mCurrentOffsetTop);
        mRefreshView.layout(left, top, left + width - right, top + height - bottom);
    }

imageview和listview放在相同的位置。

ontouchevent和onInterceptTouchEvent

如果不知道上面两个方法的关系,可以去看看我写的另一篇文章(http://blog.csdn.net/u012806692/article/details/50820070),首先重写的是拦截的方法

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        if (!isEnabled() || canChildScrollUp() || mRefreshing) {
            return false;
        }

        final int action = MotionEventCompat.getActionMasked(ev);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                setTargetOffsetTop(0, true);
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                final float initialMotionY = getMotionEventY(ev, mActivePointerId);
                if (initialMotionY == -1) {
                    return false;
                }
                mInitialMotionY = initialMotionY;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    return false;
                }
                final float y = getMotionEventY(ev, mActivePointerId);
                if (y == -1) {
                    return false;
                }
                final float yDiff = y - mInitialMotionY;
                if (yDiff > mTouchSlop && !mIsBeingDragged) {
                    mIsBeingDragged = true;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }

        return mIsBeingDragged;
    }

如果listview没有滑到最顶部或者还在加载刷新中就不执行之后的代码,直接返回false,否则记录按下位置的y坐标,注意这里还有多点触控的知识,这里不理解可以先不用管。到了action_move之后,如果开始滑动(也就是大于mTouchSlop )就拦截touch事件不传递给子view,直接执行自己的ontouchevent方法,这里我们先不管多点触控相关的直接简单理解下,接下来看ontouchevent事件
@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {

    if (!mIsBeingDragged) {
        return super.onTouchEvent(ev);
    }

    final int action = MotionEventCompat.getActionMasked(ev);

    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }

            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = y - mInitialMotionY;
            final float scrollTop = yDiff * DRAG_RATE;
            mCurrentDragPercent = scrollTop / mTotalDragDistance;
            if (mCurrentDragPercent < 0) {
                return false;
            }
            float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent));
            float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
            float slingshotDist = mTotalDragDistance;
            float tensionSlingshotPercent = Math.max(0,
                    Math.min(extraOS, slingshotDist * 2) / slingshotDist);
            float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
                    (tensionSlingshotPercent / 4), 2)) * 2f;
            float extraMove = (slingshotDist) * tensionPercent / 2;
            int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove);

            mBaseRefreshView.setPercent(mCurrentDragPercent, true);
            setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN:
            final int index = MotionEventCompat.getActionIndex(ev);
            mActivePointerId = MotionEventCompat.getPointerId(ev, index);
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            if (mActivePointerId == INVALID_POINTER) {
                return false;
            }
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE;
            mIsBeingDragged = false;
            if (overScrollTop > mTotalDragDistance) {
                setRefreshing(true, true);
            } else {
                mRefreshing = false;
                animateOffsetToStartPosition();
            }
            mActivePointerId = INVALID_POINTER;
            return false;
        }
    }

    return true;
}

代码比较多一步步看,上面我们已经执行到了move,在ontouchevent的move中计算了滑动的百分比,头部有个默认的最大值,看目前的滑动举例是他的百分之几,滑动距离超过最大值时取100%,在case action_move中的代码看着挺多,最重要的就最后的几句,设置头部的显示百分比,还有listview的偏移(相当于magin),随后主要看action_up,在抬起中判断滑动距离是否到了加载的那个指定距离,如果足够了就加载,不够就直接回到初始位置,大致流程就是这样。接下来看看imageview中的内容,就是一个

头部动画效果

其实就是imageview中的内容,一开始其实我们留下了一个问题,回想一下,在onlayout和onmeasure中我们设置的imageview的大小和显示位置和listview的是一样的,那么两个不就叠在一起了吗?接下来看下imageview的内容是什么就明白了。它的构造方法就不看了,就一堆变量的赋值,加载图片等等,直接看自定义view最重要的draw方法

    @Override
    public void draw(Canvas canvas) {
        if (mScreenWidth <= 0) return;

        final int saveCount = canvas.save();

        canvas.translate(0, mTop);
        canvas.clipRect(0, -mTop, mScreenWidth, mParent.getTotalDragDistance());

        drawSky(canvas);
        drawSun(canvas);
        drawTown(canvas);

        canvas.restoreToCount(saveCount);
    }

其中将画布移动到了mtop的位置,再看之前初始化的时候将其赋值为mTop = -mParent.getTotalDragDistance();,
canvas.clipRect(0, -mTop, mScreenWidth, mParent.getTotalDragDistance());
这里一开始是截取了一个高度为0的矩形,随着move慢慢变大,后面的draw就只能在这上面操作。重要的就讲完了,其他的包括怎么根据百分比来改变属性,这些其实可以自己发挥实现自己的效果。

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