今日头条的下拉刷新动画在下拉过程中有一个动态绘制刷新图的效果:
基本思路
按照最终呈现的图构造出完整的Path
路径,根据传入的进度percent
,动态截取新的Path
,并且每次截取都重绘新的Path
,从而实现动态绘制效果。
具体实现
将效果图自定义为一个Drawable
:PullDrawable
。
1.初始化画笔
private void iniPaint() {
// 外部边框画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0xffcccccc);
mPaint.setStrokeWidth(4f);
// 内部矩形和线条画笔
mInnerPaint = new Paint();
mInnerPaint.setAntiAlias(true);
mInnerPaint.setStyle(Paint.Style.STROKE);
mInnerPaint.setColor(0xffcccccc);
mInnerPaint.setStrokeWidth(6f);
}
2.初始化Path
根据Drawable
的宽w
和高h
设置合适的外边框圆角半径r
和内边距p
,然后根据w
、h
、r
、p
构造出最终图的完整路径:mBorderPath
、mRectPath
、mLine1Path
、mLine2Path
、mLine3Path
、mLine4Path
、mLine5Path
、mLine6Path
。
mBorderPathMeasure
、mRectPathMeasure
、mLine1PathMeasure
、mLine2PathMeasure
、mLine3PathMeasure
、mLine4PathMeasure
、mLine5PathMeasure
、mLine6PathMeasure
为路径测量类,用于测量完整路径的长度和切取制定长度的路径。
mBorderDstPath
、mRectDstPath
、mLine1DstPath
、mLine2DstPath
、mLine3DstPath
、mLine4DstPath
、mLine5DstPath
、mLine6DstPath
为从完整路径中截取后的路径,用于重绘。
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
initPath(bounds);
}
private void initPath(Rect bounds) {
int w = bounds.width();
int h = bounds.height();
int r = (int) (w * 2 / 15f); // 圆角半径
int p = (int) (r / 3f); // 内边距
// 外边框(圆角矩形)
mBorderPath = new Path();
mBorderDstPath = new Path();
mBorderPath.moveTo(w - p, p + r);
RectF rectFRightTop = new RectF(w - p - 2 * r, p, w - p, 2 * r + p);
mBorderPath.arcTo(rectFRightTop, 0, -90);
mBorderPath.lineTo(p + r, p);
RectF rectFLeftTop = new RectF(p, p, p + 2 * r, p + 2 * r);
mBorderPath.arcTo(rectFLeftTop, -90, -90);
mBorderPath.lineTo(p, h - p - r);
RectF rectFLeftBottom = new RectF(p, h - p - 2 * r, p + 2 * r, h - p);
mBorderPath.arcTo(rectFLeftBottom, -180, -90);
mBorderPath.lineTo(w - p - r, h - p);
RectF rectFRightBottom = new RectF(w - p - 2 * r, h - p - 2 * r, w - p, h - p);
mBorderPath.arcTo(rectFRightBottom, -270, -90);
mBorderPath.lineTo(w - p, p + r);
mBorderPathMeasure = new PathMeasure(mBorderPath, true);
////////////////////////////////////////////////////////////////////////////////////////////
float d = p; // 内部矩形与水平方向上的三条横线的距离
float xp = 0.8f * r; // 水平方向相对于外边框的边距
float yp = 1.2f * r; // 竖直方向相对于外边框的边距
// 内部矩形
mRectPath = new Path();
mRectDstPath = new Path();
mRectPath.moveTo(w / 2f - d / 2f, p + yp);
mRectPath.lineTo(p + xp, p + yp);
mRectPath.lineTo(p + xp, (h - 2 * p - 2 * yp) * 0.4f + yp + p);
mRectPath.lineTo(w / 2f - d / 2f, (h - 2 * p - 2 * yp) * 0.4f + yp + p);
mRectPath.lineTo(w / 2f - d / 2f, p + yp);
mRectPathMeasure = new PathMeasure(mRectPath, true);
// 第1根线条
mLine1Path = new Path();
mLine1DstPath = new Path();
mLine1Path.moveTo(w / 2f + d / 2f, p + yp);
mLine1Path.lineTo(w - p - xp, p + yp);
mLine1PathMeasure = new PathMeasure(mLine1Path, false);
// 第2根线条
mLine2Path = new Path();
mLine2DstPath = new Path();
mLine2Path.moveTo(w / 2f + d / 2f, (h - 2 * p - 2 * yp) * 0.2f + yp + p);
mLine2Path.lineTo(w - p - xp, (h - 2 * p - 2 * yp) * 0.2f + yp + p);
mLine2PathMeasure = new PathMeasure(mLine2Path, false);
// 第3根线条
mLine3Path = new Path();
mLine3DstPath = new Path();
mLine3Path.moveTo(w / 2f + d / 2f, (h - 2 * p - 2 * yp) * 0.4f + yp + p);
mLine3Path.lineTo(w - p - xp, (h - 2 * p - 2 * yp) * 0.4f + yp + p);
mLine3PathMeasure = new PathMeasure(mLine3Path, false);
// 第4根线条
mLine4Path = new Path();
mLine4DstPath = new Path();
mLine4Path.moveTo(p + xp, (h - 2 * p - 2 * yp) * 0.6f + yp + p);
mLine4Path.lineTo(w - p - xp, (h - 2 * p - 2 * yp) * 0.6f + yp + p);
mLine4PathMeasure = new PathMeasure(mLine4Path, false);
// 第5根线条
mLine5Path = new Path();
mLine5DstPath = new Path();
mLine5Path.moveTo(p + xp, (h - 2 * p - 2 * yp) * 0.8f + yp + p);
mLine5Path.lineTo(w - p - xp, (h - 2 * p - 2 * yp) * 0.8f + yp + p);
mLine5PathMeasure = new PathMeasure(mLine5Path, false);
// 第6根线条
mLine6Path = new Path();
mLine6DstPath = new Path();
mLine6Path.moveTo(p + xp, (h - 2 * p - 2 * yp) * 1f + yp + p);
mLine6Path.lineTo(w - p - xp, (h - 2 * p - 2 * yp) * 1f + yp + p);
mLine6PathMeasure = new PathMeasure(mLine6Path, false);
}
3.根据进度百分比截取路径
由于外部边框和内部内容是同时绘制,外部边框在percent
从0到1过程中不断截取,而内部内容则是分阶段(内部矩形和6条线分为7个阶段)进行的截取,我们只需要计算出阶段的临界点(pRect
、pLine1
、pLine2
、pLine3
、pLine4
、pLine5
、pLine6
),然后就可以根据百分比和临界点来计算需要截取的长度进行截取。
public void update(float percent) {
// 每次更新前重置
mBorderDstPath.reset();
// 截取制定百分比percent长度的路径,截取后的路径保存到mBorderDstPath中
mBorderPathMeasure.getSegment(0, mBorderPathMeasure.getLength() * percent, mBorderDstPath, true);
// 完整路径长度
float rectLength = mRectPathMeasure.getLength();
float line1Length = mLine1PathMeasure.getLength();
float line2Length = mLine2PathMeasure.getLength();
float line3Length = mLine3PathMeasure.getLength();
float line4Length = mLine4PathMeasure.getLength();
float line5Length = mLine5PathMeasure.getLength();
float line6Length = mLine6PathMeasure.getLength();
float totalLength = rectLength
+ line1Length
+ line2Length
+ line3Length
+ line4Length
+ line5Length
+ line6Length;
// 百分比临界点
float pRect = rectLength / totalLength;
float pLine1 = line1Length / totalLength + pRect;
float pLine2 = line2Length / totalLength + pLine1;
float pLine3 = line3Length / totalLength + pLine2;
float pLine4 = line4Length / totalLength + pLine3;
float pLine5 = line5Length / totalLength + pLine4;
float pLine6 = line6Length / totalLength + pLine5;
// 根据指定的百分比以及临界点切取路径
mRectDstPath.reset();
mRectPathMeasure.getSegment(0, rectLength * (percent / pRect), mRectDstPath, true);
mLine1DstPath.reset();
mLine1PathMeasure.getSegment(0, line1Length * ((percent - pRect) / (pLine1 - pRect)), mLine1DstPath, true);
mLine2DstPath.reset();
mLine2PathMeasure.getSegment(0, line2Length * ((percent - pLine1) / (pLine2 - pLine1)), mLine2DstPath, true);
mLine3DstPath.reset();
mLine3PathMeasure.getSegment(0, line3Length * ((percent - pLine2) / (pLine3 - pLine2)), mLine3DstPath, true);
mLine4DstPath.reset();
mLine4PathMeasure.getSegment(0, line4Length * ((percent - pLine3) / (pLine4 - pLine3)), mLine4DstPath, true);
mLine5DstPath.reset();
mLine5PathMeasure.getSegment(0, line5Length * ((percent - pLine4) / (pLine5 - pLine4)), mLine5DstPath, true);
mLine6DstPath.reset();
mLine6PathMeasure.getSegment(0, line6Length * ((percent - pLine5) / (pLine6 - pLine5)), mLine6DstPath, true);
// 重绘
invalidateSelf();
}
4.重绘截取后的路径
@Override
public void draw(Canvas canvas) {
canvas.drawPath(mBorderDstPath, mPaint);
canvas.drawPath(mRectDstPath, mInnerPaint);
canvas.drawPath(mLine1DstPath, mInnerPaint);
canvas.drawPath(mLine2DstPath, mInnerPaint);
canvas.drawPath(mLine3DstPath, mInnerPaint);
canvas.drawPath(mLine4DstPath, mInnerPaint);
canvas.drawPath(mLine5DstPath, mInnerPaint);
canvas.drawPath(mLine6DstPath, mInnerPaint);
}
5.添加一个清除图像的方法
外部调用该方法,可以清除掉当前画面。
public void clear() {
mBorderDstPath.reset();
mRectDstPath.reset();
mLine1DstPath.reset();
mLine2DstPath.reset();
mLine3DstPath.reset();
mLine4DstPath.reset();
mLine5DstPath.reset();
mLine6DstPath.reset();
invalidateSelf();
}
在Activity中调用
mImageView = (ImageView) findViewById(R.id.image_view);
mPullDrawable = new PullDrawable();
mImageView.setImageDrawable(mPullDrawable);
1.通过动画不断从0到1改变percent
实现动态绘制
private void startPullAnim() {
if (mPullValueAnimator == null) {
mPullValueAnimator = ValueAnimator.ofFloat(0, 1);
mPullValueAnimator.setInterpolator(new LinearInterpolator());
mPullValueAnimator.setDuration(4000);
mPullValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
mPullDrawable.update(value);
}
});
} else {
clear();
}
mPullValueAnimator.start();
}
private void clear() {
if (mPullValueAnimator != null && mPullValueAnimator.isRunning()) {
mPullValueAnimator.cancel();
}
mPullDrawable.clear();
}
2.通过SeekBar拖动进度来重绘
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mPullDrawable.update(progress / 100f);
}
最后
绘制逻辑比较简单,只是计算比例和截取长度稍显繁琐。主要的运用到的知识点就是Path
的构建和PathMeasure
截取。另外,PathMeasure
还可以通过getPosTan(float distance, float pos[], float tan[])
获取指定距离的Path的位置和正切值,通过这个位置和正切值可以实现很多跟随指定路径运动的动画效果,可以参考这篇文章的详细介绍PathMeasure之迷径追踪。