高性能的给RecyclerView添加下拉刷新和加载更多动画,基于drawable(二)

项目已经上传github,点击这里查看

先看看效果




动画很粗糙,请不要在意。



项目是基于我改进的一个RecyclerView.Adapter,这个adapter可以给RecyclerView添加header和footer,关于这个adapter,可以点击查看

实现的逻辑是,给RecyclerView各添加一个自定义View作为Header和Footer,自定义的view作为Drawable的Drawable.callback对象,这样drawable不断改变自身

实现动画效果。

动画实现了,那么刷新时列表时列表怎么向下移动呢?这需要说说RecyclerView和LayoutManager的关系。

如果你不熟悉RecyclerView和LayoutManager的关系,你可以阅读国外大神dave smith的博文

http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/

http://wiresareobsolete.com/2014/09/recyclerview-layoutmanager-2/

http://wiresareobsolete.com/2015/02/recyclerview-layoutmanager-3/


如果你不读,那没关系,我告诉你,RecyclerView的layout是被LayoutManager代理了,另外你在滑动列表的时候,RecyclerView的layout过程是不会走的,这可以提高性能。但是如果我们想让RecyclerView重新计算child的大小,那么需要调用notifyDataChanged方法了。


现在我们的思路是,随着手指滑动,我们改变HeaderView的高度,调用notifyDataSetChanged,这样就能使列表向下滑动。


先看看自定义的View


public class DrawableView extends View {
    private Drawable mDrawable;
    private int mHeight = 1;
    public DrawableView(Context context) {
        super(context);
    }

    public void setHeight(int height){
        if (mHeight == height)return;
        if (height == 0){
            height = 1;
        }
        mHeight = height;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mHeight,MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
    }

    public void setDrawable(Drawable drawable){
        mDrawable = drawable;
        mDrawable.setCallback(this);
    }

    int getCurrentHeight(){
        return mHeight;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDrawable == null) {
            return; // couldn't resolve the URI
        }
        if (mDrawable.getIntrinsicWidth() <= 0 || mDrawable.getIntrinsicHeight() <= 0) {
            return;     // nothing to draw (empty bounds)
        }
        canvas.clipRect(getPaddingLeft(),  getPaddingTop(),
                getRight() - getLeft() - getPaddingRight(), getBottom() - getTop() - getPaddingBottom());
        int saveCount = canvas.save();
        mDrawable.draw(canvas);
        canvas.restoreToCount(saveCount);
    }

    @Override
    public void invalidateDrawable(Drawable dr) {
        if (dr == mDrawable) {
            invalidate();
        } else {
            super.invalidateDrawable(dr);
        }
    }

}


我们重写了onMeasure方法,让view的高度是mHeight的值。setDrawable方法中把view自身设为Drawable对象的Callback对象。


public abstract class AdvancedDrawable extends Drawable implements Animatable {
    int dWidth;
    int dHeight;
    float mPercent;
    private RecyclerView.Adapter mAdapter;
    public static final float CRITICAL_PERCENT = 0.8f;

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {

    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public int getIntrinsicWidth() {
        return dWidth;
    }

    @Override
    public int getIntrinsicHeight() {
        return dHeight;
    }

    public void setPercent(float percent,boolean invalidate){
        mPercent = percent;
        if (mAdapter != null && invalidate) mAdapter.notifyDataSetChanged();
    }

    public float getPercent(){
        return mPercent;
    }

    public void setAdapter(RecyclerView.Adapter adapter){
        mAdapter = adapter;
    }

    /**
     * u have to initial ur bitmaps u needed , dWidth and dHeight here
     *
     * @param context context
     */
    protected abstract void init(Context context);
}

这是我们需要用到的Drawable对象,其中的percent和我们说的前景实现一样,是描述刷新(加载)完成度的一个度量值。setPercent中调用adapter的notifyDataSetChanged。里面的dWidth和dHeight必须被设置,否则drawable无法绘制。



这里面要说一下为什么把最小的高度值设为0而不是1,是因为我发现一个bug,如果高度设为0 ,那么下面这个方法会工作不正常。这个bug导致和LinearLayouManagert的逻辑和每16ms绘制一次界面的机制有关系。有兴趣的可以查看下android源码,我并没有进一步测试。

private boolean canChildScrollBottom() {
        return ViewCompat.canScrollVertically(this, 1);
    }
所以我重写了这个方法。

private boolean canChildScrollBottom(){
        return !showLoadFlag && !isLastChildShowingCompletely();
    }

