何不自己动手去实现一个属于自己的个性化Android下拉刷新库?

如今,下拉刷新都已经成为App的标配了,哪个App会没有下拉刷新呢?作为一个普通app用户,想要去刷新内容,习惯性地就下拉一下,已经成为用户的一种习惯啦。作为一个开发者,想要集成下拉刷新,直接就拷贝Android-PullToRefresh或者是android-Ultra-Pull-To-Refresh又或者其他下拉刷新库,直接使用经典的下拉刷新UI,一个标志性的下拉箭头,一句经典的下拉以刷新更多。一开始还好,慢慢的作为一个使用者而言,就觉得没什么新意了,作为一个开发者也觉得没什么挑战了。

那这样,何不尝试自己动手去实现一个真正属于自己的下拉刷新库呢?嘿嘿,那说干就干呗。那想想,现在下拉刷新的轮子那么多,我们也不可能从零开始写,时间也不允许啊,白天要上班写业务代码,回到宿舍还得重复别人写过的代码,而且自己写的还派不上用场,因为已经有现成的,而且别人写的东西那么多人用上了,踩过的坑肯定比你想的要多。虽然话说凡事要自己去实践,理解的才会更深。说是这么说,但是我还是觉得站在巨人肩膀上,才会看得更远,嘿嘿……

正因为如此,我才基于android-Ultra-Pull-To-Refresh实现一个很Q的笑脸下拉刷新,不吹不黑,真的很Q哦。这个下拉大概的效果就是,下拉时,随header高度变化而缩放、转眼睛,转啊转。松开时,一个转眼睛的笑脸加载动画。

扯那么多,我自己都觉得有点不耐烦了。下面来简要分析一下,怎么去实现一个个性化的下拉刷新库。以下是整个库的目录,想不到吧,就仅仅三个类,一个布局文件。就可以实现你自己能想到的下拉效果。

《何不自己动手去实现一个属于自己的个性化Android下拉刷新库?》 Paste_Image.png

下面贴出最主要相关的类

  • PullToRefreshFaceView

/**
 *
 * @作 用:下拉刷新的笑脸
 * @创 建 人: linguoding
 * @日 期: 2016/3/9
 */
public class PullToRefreshFaceView extends View {
    private Paint paint;//画笔
    private Paint eyePaint;
    private int backgroupColor;
    private int width;
    private int height;
    private int centerWidth;
    private int centerHeight;
    private float degrees;
    private float radius = 0;
    private float sweepRadius = 180;

    private int radiusCircle;
    private int eyeRadius;
    private int eyeBallRadius;
    private boolean isDrawFace = false;

    AnimatorSet set = new AnimatorSet();


    public PullToRefreshFaceView(Context context) {
        super(context);
        initView();
    }

