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

来源:互联网 发布:支付宝端口申请 编辑:程序博客网 时间:2024/06/03 14:41

项目已经上传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,不管你是项目作者或者爱好者,请来和我们一起交流吧。


阅读全文
0 0
原创粉丝点击