    private boolean isLastChildShowingCompletely(){
        return ((getLayoutManager().getPosition(getChildAt(getChildCount() - 2)) == getAdapter().getItemCount() - 2));
    }




public abstract class RefreshLoadWrapper extends HeaderAndFooterWrapper {
    private final static int REFRESH_TYPE = 199999;
    private final static int LOAD_TYPE = 199998;
    private boolean canRefresh = false;
    private boolean canLoad = false;
    private DrawableView mRefresh;
    private DrawableView mLoad;

    public RefreshLoadWrapper(Context context) {
        super(context);
    }

    private void addRefreshImage(Context c){
        if (canRefresh){
            deleteHeader(0,false);
        }
        mRefresh = new DrawableView(c);
        addHeader(0,REFRESH_TYPE);
        canRefresh = true;
    }

    public void setRefreshDrawable(Context context,AdvancedDrawable drawable){
        addRefreshImage(context);
        mRefresh.setDrawable(drawable);
    }

    public void setLoadDrawable(Context context,AdvancedDrawable drawable){
        addLoadImage(context);
        mLoad.setDrawable(drawable);
    }

    private void addLoadImage(Context c){
        if (canLoad){
            deleteFooter(getFooterViewCount() - 1,false);
        }
        mLoad = new DrawableView(c);
        addFooter(0,LOAD_TYPE);
        canLoad = true;
    }

    public void setRefreshHeight(int height){
        mRefresh.setHeight(height);
    }
    public void setLoadHeight(int height){
        mLoad.setHeight(height);
    }

    public abstract RecyclerView.ViewHolder onCreateHeaderVH(ViewGroup parent, int viewType);
    public abstract RecyclerView.ViewHolder onCreateFooterVH(ViewGroup parent, int viewType);
    public abstract RecyclerView.ViewHolder onCreateGeneralVH(ViewGroup parent, int viewType);
    public abstract void onBindHeaderVH(RecyclerView.ViewHolder holder, int position);
    public abstract void onBindFooterVH(RecyclerView.ViewHolder holder, int position);
    public abstract void onBindGeneralVH(RecyclerView.ViewHolder holder, int position);

    @Override
    public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType) {
        if (viewType == REFRESH_TYPE){
            return new MyViewHolder(mRefresh);
        }
        return onCreateHeaderVH(parent, viewType);
    }

    @Override
    public RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType) {
        if (viewType == LOAD_TYPE){
            return new MyViewHolder(mLoad);
        }
        return onCreateFooterVH(parent, viewType);
    }

    @Override
    public RecyclerView.ViewHolder onCreateGeneralViewHolder(ViewGroup parent, int viewType) {
        return onCreateGeneralVH(parent,viewType);
    }

    @Override
    public void onBindHeaderViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (!(position == 0 && canRefresh)) {
            onBindHeaderVH(holder, position);
        }
    }

    @Override
    public void onBindFooterViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (!(position == getFooterViewCount() - 1 && canLoad)){
            onBindFooterVH(holder,position);
        }
    }

    @Override
    public void onBindGeneralViewHolder(RecyclerView.ViewHolder holder, int position) {
        onBindGeneralVH(holder, position);
    }
}

这个是Adapter实现了setDrawable的时候先添加一个View为Header,这个View也是drawable的载体。


@Override
    public void setAdapter(Adapter adapter) {
        super.setAdapter(adapter);
        if (adapter instanceof RefreshLoadWrapper){
            Log.d(TAG,"adapter kind of RefreshLoadWrapper");
            expectedAdapter = true;
            ((RefreshLoadWrapper) adapter).setRefreshDrawable(getContext(),mRefreshDrawable);
            ((RefreshLoadWrapper) adapter).setLoadDrawable(getContext(),mLoadDrawable);
            mRefreshDrawable.setAdapter(adapter);
            mLoadDrawable.setAdapter(adapter);
        }else {
            expectedAdapter = false;
        }
    }

这是自定义的RecyclerView,重写了setAdatper,如果设置的adapter是支持刷新和加载更多的adapter那么就设置默认的drawable。