    public PullToRefreshFaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
        backgroupColor = array.getColor(R.styleable.LoadingView_backgroupColor, Color.BLACK);
        array.recycle();
        initView();

    }

    public void setDegrees(float degrees) {
        this.degrees = degrees;
        invalidate();
    }

    public void setRadius(float radius) {
        this.radius = radius;
        invalidate();
    }

    public void setSweepRadius(float sweepRadius) {
        this.sweepRadius = sweepRadius;
        invalidate();
    }

    public void setRadiusCircle(int radiusCircle) {
        this.radiusCircle = radiusCircle;
    }

    public void setEyeRadius(int eyeRadius) {
        this.eyeRadius = eyeRadius;

    }

    public void setEyeBallRadius(int eyeBallRadius) {
        this.eyeBallRadius = eyeBallRadius;

    }

    public void setDrawFace(boolean isDrawFace) {
        this.isDrawFace = isDrawFace;
    }

    public int getRadiusCircle() {
        return radiusCircle;
    }

    public int getEyeRadius() {
        return eyeRadius;
    }

    public int getEyeBallRadius() {
        return eyeBallRadius;
    }

    public boolean isDrawFace() {
        return isDrawFace;
    }

    public void setBackgroupColor(int backgroupColor) {
        this.backgroupColor = backgroupColor;
    }

    /**
     * 刷新用的效果
     *
     * @param sunRadius
     * @param per
     */
    public void setPerView(int sunRadius, float per) {
        if (per >= 0.5) {
            isDrawFace = true;
        } else {
            isDrawFace = false;
        }
        per = Math.min(per, 1);
        float tempRadius = sunRadius * per;
        this.radiusCircle = (int) tempRadius;
        invalidate();
    }





    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(backgroupColor);
        //眼眶画笔
        eyePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        eyePaint.setColor(Color.WHITE);
        eyePaint.setStyle(Paint.Style.FILL);
        createAnimatorSet();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /*super.onMeasure(widthMeasureSpec, heightMeasureSpec);*/
        width = measureResult(widthMeasureSpec);
        height = measureResult(heightMeasureSpec);
        centerWidth = width >> 1;
        centerHeight = height >> 1;
        setMeasuredDimension(width, height);

    }

    private int measureResult(int widthMeasureSpec) {
        int result = 0;
        int sizeSpec = MeasureSpec.getSize(widthMeasureSpec);
        int modeSpec = MeasureSpec.getMode(widthMeasureSpec);
        if (modeSpec == MeasureSpec.EXACTLY) {
            result = sizeSpec;
        } else {
            result = 400;
            if (modeSpec == MeasureSpec.AT_MOST) {
                result = Math.min(result, sizeSpec);
            }
        }
        return result;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        centerWidth = width / 2;
        centerHeight = height / 2;
        canvas.translate(centerWidth, centerHeight);
        radiusCircle = Math.min(centerWidth, centerHeight);
        //画圆
        canvas.drawCircle(0, 0, radiusCircle, paint);
        if (isDrawFace) {
            //画两个眼框
            eyeRadius = Math.min(centerWidth / 3, centerHeight / 3);
            canvas.drawCircle(-centerWidth / 2, -centerHeight >> 3, eyeRadius, eyePaint);
            canvas.drawCircle(centerWidth / 2, -centerHeight >> 3, eyeRadius, eyePaint);
            //画嘴巴
            canvas.drawArc(new RectF(-eyeRadius, 0, eyeRadius, eyeRadius * 2), 0, 180, true, eyePaint);

            canvas.save();
            //画两个眼睛
            eyeBallRadius = Math.min(centerWidth >> 3, centerHeight >> 3);
            canvas.translate(-centerWidth / 2, -centerHeight >> 3);
            canvas.rotate(-degrees);
            canvas.drawCircle(0, (eyeRadius >> 1), eyeBallRadius, paint);
            canvas.restore();

            canvas.save();

            canvas.translate(centerWidth / 2, -centerHeight >> 3);
            canvas.rotate(-degrees);
            canvas.drawCircle(0, (eyeRadius >> 1), eyeBallRadius, paint);
            canvas.restore();

            //画两个眼皮
            canvas.save();
            canvas.translate(-centerWidth / 2, -centerHeight >> 3);
            canvas.drawArc(new RectF(-eyeRadius, -eyeRadius, eyeRadius, eyeRadius), -this.radius, -this.sweepRadius, false, paint);
            canvas.restore();

            canvas.save();
            canvas.translate(centerWidth / 2, -centerHeight >> 3);
            canvas.drawArc(new RectF(-eyeRadius, -eyeRadius, eyeRadius, eyeRadius), -this.radius, -this.sweepRadius, false, paint);
            canvas.restore();
        }


    }

    private void createAnimatorSet() {
        ValueAnimator rotateAnimator = ValueAnimator.ofFloat(0, 360).setDuration(3000);
        rotateAnimator.setInterpolator(new LinearInterpolator());
        rotateAnimator.setRepeatCount(-1);
        rotateAnimator.setEvaluator(new FloatEvaluator());
        rotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                degrees = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });

        ValueAnimator translationAnimator = ValueAnimator.ofFloat(0, 90, 0).setDuration(3000);
        translationAnimator.setInterpolator(new LinearInterpolator());
        translationAnimator.setRepeatCount(-1);
        translationAnimator.setEvaluator(new FloatEvaluator());
        translationAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                radius = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });

        ValueAnimator sweepAnimator = ValueAnimator.ofFloat(180, 0, 180).setDuration(3000);
        sweepAnimator.setInterpolator(new LinearInterpolator());
        sweepAnimator.setRepeatCount(-1);
        sweepAnimator.setEvaluator(new FloatEvaluator());
        sweepAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                sweepRadius = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        set.playTogether(rotateAnimator, translationAnimator, sweepAnimator);
    }

    public void startAnimators() {
        set.start();
    }

    public void stopAnimators() {
        set.cancel();
    }
}

  • FacePullToRefreshHeader 类
