用Path动态绘制Drawable

今日头条的下拉刷新动画在下拉过程中有一个动态绘制刷新图的效果:


动态绘制过程.gif
基本思路

按照最终呈现的图构造出完整的Path路径,根据传入的进度percent,动态截取新的Path,并且每次截取都重绘新的Path,从而实现动态绘制效果。

具体实现

将效果图自定义为一个DrawablePullDrawable

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,然后根据whrp构造出最终图的完整路径:mBorderPathmRectPathmLine1PathmLine2PathmLine3PathmLine4PathmLine5PathmLine6Path
mBorderPathMeasuremRectPathMeasuremLine1PathMeasuremLine2PathMeasuremLine3PathMeasuremLine4PathMeasuremLine5PathMeasuremLine6PathMeasure为路径测量类,用于测量完整路径的长度和切取制定长度的路径。
mBorderDstPathmRectDstPathmLine1DstPathmLine2DstPathmLine3DstPathmLine4DstPathmLine5DstPathmLine6DstPath 为从完整路径中截取后的路径,用于重绘。

@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个阶段)进行的截取,我们只需要计算出阶段的临界点(pRectpLine1pLine2pLine3pLine4pLine5pLine6),然后就可以根据百分比和临界点来计算需要截取的长度进行截取。

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之迷径追踪

源码:https://github.com/xiaoyanger0825/HeadLinesPull

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,478评论 25 707
  • 这次不是十分钟动画,今天要分享的是PathMeasure的玩法。 首先我们来回顾一下童年吧~~90后满满的记忆 小...
    Anonymous___阅读 7,317评论 47 146
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,357评论 0 17
  • 今天九月初九,重阳节。登高纪念仙人的日子,也是我家母亲的生日。妈妈今年63岁了,但在她脸上完全找不到63岁的身影,...
    Candy213阅读 178评论 0 0
  • 梦做多了,会觉得,现实才是梦。弗洛伊德说梦是人们潜意识里对欲望的满足。可是接下来这个梦我却不知道该怎么解释。第一次...
    往川往川阅读 561评论 0 1