啊,刚吃完中秋晚饭,继续写。


下面我们要说说自定义的RecyclerView,这个自定义的RecyclerView和前景实现中自定义的RecyclerView的手势处理差不多,就是有一些细节需要处理。下面是整个

自定义的RecyclerView的代码

public class AdvancedDrawableRecyclerView extends RecyclerView {
    private boolean canRefresh = true;
    private boolean canLoad = false;

    private static final int DRAG_MAX_DISTANCE_V = 300;
    public static final long MAX_OFFSET_ANIMATION_DURATION = 500;
    private static final float DRAG_RATE = 0.3f;

    private float INITIAL_X = -1;
    private float INITIAL_Y = -1;
    private float lastY = 0;

    private static final String TAG = "ADR";

    private AdvancedDrawable mRefreshDrawable;
    private AdvancedDrawable mLoadDrawable;

    private boolean expectedAdapter = false;
    private boolean showRefreshFlag = false;
    private boolean showLoadFlag = false;
    private ValueAnimator animator;
    private Interpolator mInterpolator = new LinearInterpolator();
    private RefreshableAndLoadable mDataSource;
    private boolean gettingData = false;

    public AdvancedDrawableRecyclerView(Context context) {
        super(context);
        init(context);
    }

    public AdvancedDrawableRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public AdvancedDrawableRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    void init(Context context){
        ViewCompat.setChildrenDrawingOrderEnabled(this, true);
        mRefreshDrawable = new SunAdvancedDrawable(context,this);
        mLoadDrawable = new SunAdvancedBottomDrawable(context,this);
    }

    public void setRefreshDrawable(AdvancedDrawable drawable){
        mRefreshDrawable = drawable;
        if (expectedAdapter){
            ((RefreshLoadWrapper) getAdapter()).setRefreshDrawable(getContext(),mRefreshDrawable);
        }
    }

    public void setLoadDrawable(AdvancedDrawable drawable){
        mLoadDrawable = drawable;
        if (expectedAdapter){
            ((RefreshLoadWrapper) getAdapter()).setRefreshDrawable(getContext(),mLoadDrawable);
        }
    }