package com.pulltorefreshlibrary;

import android.content.Context;
import android.content.SharedPreferences;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;

import com.pulltorefreshlibrary.view.PullToRefreshFaceView;

import java.text.SimpleDateFormat;
import java.util.Date;

import in.srain.cube.views.ptr.PtrFrameLayout;
import in.srain.cube.views.ptr.PtrUIHandler;
import in.srain.cube.views.ptr.indicator.PtrIndicator;

public class FacePullToRefreshHeader extends FrameLayout implements PtrUIHandler {
    private final static String KEY_SharedPreferences = "face_ptr_classic_last_update";
    private TextView mTitleTextView;
    private PullToRefreshFaceView mLoadView;
    private long mLastUpdateTime = -1;
    private TextView mLastUpdateTextView;
    private String mLastUpdateTimeKey;
    private static SimpleDateFormat sDataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private boolean mShouldShowLastUpdate;

    private LastUpdateTimeUpdater mLastUpdateTimeUpdater = new LastUpdateTimeUpdater();


    public FacePullToRefreshHeader(Context context) {
        super(context);
        initViews(null);
    }

    public FacePullToRefreshHeader(Context context, AttributeSet attrs) {
        super(context, attrs);
        initViews(attrs);
    }

    public FacePullToRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initViews(attrs);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mLastUpdateTimeUpdater != null) {
            mLastUpdateTimeUpdater.stop();
        }
    }

    private void initViews(AttributeSet attrs) {
        View header = LayoutInflater.from(getContext()).inflate(R.layout.face_pull_to_refresh_header, this);
        mTitleTextView = (TextView) header.findViewById(R.id.ptr_face_header_title);
        mLastUpdateTextView = (TextView) header.findViewById(R.id.ptr_face_header_last_update);
        mLoadView = (PullToRefreshFaceView) header.findViewById(R.id.ptr_load_view);

    }


    private void tryUpdateLastUpdateTime() {
        if (TextUtils.isEmpty(mLastUpdateTimeKey) || !mShouldShowLastUpdate) {
            mLastUpdateTextView.setVisibility(GONE);
        } else {
            String time = getLastUpdateTime();
            if (TextUtils.isEmpty(time)) {
                mLastUpdateTextView.setVisibility(GONE);
            } else {
                mLastUpdateTextView.setVisibility(VISIBLE);
                mLastUpdateTextView.setText(time);
            }
        }
    }


    public void setLastUpdateTimeKey(String key) {
        if (TextUtils.isEmpty(key)) {
            return;
        }
        mLastUpdateTimeKey = key;
    }

    public void setLastUpdateTimeRelateObject(Object object) {
        setLastUpdateTimeKey(object.getClass().getName());
    }

    
     /**
     * 得到最后刷新时间
     *
     * @return
     */
    private String getLastUpdateTime() {

        if (mLastUpdateTime == -1 && !TextUtils.isEmpty(mLastUpdateTimeKey)) {
            mLastUpdateTime = getContext().getSharedPreferences(KEY_SharedPreferences, 0).getLong(mLastUpdateTimeKey, -1);
        }
        if (mLastUpdateTime == -1) {
            return null;
        }
        long diffTime = new Date().getTime() - mLastUpdateTime;
        int seconds = (int) (diffTime / 1000);
        if (diffTime < 0) {
            return null;
        }
        if (seconds <= 0) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(getContext().getString(R.string.ai_jia_ptr_last_update));

        if (seconds < 60) {
            sb.append(seconds + getContext().getString(in.srain.cube.views.ptr.R.string.cube_ptr_seconds_ago));
        } else {
            int minutes = (seconds / 60);
            if (minutes > 60) {
                int hours = minutes / 60;
                if (hours > 24) {
                    Date date = new Date(mLastUpdateTime);
                    sb.append(sDataFormat.format(date));
                } else {
                    sb.append(hours + getContext().getString(R.string.ai_jia_ptr_hours_ago));
                }

            } else {
                sb.append(minutes + getContext().getString(R.string.ai_jia_ptr_minutes_ago));
            }
        }
        return sb.toString();
    }

    private void resetView() {
        //隐藏加载view和停止动画
        mLoadView.stopAnimators();
        mLoadView.setVisibility(INVISIBLE);

    }


    /**
     * 重置,回到顶部的
     *
     * @param frame
     */
    @Override
    public void onUIReset(PtrFrameLayout frame) {
        resetView();
        mShouldShowLastUpdate = true;
        tryUpdateLastUpdateTime();
    }


    /**
     * 准备刷新,Header 将要出现时调用。
     *
     * @param frame
     */
    @Override
    public void onUIRefreshPrepare(PtrFrameLayout frame) {
        mShouldShowLastUpdate = true;
        tryUpdateLastUpdateTime();
        mLoadView.setVisibility(VISIBLE);
        mTitleTextView.setVisibility(VISIBLE);
        mTitleTextView.setText(getResources().getString(R.string.ai_jia_ptr_pull_down_to_refresh));

    }

    /**
     * 开始刷新,Header 进入刷新状态之前调用。
     *
     * @param frame
     */
    @Override
    public void onUIRefreshBegin(PtrFrameLayout frame) {
        mShouldShowLastUpdate = false;

        /**
         * 让loadView开始动画
         * */
        mLoadView.startAnimators();
        mTitleTextView.setVisibility(VISIBLE);
        mTitleTextView.setText(R.string.ai_jia_ptr_refreshing);
        tryUpdateLastUpdateTime();
        mLastUpdateTimeUpdater.stop();
    }

    @Override
    public void onUIRefreshComplete(PtrFrameLayout frame) {
        /*hideRotateView();
        mProgressBar.setVisibility(INVISIBLE);*/
        /*
         * 加载完成,loadView停止动画
         * */
        mLoadView.stopAnimators();
        mTitleTextView.setVisibility(VISIBLE);
        mTitleTextView.setText(getResources().getString(R.string.ai_jia_ptr_refresh_complete));

        // update last update time
        SharedPreferences sharedPreferences = getContext().getSharedPreferences(KEY_SharedPreferences, 0);
        if (!TextUtils.isEmpty(mLastUpdateTimeKey)) {
            mLastUpdateTime = new Date().getTime();
            sharedPreferences.edit().putLong(mLastUpdateTimeKey, mLastUpdateTime).commit();
        }
    }


    /**
     * 下拉过程中位置变化回调。
     *
     * @param frame
     * @param isUnderTouch
     * @param status
     * @param ptrIndicator
     */
    @Override
    public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
        float percent = Math.min(1f, ptrIndicator.getCurrentPercent());//得到下拉过程的位置比例
        if (status == PtrFrameLayout.PTR_STATUS_PREPARE) {
            mLoadView.setDegrees(percent * 360 * 4);
            ViewCompat.setScaleX(mLoadView, percent);
            ViewCompat.setScaleY(mLoadView, percent);
            mLoadView.setEyeRadius((int) (mLoadView.getEyeRadius()*percent));
            mLoadView.setEyeBallRadius((int) (mLoadView.getEyeBallRadius()*percent));
            mLoadView.setPerView(mLoadView.getRadiusCircle(), percent);
            if (percent < 0.5) {
                mLoadView.setRadius((percent * 180));
                mLoadView.setSweepRadius((180 - percent * 360));
            } else {
                percent = (float) (percent - 0.5);
                mLoadView.setRadius((90 - percent * 180));
                mLoadView.setSweepRadius((percent * 360));
            }

        }
        final int mOffsetToRefresh = frame.getOffsetToRefresh();
        final int currentPos = ptrIndicator.getCurrentPosY();//当前位置
        final int lastPos = ptrIndicator.getLastPosY();//上一个位置
        if (currentPos < mOffsetToRefresh && lastPos >= mOffsetToRefresh) {
            if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) {
                //下拉刷新
                crossRotateLineFromBottomUnderTouch(frame);

            }
        } else if (currentPos > mOffsetToRefresh && lastPos <= mOffsetToRefresh) {
            if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) {
                //释放刷新
                crossRotateLineFromTopUnderTouch(frame);

            }
        }
    }


    /**
     * 释放刷新
     *
     * @param frame
     */
    private void crossRotateLineFromTopUnderTouch(PtrFrameLayout frame) {
        if (!frame.isPullToRefresh()) {
            mTitleTextView.setVisibility(VISIBLE);
            mTitleTextView.setText(R.string.ai_jia_ptr_release_to_refresh);
        }
    }

    /**
     * 下拉刷新
     *
     * @param frame
     */
    private void crossRotateLineFromBottomUnderTouch(PtrFrameLayout frame) {
        mTitleTextView.setVisibility(VISIBLE);
        if (frame.isPullToRefresh()) {
            mTitleTextView.setText(getResources().getString(R.string.ai_jia_ptr_pull_down_to_refresh));
        } else {
            mTitleTextView.setText(getResources().getString(R.string.ai_jia_ptr_pull_down_to_refresh));
        }
    }


    private class LastUpdateTimeUpdater implements Runnable {
        private boolean mRunning = false;

        private void start() {
            if (TextUtils.isEmpty(mLastUpdateTimeKey)) {
                return;
            }
            mRunning = true;
            run();
        }

        private void stop() {
            mRunning = false;
            removeCallbacks(this);
        }

        @Override
        public void run() {
            tryUpdateLastUpdateTime();
            if (mRunning) {
                postDelayed(this, 1000);
            }
        }
    }
}
  • FacePullToRefreshLayout


