前段时间,看了下path的动画部分。看完之后,就过去了。最近刚好抽点时间把之前看的内容整理下,就当做是温故而知新了。

不过,整理的过程中也确实学习到了新的东西。还是有收获的哈。

希望每一个爱学习的同学都把自己平时学习的心得体会写出来,避免自己以后犯错,也为他人谋福利。

效果图:

image

介绍

Path其实是android中很常见的一个类。它方便让用户自己先确定好一个轨迹后,方便在Canvas上面画出来。其实就是所谓矢量图形。比如我们常见的svg图片其实就是一层一层的Path轨迹排列组合出来的。想了解svg使用的可以参考svg-android.关于Path就不多说了。自行Google。

如何在Path上面运动。

其实也不是什么神秘的事情,Android提供了PathMeasure这个类。可以通过它来获取到整个Path的长度。然后我们可以通过它来获取某一段长度的path。然后使用animator不断获取改变区间,并把这个path不断画出来即可以动起来了。

使用这个API:

/**
 * Given a start and stop distance, return in dst the intervening
 * segment(s). If the segment is zero-length, return false, else return
 * true. startD and stopD are pinned to legal values (0..getLength()).
 * If startD <= stopD then return false (and leave dst untouched).
 * Begin the segment with a moveTo if startWithMoveTo is true.
 *
 * <p>On {@link android.os.Build.VERSION_CODES#KITKAT} and earlier
 * releases, the resulting path may not display on a hardware-accelerated
 * Canvas. A simple workaround is to add a single operation to this path,
 * such as <code>dst.rLineTo(0, 0)</code>.</p>
 */
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
    dst.isSimplePath = false;
    return native_getSegment(native_instance, startD, stopD, dst.ni(), startWithMoveTo);
}

通过这个api,我们可以获取到startD到stopD这一段的path。这样就可以实现截取一小段的问题。

其实要想让整个动画顺畅起来,用这个api是不能简单达到的。比如你想实现0.9到0.1这一段距离,你不能简单的使用 startD = 0.9f * len, stopD = 0.1f * len ,结果就是它只会显示0.9到1.0这一段。那么问题就来了,怎样才能显示出来0.9到1.0呢。其实思路很简单,它不允许超过1.0的话,我可以把它拆成两端嘛:0.9~1.00.0~0.1。然后把这两段合并即可。同样的,比如我想实现-0.1到1.0这一段的话,也可以用这种思路,不过需要指出的时候PathMeasure只认正数。下面是我的解决方案:

public static void setSegment(PathMeasure pm, Path p, float start, float end) {
    final float totalLen = pm.getLength();
    float len = end - start;
    // 长度超过1没有意义
    if (Math.abs(len) > 1) {
        len = len > 0 ? 1 : -1;
    }
    // 起始点在-1和1之间
    while (Math.abs(start) > 1) {
        //变成(-1,1)
        start = start + (start > 0 ? -1 : 1);
    }
    end = start + len;
    //
    start = Math.min(start, end);
    end = start + Math.abs(len);
    //
    if (start < 0) {
        if (end < 0) {
            pm.getSegment((1 + start) * totalLen, (1 + end) * totalLen, p, true);
            return;
        }
        pm.getSegment((1 + start) * totalLen, totalLen, p, true);
        start = 0;
    }
    if (end > 1) {
        pm.getSegment(0, (end - 1) * totalLen, p, true);
        end = 1;
    }
    pm.getSegment(start * totalLen, end * totalLen, p, true);
}

但是,path还有一个问题就是,一个path可以包含N个闭合路径。默认的操作都是在第一个闭合路径上面进行的。于是就有了下面的代码:

public List<Pair<Path, PathMeasure>> extract() {
    if (mPathMeasure == null || mRawPath == null) {
        return null;
    }
    mList.clear();
    do {
        final float len = mPathMeasure.getLength();
        Path path = new Path();
        mPathMeasure.getSegment(0, len, path, true);
        path.close();
        //
        mList.add(new Pair<>(path, new PathMeasure(path, true)));
    } while (mPathMeasure.nextContour());
    return mList;
}

这段代码的主要作用是把一个Path分解成N个闭合路径,每一个路径生成一个单独的只有一条路径的path.