    @Override
    public void setAdapter(Adapter adapter) {
        super.setAdapter(adapter);
        if (adapter instanceof RefreshLoadWrapper){
            Log.d(TAG,"adapter kind of RefreshLoadWrapper");
            expectedAdapter = true;
            ((RefreshLoadWrapper) adapter).setRefreshDrawable(getContext(),mRefreshDrawable);
            ((RefreshLoadWrapper) adapter).setLoadDrawable(getContext(),mLoadDrawable);
            mRefreshDrawable.setAdapter(adapter);
            mLoadDrawable.setAdapter(adapter);
        }else {
            expectedAdapter = false;
        }
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent ev) {
        if (!expectedAdapter || (!canRefresh && !canLoad))return super.onTouchEvent(ev);
        if (gettingData)return true;
        final int action = MotionEventCompat.getActionMasked(ev);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (isRunning()){//
                    // can stop animation
                    stop();
                    //fix initial action_down position
                    calculateInitY(MotionEventCompat.getY(ev,0),DRAG_MAX_DISTANCE_V,DRAG_RATE,
                            showRefreshFlag ? mRefreshDrawable.getPercent() : -mLoadDrawable.getPercent());
                }else {
                    INITIAL_Y = MotionEventCompat.getY(ev,0);
                    lastY = INITIAL_Y;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                final float agentY = MotionEventCompat.getY(ev,0);
                if (agentY > INITIAL_Y){
                    // towards bottom
                    if (!canChildScrollUp()){
                        if (!canRefresh)return super.onTouchEvent(ev);
                        if (showLoadFlag)showLoadFlag = false;
                        if (!showRefreshFlag){
                            showRefreshFlag = true;
                            INITIAL_Y = agentY;
                        }
                        mRefreshDrawable.setPercent(fixPercent(Math.abs(calculatePercent(INITIAL_Y,
                                agentY,DRAG_MAX_DISTANCE_V,DRAG_RATE))),true);
                        ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(mRefreshDrawable.getPercent()));
                        lastY = agentY;
                        return true;
                    }else {
                        if(showRefreshFlag)showRefreshFlag = false;
                        lastY = agentY;
                        break;
                    }
                } else if (agentY < INITIAL_Y){

                    if (!canChildScrollBottom()){
                        if (!canLoad)return super.onTouchEvent(ev);
                        if (showRefreshFlag)showRefreshFlag = false;
                        if (!showLoadFlag){
                            showLoadFlag = true;
                            INITIAL_Y = agentY;
                            lastY = agentY;
                        }
                        if (lastY == agentY){
                            break;
                        }
                        float prePercent = mLoadDrawable.getPercent();
                        float newPercent = fixPercent(Math.abs(calculatePercent(INITIAL_Y,
                                agentY, DRAG_MAX_DISTANCE_V, DRAG_RATE)));
                        mLoadDrawable.setPercent(newPercent,true);
                        ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(newPercent));
                        getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(newPercent)));
                        lastY = agentY;
                        return true;
                    }else {
                        if (showLoadFlag)showLoadFlag = false;
                        lastY = agentY;
                        break;
                    }
                }else {
                    showLoadFlag = showRefreshFlag = false;
                    mRefreshDrawable.setPercent(0,false);
                    mLoadDrawable.setPercent(0,true);
                    lastY = agentY;
                    return true;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (!showLoadFlag && !showRefreshFlag)break;
                actionUpOrCancel();
                return true;
        }
        return super.onTouchEvent(ev);
    }

    private boolean isRunning(){
        return animator != null && (animator.isRunning() || animator.isStarted());
    }

    private void stop(){
        animator.cancel();
    }

    private int getViewOffset(float percent){
        if (showRefreshFlag){
            return Math.min((int) (percent * (float) mRefreshDrawable.getIntrinsicHeight() * 0.8),
                    mRefreshDrawable.getIntrinsicHeight());
        }
        return Math.min((int) (percent * (float) mLoadDrawable.getIntrinsicHeight() * 0.8),
                mRefreshDrawable.getIntrinsicHeight());
    }

    private void actionUpOrCancel(){
        if(showLoadFlag && showRefreshFlag){
            throw new IllegalStateException("load state and refresh state should be mutual exclusion!");
        }
        if (showRefreshFlag){
            if (mRefreshDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){
                // 回到临界位置
                toCriticalPositionAnimation(mRefreshDrawable.getPercent());
            }else {
                toStartPositionAnimation(mRefreshDrawable.getPercent());
            }
        }else {
            if (mLoadDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){
                // 回到临界位置
                toCriticalPositionAnimation(mLoadDrawable.getPercent());
            }else {
                toStartPositionAnimation(mLoadDrawable.getPercent());
            }
        }
    }

    private void toCriticalPositionAnimation(final float start){
        animator = ValueAnimator.ofFloat(start,AdvancedDrawable.CRITICAL_PERCENT);
        animator.setInterpolator(mInterpolator);
        animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * (start - AdvancedDrawable.CRITICAL_PERCENT)));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float percent = (float) animation.getAnimatedValue();
                if (showRefreshFlag){
                    mRefreshDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent));
                }else {
                    float prePercent = mLoadDrawable.getPercent();
                    mLoadDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent));
                    getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent)));
                }
                if (percent == AdvancedDrawable.CRITICAL_PERCENT){
                    if (showRefreshFlag){
                        gettingData = true;
                        Toast.makeText(getContext(),"refresh",Toast.LENGTH_SHORT).show();
                        if (mDataSource != null){
                            mDataSource.onRefreshing();
                        }
                        mRefreshDrawable.start();
                    }else {
                        gettingData = true;
                        Toast.makeText(getContext(),"load",Toast.LENGTH_SHORT).show();
                        if (mDataSource != null){
                            mDataSource.onLoading();
                        }
                        mLoadDrawable.start();
                    }
                }
            }
        });
        animator.start();
    }

    private void toStartPositionAnimation(final float start){
        animator = ValueAnimator.ofFloat(start,0);
        animator.setInterpolator(mInterpolator);
        animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * start));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float percent = (float) animation.getAnimatedValue();
                if (showRefreshFlag){
                    mRefreshDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent));
                }else {
                    float prePercent = mLoadDrawable.getPercent();
                    mLoadDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent));
                    getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent)));
                }
                if (percent == 0){
                    showLoadFlag = showRefreshFlag = false;
                }
            }
        });
        animator.start();
    }


    private float fixPercent(float initPercent){
        if (initPercent <= 1){
            return initPercent;
        }else {
            return 1f + (initPercent - 1f) * 0.6f;
        }
    }

    private float calculatePercent(float initialPos,float currentPos,int maxDragDistance,float rate){
        return (currentPos - initialPos) * rate / ((float) maxDragDistance);
    }

    private void calculateInitY(float agentY,int maxDragDistance,float rate,float percent){
        INITIAL_Y = agentY - percent * (float) maxDragDistance / rate;
    }

    private boolean canChildScrollUp() {
        return ViewCompat.canScrollVertically(this, -1);
    }

    private boolean canChildScrollBottom(){
        return !showLoadFlag && !isLastChildShowingCompletely();
    }

    private boolean isLastChildShowingCompletely(){
        return ((getLayoutManager().getPosition(getChildAt(getChildCount() - 2)) == getAdapter().getItemCount() - 2));
    }

    public void setRefreshableAndLoadable(RefreshableAndLoadable dataSource){
        mDataSource = dataSource;
    }

    public void stopRefreshingOrLoading(){
        if (gettingData){
            gettingData = false;
        }
        if (showRefreshFlag){
            mRefreshDrawable.stop();
            toStartPositionAnimation(AdvancedDrawable.CRITICAL_PERCENT);
        }else {
            mLoadDrawable.stop();
            toStartPositionAnimation(AdvancedDrawable.CRITICAL_PERCENT);
        }
    }

    public void setCanRefresh(boolean canRefresh){
        this.canRefresh = canRefresh;
    }

    public void setCanLoad(boolean canLoad){
        this.canLoad = canLoad;
    }
}

