今天终于有时间把最后的成果分享给大家了,为了提高一下博客的逼格,我也找了一个专门做原型、导图的在线网站:processon(www.processon.com),这个工具真的很棒,也很方便,这里给他点个赞。
CircleProgressSuperBar为了完成最终的效果,我也是踩了一些坑,今天把我总结的最清晰的思路分享给大家,首先我们回顾一下我们的效果图:
首先我们来分析一下,CircleProgressSuperBar总共分为几种状态:
这四种状态,我们不关心是怎么切换的,我们只关心动画是怎么过渡的。
首先我们知道准备一些类:
1、CircleProgressSuperBar,这个是主类,也是最终要完成的view。
2、四个状态的Drawable,我们分别命名为:NormalDrawable、LoadingDrawable、CompleteDrawable和ErrorDrawable。
最终为了方便扩展和解耦,我最终实现的架构图是这样的:
我的主要目的:
1、在View和Drawable之间创建工厂类,即能降低类之间的耦合,也可以方便扩展更多状态的Drawable。
2、各种状态Drawble的基类BaseStatusDrawable,封装公共的属性和方法,让View直接使用BaseStatusDrawable类型,而不去具体关心具体的Drawable的实现。
3、BaseStatusDrawable内部带有样式的信息,防止和内部的画笔有关的颜色弄混。
那我们就从最基础的部分,首先新建CircleProgressSuperInfo:
public class CircleProgressSuperInfo {
/**
* 宽, 在设置动画的时候需要知道宽
*/
private int mWidth;
/**
* 高,在设置动画的时候需要知道高
*/
private int mHeight;
/**
* 圆角
*/
private int mRadius;
/**
* 背景颜色
*/
private int mBgColor;
/**
* 边框颜色
*/
private int mBorderColor;
/**
* 边框的宽度
*/
private int mBorderWidth;
/**
* 最大的间距
* */
private float mPadding;
public CircleProgressSuperInfo(int bgColor, int borderColor, int borderWidth) {
this.mBgColor = bgColor;
this.mBorderColor = borderColor;
this.mBorderWidth = borderWidth;
}
...
// 此处省略setter和getter方法
}
然后就是需要BaseStatusDrawable,我们直接把之前写好的形状变化的ChangeShapeAndColorButton进行改造,变成我们需要的BaseStatusDrawable:
/**
* Created by li.zhipeng on 2017/7/12.
*
* 所有的状态图片需要实现此接口
*/
public abstract class BaseStatusDrawable extends Drawable {
protected CircleProgressSuperInfo mInfo;
/**
* 宽, 在设置动画的时候需要知道宽
*/
protected int mWidth;
/**
* 高,在设置动画的时候需要知道高
*/
protected int mHeight;
/**
* 圆角
*/
protected float mRadius;
/**
* 文字
*/
protected String mText;
/**
* 文字颜色
*/
protected int mTextColor = Color.parseColor("#ffffff");
/**
* 文字大小
*/
protected int mTextSize;
/**
* 画笔
*/
protected Paint mPaint;
/**
* 形状
*/
protected RectF mRectF;
/**
* 背景颜色
*/
protected int mBgColor;
/**
* 边框颜色
*/
protected int mBorderColor;
/**
* 边框的宽度
*/
protected int mBorderWidth;
/**
* 偏移值,也就是大小要发生的变化值
*/
protected float mPadding;
/**
* 最小大小
*/
protected float mMinSize = -1;
/**
* 正在动画在中
*/
protected boolean isAnim;
public BaseStatusDrawable(CircleProgressSuperInfo info) {
this.mInfo = info;
// 初始化画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setDither(true);
mRectF = new RectF();
}
// 此处省略各种setter和getter方法
...
public CircleProgressSuperInfo getInfo(){return this.mInfo;};
/**
* 回到后台,调用此方法
*/
public abstract void release();
/**
* 绘制
*/
@Override
public void draw(@NonNull Canvas canvas) {
// 这是绘制过渡动画
if (isAnim()) {
drawTransition(canvas);
}
// 绘制正常状态,例如loading就要使用绘制旋转的圆圈
else {
drawSelf(canvas);
}
}
/**
* 绘制过度动画
*/
protected void drawTransition(Canvas canvas) {
// 先画出背景,背景是居中的
// 判断宽高
int width = getWidth();
int height = getHeight();
// 计算左右的间距值,并且判断不能小于minSize
float paddingLR = width - mPadding * 2 < mMinSize ? (width - mMinSize) / 2 : mPadding;
float paddingTB = height - mPadding * 2 < mMinSize ? (height - mMinSize) / 2 : mPadding;
// 绘制描边
mRectF.set(paddingLR + mBorderWidth / 2, paddingTB + mBorderWidth / 2, getWidth() - paddingLR - mBorderWidth / 2,
getHeight() - paddingTB - mBorderWidth / 2);
// 开始画后面的背景
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mBgColor);
canvas.drawRoundRect(mRectF, mRadius, mRadius, mPaint);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mBorderColor);
mPaint.setStrokeWidth(mBorderWidth);
canvas.drawRoundRect(mRectF, mRadius, mRadius, mPaint);
// 居中绘制文字
if (!TextUtils.isEmpty(mText)) {
float textDescent = mPaint.getFontMetrics().descent;
float textAscent = mPaint.getFontMetrics().ascent;
float delta = Math.abs(textAscent) - textDescent;
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
float textWidth = mPaint.measureText(mText);
canvas.drawText(mText, (width - textWidth) / 2, height / 2 + delta / 2, mPaint);
}
}
/**
* 绘制正常状态
*/
public abstract void drawSelf(Canvas canvas);
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int i) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
这里要强调几点:
1、继承Drawable的原因:主要是为了重绘,例如之后的加载状态,是需要不断重绘的才会有转圈的动画,但是已经和View分离了,就无法借助View.invaliate(),所以这里继承了Drawable。
2、drawSelf()是绘制非过渡动画的状态,这里主要是给LoadingDrawable用的。
3、getOpacity()方法的作用:返回图片的质量类型。
为了理解getOpacity的作用,我们就先看一下他的源码和注释:
// 截取的注释,getOpacity值能返回一下几个值
PixelFormat.UNKNOWN
PixelFormat.TRANSLUCENT
PixelFormat.TRANSPARENT
PixelFormat.OPAQUE
// 系统自动适配
public static final int UNKNOWN = 0;
// 简单的说就是支持半透明
public static final int TRANSLUCENT = -3;
// 支持全透明
public static final int TRANSPARENT = -2;
// 不支持透明
public static final int OPAQUE = -1;
属性的命名规则就是见字如见人,字面意思就是这样。其他两个方法,这里没有用到就不说明了,有兴趣的可以自己去研究研究。
这个时候去就可以创建四个状态的Drawable了,普通状态、完成状态还有错误状态都是一样的,所以就只贴一个类的代码了:
/**
* Created by li.zhipeng on 2017/7/12.
*
* 正常状态下的图片
*/
public class NormalDrawable extends BaseStatusDrawable {
public NormalDrawable(CircleProgressSuperInfo info) {
super(info);
// 普通状态的颜色要设置,其他的不用设置
setBgColor(info.getBgColor());
setBorderColor(info.getBorderColor());
}
@Override
public void drawSelf(Canvas canvas) {
drawTransition(canvas);
}
@Override
public void release() {
}
}
重点是LoadingDrawable,其实也很简单,因为继承的关系,现在只需要关心绘制加载状态就足够了,这个时候把我们第一篇CircleProgressBar进行改造:
/**
* Created by li.zhipeng on 2017/7/12.
*
* 加载状态或是进度条的drawable
*/
public class LoadingDrawable extends BaseStatusDrawable {
/**
* 圆周的角度
*/
private static final Float CIRCULAR = 360f;
/**
* 进度
*/
private float mProgress = 50;
/**
* 最大进度
*/
private int mMaxProgress = 100;
/**
* 边框颜色,也就是进度的颜色
*/
private int mProgressColor = Color.parseColor("#ff00ff");
/**
* 绘制的不全进度的颜色
*/
private int mDrawBorderColor;
/**
* 要绘制的进度条的颜色
*/
private int mDrawProgressColor;
/**
* 是否打开过度模式,也就是我们平时看到的类似追赶的效果
*/
private boolean mIsIntermediateMode = true;
/**
* 最小弧度,进度条过度模式最小的弧度
*/
private int mMinProgress = 5;
/**
* 过度动画的时间
*/
private static final int DURATION = 1000;
/**
* 过度动画
*/
private ValueAnimator valueAnimator;
/**
* 开始角度,在过度动画中使用
*/
private float mStartAngle = -90f;
public LoadingDrawable(CircleProgressSuperInfo info) {
super(info);
}
/**
* 设置进度
*/
public void setProgress(float progress) {
this.mProgress = progress;
}
/**
* 获取进度条的颜色
*/
public int getProgressColor() {
return this.mProgressColor;
}
/**
* 设置进度条的颜色
*/
public void setProgressColor(int color) {
this.mProgressColor = color;
}
/**
* 设置进度条的颜色
*/
public void setDrawProgressColor(int color) {
this.mDrawProgressColor = color;
}
public int getDrawProgressColor() {
return mDrawProgressColor;
}
public int getDrawBorderColor() {
return mDrawBorderColor;
}
public void setDrawBorderColor(int mDrawBorderColor) {
this.mDrawBorderColor = mDrawBorderColor;
}
/**
* 是否是过度模式
*/
public boolean isIntermediateMode() {
return mIsIntermediateMode;
}
/**
* 设置绘制区域
*/
@Override
public void setRadius(float radius) {
super.setRadius(radius);
// 计算要绘制的区域
mRectF.set(mWidth / 2 - mRadius + mBorderWidth / 2, mHeight / 2 - mRadius + mBorderWidth / 2,
mWidth / 2 + mRadius - mBorderWidth / 2, mHeight / 2 + mRadius - mBorderWidth / 2);
}
/**
* 设置loading模式
*/
public void setIntermediateMode(boolean intermediateMode) {
if (mIsIntermediateMode != intermediateMode) {
this.mIsIntermediateMode = intermediateMode;
// 取消动画
if (!mIsIntermediateMode) {
valueAnimator.cancel();
} else {
//这里要开启动画
startIntermediateAnim();
}
}
}
@Override
public void drawSelf(Canvas canvas) {
// 是否要显示loading状态
if (mIsIntermediateMode) {
startIntermediateAnim();
drawIntermediateProgress(canvas);
}
// 绘制进度条
else {
drawProgress(canvas);
}
}
/**
* 绘制过度进度条
*/
private void drawIntermediateProgress(Canvas canvas) {
// 首先画出背景圆
mPaint.setColor(mBgColor);
mPaint.setStyle(Paint.Style.FILL);
// 这里减去了边框的宽度
canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius - mBorderWidth, mPaint);
// 画出进度条
mPaint.setColor(mDrawProgressColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mBorderWidth);
// 计算圆弧划过的角度
float angle = CIRCULAR / mMaxProgress * mProgress;
// 这里要画圆弧
canvas.drawArc(mRectF, mStartAngle, angle, false, mPaint);
// 画出另一部分的进度条
mPaint.setColor(mDrawBorderColor);
mPaint.setStrokeWidth(mBorderWidth);
// 这里要画圆弧
canvas.drawArc(mRectF, mStartAngle + angle, CIRCULAR - angle, false, mPaint);
}
/**
* 绘制进度条
*/
private void drawProgress(Canvas canvas) {
// 开始画进度条
// 首先画出背景圆
mPaint.setColor(mBgColor);
mPaint.setStyle(Paint.Style.FILL);
// 这里减去了边框的宽度
canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius - mBorderWidth, mPaint);
// 画出进度条
mPaint.setColor(mDrawProgressColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mBorderWidth);
// 计算圆弧划过的角度
float angle = CIRCULAR / mMaxProgress * mProgress;
// 这里要画圆弧
canvas.drawArc(mRectF, -90, angle, false, mPaint);
// 画出另一部分的进度条
mPaint.setColor(mBorderColor);
mPaint.setStrokeWidth(mBorderWidth);
// 这里要画圆弧
canvas.drawArc(mRectF, -90 + angle, CIRCULAR - angle, false, mPaint);
}
/**
* 开始过度动画
*/
private synchronized void startIntermediateAnim() {
if (valueAnimator != null && valueAnimator.isStarted()) {
return;
}
if (valueAnimator == null) {
valueAnimator = new ValueAnimator().ofFloat(mMinProgress, mMaxProgress - mMinProgress);
valueAnimator.setDuration(DURATION);
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float value = (float) valueAnimator.getAnimatedValue();
setProgress(value);
mStartAngle += 2;
invalidateSelf();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
}
@Override
public void onAnimationCancel(Animator animator) {
// 充值旋转的角度
mStartAngle = -90;
}
@Override
public void onAnimationRepeat(Animator animator) {
// 互换颜色和位置
mStartAngle = mStartAngle - CIRCULAR / mMaxProgress * mMinProgress;
int color = getDrawProgressColor();
setDrawProgressColor(getDrawBorderColor());
setDrawBorderColor(color);
}
});
}
// 开始动画的时候,要重新设置颜色,否则颜色可能会错乱,因为在动画的过程已经互换
setDrawProgressColor(mProgressColor);
setDrawBorderColor(mBorderColor);
valueAnimator.setRepeatCount(-1);
valueAnimator.start();
}
/**
* 停止动画
*/
private void stopAnim() {
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.cancel();
}
}
@Override
public void release() {
stopAnim();
}
}
几乎是没有什么变化,增加了开始动画和结束动画方法,这样其他的状态时,可以节省系统资源。
然后是工厂类:
/**
* Created by li.zhipeng on 2017/7/13.
*
* 生产不同状态的Drawable的生产类
*/
public class StatusDrawableFactory {
private static StatusDrawableFactory mInstance;
public synchronized static StatusDrawableFactory getInstance() {
if (mInstance == null) {
mInstance = new StatusDrawableFactory();
}
return mInstance;
}
/**
* 返回指定状态的drawable
* */
public BaseStatusDrawable getDrawable(int status, CircleProgressSuperInfo info) {
BaseStatusDrawable drawable = null;
switch (status) {
case Status.NORMAL:
drawable = new NormalDrawable(info);
break;
case Status.LOADING:
drawable = new LoadingDrawable(info);
break;
case Status.COMPLETE:
drawable = new CompleteDrawable(info);
break;
case Status.ERROR:
drawable = new ErrorDrawable(info);
break;
}
return drawable;
}
}
非常简单的单例模式,返回指定的BaseStatusDrawable类型。
最后就是CircleProgressSuperBar:
/**
* Created by li.zhipeng on 2017/7/12.
*
* 具有多状态的CircleProgressBar,整合前两个控件的效果
*/
public class CircleProgressSuperBar extends View {
/**
* 保存四张状态的Drawable
*/
private BaseStatusDrawable[] drawables = new BaseStatusDrawable[4];
/**
* 测试就只要一个xml的构造方法就足够了
*/
public CircleProgressSuperBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// 初始化信息类
drawables[Status.NORMAL] = StatusDrawableFactory.getInstance().getDrawable(Status.NORMAL,
new CircleProgressSuperInfo(Color.parseColor("#3399ff"), Color.parseColor("#3399ff"), 10));
drawables[Status.LOADING] = StatusDrawableFactory.getInstance().getDrawable(Status.LOADING,
new CircleProgressSuperInfo(Color.parseColor("#ff0000"), Color.parseColor("#000000"), 10));
drawables[Status.COMPLETE] = StatusDrawableFactory.getInstance().getDrawable(Status.COMPLETE,
new CircleProgressSuperInfo(Color.parseColor("#ffcc00"), Color.parseColor("#ffcc00"), 10));
drawables[Status.ERROR] = StatusDrawableFactory.getInstance().getDrawable(Status.ERROR,
new CircleProgressSuperInfo(Color.parseColor("#ff3300"), Color.parseColor("#ff3300"), 10));
drawables[Status.NORMAL].setText("Normal");
drawables[Status.ERROR].setText("Error");
drawables[Status.COMPLETE].setText("Complete");
drawables[Status.NORMAL].setTextSize(42);
drawables[Status.ERROR].setTextSize(42);
drawables[Status.COMPLETE].setTextSize(42);
drawables[Status.LOADING].setBorderWidth(20);
// 设置重绘回调
drawables[Status.LOADING].setCallback(this);
}
/**
* 动画时长
*/
private int mDuration = 500;
/**
* 目前的状态t
*/
private int mCurrentStatus = Status.NORMAL;
/**
* 是否正在动画中
*/
private boolean isAnim;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 这里设置一些初始值
int width = getMeasuredWidth();
int height = getMeasuredHeight();
drawables[Status.NORMAL].setWidth(width);
drawables[Status.NORMAL].setHeight(height);
drawables[Status.LOADING].setWidth(width);
drawables[Status.LOADING].setHeight(height);
drawables[Status.ERROR].setWidth(width);
drawables[Status.ERROR].setHeight(height);
drawables[Status.COMPLETE].setWidth(width);
drawables[Status.COMPLETE].setHeight(height);
// 设置的Radius
int radius = width > height ? height / 2 : width / 2;
drawables[Status.NORMAL].setMinSize(radius * 2);
drawables[Status.LOADING].setMinSize(radius * 2);
drawables[Status.ERROR].setMinSize(radius * 2);
drawables[Status.COMPLETE].setMinSize(radius * 2);
drawables[Status.LOADING].getInfo().setRadius(radius);
drawables[Status.LOADING].getInfo().setPadding(width > height ? (width - radius * 2) / 2 : (height - radius * 2) / 2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画出不同状态的内容
drawables[mCurrentStatus].draw(canvas);
}
/**
* 设置状态
*/
public void setStatus(int status) {
if (mCurrentStatus != status && !isAnim) {
// 这里设置动画效果
changeStatus(mCurrentStatus, status);
// 释放之前的动画
this.drawables[mCurrentStatus].release();
this.mCurrentStatus = status;
this.drawables[mCurrentStatus].setIsAnim(true);
}
}
/**
* 状态改变的动画
*/
private void changeStatus(int fromStatus, int toStatus) {
isAnim = true;
// 取出相关的动画信息
CircleProgressSuperInfo fromStatusInfo = drawables[fromStatus].getInfo();
CircleProgressSuperInfo toStatusInfo = drawables[toStatus].getInfo();
// 开始动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(mDuration);
animatorSet.playTogether(AnimUtil.getColorAnim(fromStatusInfo.getBgColor(), toStatusInfo.getBgColor(), mDuration, colorUpdateListener),
AnimUtil.getColorAnim(fromStatusInfo.getBorderColor(), toStatusInfo.getBorderColor(), mDuration, borderColorUpdateListener),
AnimUtil.getRadiusAnim(fromStatusInfo.getRadius(), toStatusInfo.getRadius(), mDuration, radiusUpdateListener),
AnimUtil.getShapeAnim(fromStatusInfo.getPadding(), toStatusInfo.getPadding(), mDuration, shapeUpdateListener)
);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
isAnim = false;
drawables[mCurrentStatus].setIsAnim(false);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animatorSet.start();
}
/**
* color动画的回调
*/
private ValueAnimator.AnimatorUpdateListener colorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
drawables[mCurrentStatus].setBgColor((Integer) valueAnimator.getAnimatedValue());
invalidate();
}
};
/**
* borderColor动画的回调
*/
private ValueAnimator.AnimatorUpdateListener borderColorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
drawables[mCurrentStatus].setBorderColor((Integer) valueAnimator.getAnimatedValue());
}
};
/**
* radius动画的回调
*/
private ValueAnimator.AnimatorUpdateListener radiusUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
drawables[mCurrentStatus].setRadius((float) valueAnimator.getAnimatedValue());
}
};
/**
* shape动画的回调
*/
private ValueAnimator.AnimatorUpdateListener shapeUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
drawables[mCurrentStatus].setPadding((Float) valueAnimator.getAnimatedValue());
}
};
/**
* Invalidates the specified Drawable.
*
* @param drawable the drawable to invalidate
*/
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
invalidate();
}
}
主要是找到各个状态的BaseStatusDrawable,然后取出信息,开始属性动画,但是有几个小知识点,你还记得吗?
1、在获取指定状态的图片,直接使用Status.xxx作为数组的索引,保存和取出的速度都很快,是不是想起之前我们聊过的哈希表了?
2、onMeasure方法里,记得使用getMeasuredXXX,因为getWidth和getHeight都是0,千万别忘了。
有些朋友发现了:怎么突然冒出来一个invalidateDrawable()方法?我这里直说了,大家想自己去踩坑的可以试试:
还记得之前说过的继承Drawable是为了重绘吗,如果是你只是调用了Drawable.invalidateSelf(),很遗憾的告诉你,是不可能重绘的,所以这里要重写这个方法,强制重绘。
直接看源码就知道原因了:
//Drawable的重绘方法,实际上是调用了callback,这样就和View解耦了
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
// view本身就实现了Callback
public class View implements Drawable.Callback{
// 请注意里面的判断
@Override
public void invalidateDrawable(@NonNull Drawable drawable) {
// 满足了这个条件,才会重绘,所以要看看判断条件是什么
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getDirtyBounds();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
rebuildOutline();
}
}
@CallSuper
protected boolean verifyDrawable(@NonNull Drawable who) {
// 这里就是判断,view要判断是否使用了这个Drawable,如果没有使用,就不去重绘了,这个理论都是可以理解的。
return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);
}
}
因为我们仅仅是canvas绘图,并没有设置背景或者是前景图片之类的东西,自然就无法重绘,也就是重写invalidateDrawable()方法的原因。
还有两个类,没有贴出来:Status(Drawable的状态),AnimUtil(动画工具类),因为感觉今天的内容已经很长了,所以就省略了把,大家可以在demo中去查看。
看的说的挺溜,其实在写的时候还是出现了很多问题的,而且现在也还存在一些小问题,如果你发现博客中的代码和demo中有一点点区别,那就是我后来又修改了,但是主要思想是不会变了,大家可以自己去设置自定义属性,这样我们的完成度就更完美了。
github地址