public class FacePullToRefreshLayout extends PtrFrameLayout {
    private FacePullToRefreshHeader mPullToRefreshHeader;
    private RefreshListener refreshListener;

    public FacePullToRefreshLayout(Context context) {
        super(context);
        initViews();
    }

    public FacePullToRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initViews();
    }

    public FacePullToRefreshLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initViews();
    }

    private void initViews() {
        mPullToRefreshHeader = new FacePullToRefreshHeader(getContext());
        setHeaderView(mPullToRefreshHeader);
        addPtrUIHandler(mPullToRefreshHeader);
        setPtrHandler(new PtrDefaultHandler() {
            @Override
            public void onRefreshBegin(PtrFrameLayout frame) {
                if (refreshListener != null) {
                    refreshListener.onRefresh(frame);
                }
            }
        });
    }

    public FacePullToRefreshHeader getHeader() {
        return mPullToRefreshHeader;
    }

    public void setLastUpdateTimeKey(String key) {
        if (mPullToRefreshHeader != null) {
            mPullToRefreshHeader.setLastUpdateTimeKey(key);
        }
    }


    public void setLastUpdateTimeRelateObject(Object object) {
        if (mPullToRefreshHeader != null) {
            mPullToRefreshHeader.setLastUpdateTimeRelateObject(object);
        }
    }


    public interface RefreshListener {
        void onRefresh(PtrFrameLayout frame);
    }

    public void setRefreshListener(RefreshListener refreshListener) {
        this.refreshListener = refreshListener;
    }
}

  • 布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="90dp"
              android:gravity="center"
              android:orientation="vertical">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical">

            <com.pulltorefreshlibrary.view.PullToRefreshFaceView
                android:id="@+id/ptr_load_view"
                android:layout_width="50dp"
                android:layout_height="50dp"
                app:backgroupColor="#88c6c6c6"
                />

            <TextView
                android:id="@+id/ptr_face_header_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="下拉刷新..."
                android:textColor="#666666"
                android:textSize="12sp"/>

        </LinearLayout>

        <TextView
            android:id="@+id/ptr_face_header_last_update"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="2dp"
            android:text="距离上次刷新:08秒之前"
            android:textColor="#999999"
            android:textSize="10sp"/>
    </LinearLayout>


</LinearLayout>

想想,还是放张图片,比较有吸引力。

《何不自己动手去实现一个属于自己的个性化Android下拉刷新库?》 Paste_Image.png

好啦,就到此为止吧,代码不难,注释写得也清楚,所以就不说明了哈。主要还是因为我懒,以后再慢慢试着写点分析,自己的思路什么的。最后,源码放在Github上,觉得可以学到点东西的,start一下吧!点我,点我吧

    原文作者:峥嵘岁月Z
    原文地址: https://www.jianshu.com/p/6567ea130182
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