AdvancedDrawableRecyclerView这个类的onTouchEvent中有一个细节,就是处理加载更多时的手势代码中,有这两句

((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(newPercent));
                        getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(newPercent)));
第一句是修改footer的高度,第二句是通知LayoutManager需要向下偏移一掉段距离,这个距离是新旧高度之差。那为啥要告诉LayoutManager要偏移呢?这是因为

LayoutManager代理RecyclerView的layout过程,而这里有一个被称为锚的类,这个类可以标记RecyclerView正在展示的child从哪开始,而child在上拉的过程中,如果不偏移,重绘的时候这个起始点的信息不会变,那么上面列表的高度没有变化,那么列表就不会随着手指向上移动,虽然footer高度变化了,但是看不出来,所以需要偏移去修正。


手势处理过程如下,和前景实现保持一致。



                                                手势监听

                                                       |

                                                       |    如果如果手势向下,view不能向下继续滑动

                                                       |   

                                           计算滑动的距离,当滑动距离没有到阀值的时候,根据滑动距离和

                                            最大滑动距离的百分比,计算绘制的图案位置,圆弧的角度等等

                                                       |                                                                   |

                                                       |      当滑动时未超过阀值松开                        |  超过阀值时松开

                                                       |                                                                   |

                                 这个时候让图案回弹就Ok                      图案先回弹到阀值对应的位置,然后开始旋转,

                                                                                                    当刷新完成的时候,回弹




手势过程中平移的百分比计算代码中不难理解,另外在前景实现中已经说过了,所以这儿不说了,可以参考前一篇文章。


我们说说手指抬起来后发生了什么

private void actionUpOrCancel(){
        if(showLoadFlag && showRefreshFlag){
            throw new IllegalStateException("load state and refresh state should be mutual exclusion!");
        }
        if (showRefreshFlag){
            if (mRefreshDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){
                // 回到临界位置
                toCriticalPositionAnimation(mRefreshDrawable.getPercent());
            }else {
                toStartPositionAnimation(mRefreshDrawable.getPercent());
            }
        }else {
            if (mLoadDrawable.getPercent() >= AdvancedDrawable.CRITICAL_PERCENT){
                // 回到临界位置
                toCriticalPositionAnimation(mLoadDrawable.getPercent());
            }else {
                toStartPositionAnimation(mLoadDrawable.getPercent());
            }
        }
    }


很简单,如果超过开始刷新(加载更多)的临界值,就开启回到临界位置的动画,否则就开启回到初始位置的动画