这样我们就可以在一个复杂的path上面将每一个闭合路径都显示出来了。当然了,如果需要动起来的话,我们还要有一个Animator。如下:

public Animator start() {
    if (mAnimator != null) {
        mAnimator.cancel();
    }
    //
    final float lastProgress = (mProgress > 0 && mProgress < 1) ? mProgress : 0;
    //
    final ValueAnimator animator = ValueAnimator.ofFloat(0, 1.0f);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            final Object value = animation.getAnimatedValue();
            if (value instanceof Float) {
                mProgress = ((Float) value).floatValue();
                invalidateSelf();
            }
        }
    });
    animator.setInterpolator(new TimeInterpolator() {
        @Override
        public float getInterpolation(float input) {
            input += lastProgress;
            if (input > 1) {
                input = input - 1;
            }
            return (mInterpolator != null) ? mInterpolator.getInterpolation(input) : input;
        }
    });
    animator.setRepeatMode(ValueAnimator.RESTART);
    animator.setRepeatCount(-1);
    animator.setDuration(500);
    animator.start();
    //
    return mAnimator = animator;
}

给ImageView设定一个VividDrawable后,可以实现在VividDrawable中通过Animator来实现不断刷新自己从而让动画跑起来。但是在我自己写的VividView中自己draw这个VividDrawable就不能实现动画。

于是,我便怀疑起了人生。

我看了下IamgeView里面关于setImageDrawable的源码。如下:

public void setImageDrawable(@Nullable Drawable drawable) {
    if (mDrawable != drawable) {
        mResource = 0;
        mUri = null;

        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        updateDrawable(drawable);

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }
}

可以看到,当drawable传递过来的时候会执行了一个关键的updateDrawable(drawable). 如下:

private void updateDrawable(Drawable d) {
    if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
        mRecycleableBitmapDrawable.setBitmap(null);
    }

    if (mDrawable != null) {
        mDrawable.setCallback(null);
        unscheduleDrawable(mDrawable);
    }

    mDrawable = d;

    if (d != null) {
        d.setCallback(this);
        d.setLayoutDirection(getLayoutDirection());
        if (d.isStateful()) {
            d.setState(getDrawableState());
        }
        d.setVisible(getVisibility() == VISIBLE, true);
        d.setLevel(mLevel);
        mDrawableWidth = d.getIntrinsicWidth();
        mDrawableHeight = d.getIntrinsicHeight();
        applyImageTint();
        applyColorMod();

        configureBounds();
    } else {
        mDrawableWidth = mDrawableHeight = -1;
    }
}

这时候可能还是不知道为啥。为什么ImageView就可以。此时,我们回到最初产生问题的地方。也就是Drawable里面的invalidateSelf(). 源码如下:

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

有没有发现invalidateSelf()之后,会通过一个CallBack调用callback.invalidateDrawable(this).此时就可以updateDrawable对应起来了,从它的源码可以看到它会把传过来的有效的drawable通过setCallBackCallBack设置到Drawable里面。

在我以为雨过天晴,一切都将要顺风顺水的时候。我重新编译运行后发现,依然如故。就像一杯老酒,给人的挫败依然浓烈。不过,我们再看一下CallBack的实现的地方(View继承了那个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();
    }
}

在这里我们就看到了熟悉的invalidate,啊哈哈哈。也就是说如果没有刷新界面,那就是invalidate没有执行到。所以问题就出在了verifyDrawable(drawable)这个地方,源码如下:

protected boolean verifyDrawable(Drawable who) {
    return who == mBackground || (mScrollCache != null && mScrollCache.scrollBar == who)
            || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);
}

这里其实就是校验了一下,传入的drawable是不是有效的。因此解决方案就是让这里返回true即可,但是未免也太简单粗暴了点。下面是我的解决方案:

@Override
protected boolean verifyDrawable(Drawable who) {
    // when Drawable.invalidateSelf is invoked, view need to check if the drawable is valid, make it valid
    return who == mVividDrawable || super.verifyDrawable(who);
}

总结

有坑不怕,来来来,我们看源码。

By @hyongbai 共7207个字

本文链接 http://yourbay.me/all-about-tech/2016/04/26/vivid-path-vivid-view/