如今,下拉刷新都已经成为App的标配了,哪个App会没有下拉刷新呢?作为一个普通app用户,想要去刷新内容,习惯性地就下拉一下,已经成为用户的一种习惯啦。作为一个开发者,想要集成下拉刷新,直接就拷贝Android-PullToRefresh或者是android-Ultra-Pull-To-Refresh又或者其他下拉刷新库,直接使用经典的下拉刷新UI,一个标志性的下拉箭头,一句经典的下拉以刷新更多。一开始还好,慢慢的作为一个使用者而言,就觉得没什么新意了,作为一个开发者也觉得没什么挑战了。
那这样,何不尝试自己动手去实现一个真正属于自己的下拉刷新库呢?嘿嘿,那说干就干呗。那想想,现在下拉刷新的轮子那么多,我们也不可能从零开始写,时间也不允许啊,白天要上班写业务代码,回到宿舍还得重复别人写过的代码,而且自己写的还派不上用场,因为已经有现成的,而且别人写的东西那么多人用上了,踩过的坑肯定比你想的要多。虽然话说凡事要自己去实践,理解的才会更深。说是这么说,但是我还是觉得站在巨人肩膀上,才会看得更远,嘿嘿......
正因为如此,我才基于android-Ultra-Pull-To-Refresh实现一个很Q的笑脸下拉刷新,不吹不黑,真的很Q哦。这个下拉大概的效果就是,下拉时,随header高度变化而缩放、转眼睛,转啊转。松开时,一个转眼睛的笑脸加载动画。
扯那么多,我自己都觉得有点不耐烦了。下面来简要分析一下,怎么去实现一个个性化的下拉刷新库。以下是整个库的目录,想不到吧,就仅仅三个类,一个布局文件。就可以实现你自己能想到的下拉效果。
下面贴出最主要相关的类
- 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>
想想,还是放张图片,比较有吸引力。
好啦,就到此为止吧,代码不难,注释写得也清楚,所以就不说明了哈。主要还是因为我懒,以后再慢慢试着写点分析,自己的思路什么的。最后,源码放在Github上,觉得可以学到点东西的,start一下吧!点我,点我吧