private void toCriticalPositionAnimation(final float start){
        animator = ValueAnimator.ofFloat(start,AdvancedDrawable.CRITICAL_PERCENT);
        animator.setInterpolator(mInterpolator);
        animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * (start - AdvancedDrawable.CRITICAL_PERCENT)));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float percent = (float) animation.getAnimatedValue();
                if (showRefreshFlag){
                    mRefreshDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent));
                }else {
                    float prePercent = mLoadDrawable.getPercent();
                    mLoadDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent));
                    getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent)));
                }
                if (percent == AdvancedDrawable.CRITICAL_PERCENT){
                    if (showRefreshFlag){
                        gettingData = true;
                        Toast.makeText(getContext(),"refresh",Toast.LENGTH_SHORT).show();
                        if (mDataSource != null){
                            mDataSource.onRefreshing();
                        }
                        mRefreshDrawable.start();
                    }else {
                        gettingData = true;
                        Toast.makeText(getContext(),"load",Toast.LENGTH_SHORT).show();
                        if (mDataSource != null){
                            mDataSource.onLoading();
                        }
                        mLoadDrawable.start();
                    }
                }
            }
        });
        animator.start();
    }


这是回弹到临界位置的动画逻辑,这里面,需要更新时的计算出来的值就是Percent,然后把percent传递给Drawable,并且修改Drawable对应的view的高度。

回到初始位置的动画逻辑类似


private void toStartPositionAnimation(final float start){
        animator = ValueAnimator.ofFloat(start,0);
        animator.setInterpolator(mInterpolator);
        animator.setDuration((long) (MAX_OFFSET_ANIMATION_DURATION * start));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float percent = (float) animation.getAnimatedValue();
                if (showRefreshFlag){
                    mRefreshDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setRefreshHeight(getViewOffset(percent));
                }else {
                    float prePercent = mLoadDrawable.getPercent();
                    mLoadDrawable.setPercent(percent,true);
                    ((RefreshLoadWrapper) getAdapter()).setLoadHeight(getViewOffset(percent));
                    getLayoutManager().offsetChildrenVertical((getViewOffset(prePercent) - getViewOffset(percent)));
                }
                if (percent == 0){
                    showLoadFlag = showRefreshFlag = false;
                }
            }
        });
        animator.start();
    }

我们继续看看drawable是如何根据percent值绘制的。

这是我自定义的一个drawable,对应的是默认的刷新的动画。


public class SunAdvancedDrawable extends AdvancedDrawable {
    private Bitmap mSky;
    private Bitmap mSun;
    private Matrix mMatrix;
    private static final long MAX_OFFSET_ANIMATION_DURATION = 1000;

    private ValueAnimator valueAnimator;
    private Interpolator mInterpolator = new LinearInterpolator();

    private int mSkyHeight;

    private int mSunSize = 100;
    private float mSunLeftOffset = 220;
    private float mRotate = 0.0f;
    private int sunRoutingHeight;
    private boolean startAnimation = false;

    public SunAdvancedDrawable(Context context, final View view){
        super();
        view.post(new Runnable() {
            @Override
            public void run() {
                dWidth = view.getMeasuredWidth();
                dHeight = (int) (dWidth * 0.5f);
                mMatrix = new Matrix();
                init(view.getContext());
            }
        });
    }

    @Override
    protected void init(Context context){
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        mSky = BitmapFactory.decodeResource(context.getResources(), R.drawable.sky, options);
        mSky = Bitmap.createScaledBitmap(mSky, dWidth, dHeight, true);

        mSkyHeight = dHeight;
        sunRoutingHeight = (int) ((mSkyHeight - mSunSize) * 0.9);
        mSunLeftOffset = 0.3f * (float) dWidth;

        createBitmaps(context);
    }

    private void createBitmaps(Context context) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;

        mSky = BitmapFactory.decodeResource(context.getResources(), R.drawable.sky, options);
        mSky = Bitmap.createScaledBitmap(mSky, dWidth, mSkyHeight, true);
        mSun = BitmapFactory.decodeResource(context.getResources(), R.drawable.sun, options);
        mSun = Bitmap.createScaledBitmap(mSun, mSunSize, mSunSize, true);
    }

    @Override
    public void start() {
        startAnimation = true;
        ensureAnimation();
        valueAnimator.start();
    }

    @Override
    public void stop() {
        startAnimation = false;
        if(valueAnimator.isRunning() || valueAnimator.isStarted()){
            valueAnimator.cancel();
        }
    }

    @Override
    public boolean isRunning() {
        return valueAnimator != null && valueAnimator.isRunning();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        drawSky(canvas);
        drawSun(canvas);
    }

    private void ensureAnimation(){
        valueAnimator = ValueAnimator.ofFloat(0,359);
        valueAnimator.setDuration(MAX_OFFSET_ANIMATION_DURATION);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setRepeatMode(ValueAnimator.RESTART);
        valueAnimator.setInterpolator(mInterpolator);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mRotate = (float) animation.getAnimatedValue();
                invalidateSelf();
            }
        });
    }

    private void drawSky(Canvas canvas) {
        Matrix matrix = mMatrix;
        matrix.reset();
        int offsetY = (int) (30 * Math.min(mPercent,1)) - 50;
        matrix.postTranslate(0,offsetY);
        canvas.drawBitmap(mSky, matrix, null);
    }

    private void drawSun(Canvas canvas) {
        Matrix matrix = mMatrix;
        matrix.reset();

        float dragPercent = mPercent;
        if (dragPercent > 1){
            dragPercent = 1f + (dragPercent - 1f) * 0.4f;
        }

        int offsetY = (int) (Math.max(mSkyHeight - mSunSize - (int) (dragPercent * sunRoutingHeight),0) * 0.8);
        matrix.postTranslate(mSunLeftOffset,offsetY);
        matrix.postRotate(
                startAnimation ? mRotate : 360 * mPercent,
                mSunLeftOffset + mSunSize / 2,
                offsetY + mSunSize / 2);

        canvas.drawBitmap(mSun, matrix, null);
    }
}


这是根据github上优秀项目pullToRefresh改写的,在这里感谢pullToRefresh项目作者。



我们继续说源码,自定义的drawable中根据传进来的View初始化了相关数据。


draw的过程分为两步,绘制天空,绘制太阳。

private void drawSky(Canvas canvas) {
        Matrix matrix = mMatrix;
        matrix.reset();
        int offsetY = (int) (30 * Math.min(mPercent,1)) - 50;
        matrix.postTranslate(0,offsetY);
        canvas.drawBitmap(mSky, matrix, null);
    }
绘制天空中offsetY的作用时让天空有一个向下平移的过程,而不是呆板的保持不动。

private void drawSun(Canvas canvas) {
        Matrix matrix = mMatrix;
        matrix.reset();

        float dragPercent = mPercent;
        if (dragPercent > 1){
            dragPercent = 1f + (dragPercent - 1f) * 0.4f;
        }

        int offsetY = (int) (Math.max(mSkyHeight - mSunSize - (int) (dragPercent * sunRoutingHeight),0) * 0.8);
        matrix.postTranslate(mSunLeftOffset,offsetY);
        matrix.postRotate(
                startAnimation ? mRotate : 360 * mPercent,
                mSunLeftOffset + mSunSize / 2,
                offsetY + mSunSize / 2);

        canvas.drawBitmap(mSun, matrix, null);
    }
绘制太阳逻辑除了高度的偏移值,还需要计算旋转的角度。

关于刷新时太阳旋转的动画实现

private void ensureAnimation(){
        valueAnimator = ValueAnimator.ofFloat(0,359);
        valueAnimator.setDuration(MAX_OFFSET_ANIMATION_DURATION);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setRepeatMode(ValueAnimator.RESTART);
        valueAnimator.setInterpolator(mInterpolator);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mRotate = (float) animation.getAnimatedValue();
                invalidateSelf();
            }
        });
    }

开启一个ValueAnimator,这个ValueAnimator用来计算每个时间点对应的角度值。这样随着时间的流逝,太阳就转动了。



如果你想自定义Drawable,你只需要创建一个继承AdvancedDrawable的类,实现相关方法,然后把类实例设置给Adapter就好。


好了,这个项目差不多就是这样了,欢迎各位留言交流。


欢迎加入github优秀项目分享群:589284497,不管你是项目作者或者爱好者,请来和我们一起交流吧。


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值