flutter 自定义绘制_自定义可绘制

flutter 自定义绘制

I love our new designs! Recently I’ve been working on user interactions. One of them is presented on the GIF above. I wanted to create a custom Drawable, so I can set a background on any view I want without wrapping it with some custom ViewGroup. An extra layer means extra calculations.

我喜欢我们的新设计! 最近,我一直在进行用户交互。 其中一个在上面的GIF上介绍。 我想创建一个自定义Drawable,因此我可以在所需的任何视图上设置背景,而无需将其与某些自定义ViewGroup进行包装。 额外的层意味着额外的计算。

I couldn’t find any guide nor article that would help me create a Drawable object that I would be able to set as a background via XML. Like a Ripple Drawable. Unfortunately if someone counts that in this article I solved that mistery I’ll heads up a little — I didn’t. I looked though how RippleDrawable is defined and I prepared my Drawable classes based on what I saw.

我找不到任何指南和文章来帮助我创建一个Drawable对象,该对象可以通过XML设置为背景。 就像涟漪可绘制。 不幸的是,如果有人认为我在这篇文章中解决了我的困惑,我会抬起头,但我没有。 我查看了RippleDrawable的定义方式,并根据所见内容准备了Drawable类。

分而治之 (Divide and Counquer)

All advanced animations look complicated until we find a way to disassemble them. What animations are present on this GIF?

所有高级动画看上去都很复杂,直到我们找到一种将其分解的方法为止。 此GIF上有哪些动画?

Image for post

There are three:

有三种:

  1. Rounded Rectangle Bounce

    圆角矩形弹跳
  2. Alpha Mask Appearance

    阿尔法面膜外观
  3. Long-Click Filling Color

    长按填充色
Image for post
Image for post
Image for post

How a Ripple knows when animate? That was my first question. When I think about a View it comes to my mind onClickListener or onTouchEvent. Drawable doesn’t have such methods. It has a method named onStateChanged like this:

涟漪如何知道何时设置动画? 那是我的第一个问题。 当我想到视图时,我想到的是onClickListener或onTouchEvent。 Drawable没有这种方法。 它具有一个名为onStateChanged的方法,如下所示:

…
@Override
protected boolean onStateChange(int[] stateSet) {
    final boolean changed = super.onStateChange(stateSet);


    boolean enabled = false;
    boolean pressed = false;
    boolean focused = false;
    boolean hovered = false;


    for (int state : stateSet) {
        if (state == R.attr.state_enabled) {
            enabled = true;
        } else if (state == R.attr.state_focused) {
            focused = true;
        } else if (state == R.attr.state_pressed) {
            pressed = true;
        } else if (state == R.attr.state_hovered) {
            hovered = true;
        }
    }


    setRippleActive(enabled && pressed);
    setBackgroundActive(hovered, focused, pressed);


    return changed;
}
…

圆角矩形弹跳 (Rounded Rectangle Bounce)

That was a starting point. I reflected this method in my class, removing the irrelevant states:

那是一个起点。 我在课堂上反映了此方法,删除了不相关的状态:

class ClickDrawable(layers: Array<out Drawable> = arrayOf()) : LayerDrawable(layers) {


    private var wasPressed = false
    private var mActive = false
    private var canStart = true
    private var mBounds = bounds


    override fun isStateful(): Boolean {
        return true
    }


    override fun onStateChange(state: IntArray?): Boolean {
        val stateSet = state?.toSet() ?: emptySet()


        var enabled = false
        var pressed = false


        for (s in stateSet) {
            when (s) {
                android.R.attr.state_enabled -> enabled = true
                android.R.attr.state_pressed -> pressed = true
            }
        }


        setClickActive(enabled && wasPressed && !pressed)


        wasPressed = pressed
        return super.onStateChange(state)
    }


    private fun setClickActive(active: Boolean) {
        if (mActive != active) {
            mActive = active
            if (active && canStart) {
                canStart = false
                mBounds = copyBounds()
                startAnimation()
            }
        }
    }


    private fun setBounds(value: Float) {
        if (value == 1f) {
            canStart = true
        }
        val x = mBounds.width()
        val y = mBounds.height()
        val factorX = (x * value).roundToInt()
        val factorY = (y * value).roundToInt()
        bounds = mBounds.run {
            Rect(
                right - factorX,
                bottom - factorY,
                factorX,
                factorY
            )
        }
        invalidateSelf()
    }


    private fun startAnimation() {
        val anim = ObjectAnimator.ofFloat(this, "bounds", 0.99f, 0.95f, 1f)
        anim.duration = 225
        anim.interpolator = LinearInterpolator()
        anim.start()
    }
}

So first step is to override a method isStateful. Without that, the code from onStateChanged will never be triggered.Next, I wanted to start the animation on finger released. Otherwise long click would start with a bouncing Drawable. The condition to start was: is enabled and was pressed but is not pressed anymore.

因此,第一步是重写isStateful方法。 否则,onStateChanged的代码将永远不会被触发。接下来,我想松开手指开始动画。 否则,长按会从反弹的Drawable开始。 开始的条件是:已启用并被按下,但不再被按下。

setClickActive(enabled && wasPressed && !pressed)

To start an animation I used an ObjectAnimator and took advantage of float type by starting a value from 0.99 and finishing on 1. This cheats an eye enough to see a fluent movement and I get an end-value different from start-value which helps me differentiate if the anim is in run without extra flags.

要开始动画,我使用了ObjectAnimator并利用了float类型,即从0.99开始并在1上结束。这足以使眼睛看到流畅的运动,并且得到的最终值不同于开始值,这对我有帮助区分动画是否在运行中没有额外的标志。

阿尔法面膜外观 (Alpha Mask Appearance)

So the rectangle shrinks, but the animation also has a transparent mask showing on top of. The mask must be fully transparent at first and fade-in and out within the same time as previous animation.

因此,矩形会缩小,但动画的顶部也会显示一个透明蒙版。 蒙版首先必须完全透明,并且在与上一个动画相同的时间内淡入和淡出。

class ClickDrawable(layers: Array<out Drawable> = arrayOf()) : LayerDrawable(layers) {


    …
    private val mask: Drawable =
        ColorDrawable(0xFF00FFFF.toInt()).apply { alpha = 0 }


    init {
        addLayer(mask)
    }
    …


    @Keep
    private fun setMaskAlpha(alpha: Int) {
        mask.alpha = alpha
        invalidateSelf()
    }


    private fun colorMask(fromAlpha: Int, toAlpha: Int) {
        val anim = ObjectAnimator.ofInt(this, "maskAlpha", fromAlpha, toAlpha, fromAlpha)
        anim.duration = DURATION
        anim.interpolator = LinearInterpolator()
        anim.start()
    }


    private fun setClickActive(active: Boolean) {
        if (mActive != active) {
            mActive = active
            if (active && canStart) {
                …
                colorMask(0, 64)
            }
        }
    }


    @Keep
    private fun setBounds(value: Float) { … }
    …


    private companion object { 
        const val DURATION = 225L
    }
}

This code is not a final touch but the POC that I went out of. Anyone may feel free to do the same thing.Also, keep in mind that an ObjectAnimator uses reflection to trigger set methods, so always add a @Keep annotation, to prevent ProGuard from minifying them.

这段代码不是最终的决定,而是我淘汰的POC。 任何人都可以随意做同样的事情。此外,请记住,ObjectAnimator使用反射来触发设置方法,因此请始终添加@Keep注释,以防止ProGuard缩小它们。

使用自定义可绘制 (Using Custom Drawable)

As I mentioned in the beginning of the article I wasn’t able to add a background drawable in a layout resource file, so I had to do it programmatically like this:

正如我在文章开头提到的那样,我无法在布局资源文件中添加背景可绘制对象,因此我必须以编程方式进行如下操作:

Image for post

Cascade usage of drawables was possible only because of deriving from LayerDrawable class which is a well known <layer-list> node in drawable XMLs.

级联使用可绘制对象仅是因为它是从LayerDrawable类派生的,该类是可绘制XML中众所周知的<layer-list>节点。

长按填充色 (Long-Click Filling Color)

I have a nice bouncing animation now that I can add to any of my Views. There’s only one more animation left to be done. Filling a color on long click.

现在,我有一个不错的弹跳动画,可以将其添加到任何“视图”中。 只剩下一个动画需要完成。 长按即可填充颜色。

I know how to react to states of the Drawable. The only difference is the start of the animation. I wouldn’t like the color to start filling right after I press a View, because I would loose that experience that, these are in fact, two different actions. There’s an easy way though. Object animator has a property startDelay that I experimented with to get that natural distinction between a simple click and a long click. To me 100ms is enough:

我知道如何对Drawable的状态做出React。 唯一的区别是动画的开始。 我不希望在按下View后立即开始填充颜色,因为我会失去那种实际上是两个不同动作的体验。 有一个简单的方法。 对象动画师具有一个属性startDelay,我尝试过该属性以实现简单单击和长按之间的自然区别。 对我来说100ms就足够了:

class FillDrawable(private val color: Int, layers: Array<out Drawable>) : LayerDrawable(layers) {


    private val radius: Float = 20f


    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = this@FillDrawable.color
    }


    private var r = copyBounds()


    private var activeState = false


    override fun isStateful(): Boolean = true


    override fun onBoundsChange(bounds: Rect?) {
        super.onBoundsChange(bounds)
        val b = requireNotNull(bounds)
        r = Rect(b.left, b.top, b.left + (b.right * 0.1).toInt(), b.bottom)
    }


    override fun draw(canvas: Canvas) {
        super.draw(canvas)
        canvas.drawRoundRect(r.toRectF(), radius, radius, paint)
    }


    override fun onStateChange(state: IntArray?): Boolean {
        val stateSet = state?.toSet() ?: emptySet()


        var enabled = false
        var pressed = false
        for (s in stateSet) {
            when (s) {
                android.R.attr.state_enabled -> enabled = true
                android.R.attr.state_pressed -> pressed = true
            }
        }


        setActive(enabled && pressed)


        return super.onStateChange(state)
    }


    private fun setActive(active: Boolean) {
        if (activeState != active) {
            activeState = active
            if (active) {
                extendsWidth()
            } else {
                anim.cancel()
            }
        }
    }


    private fun setWidth(factor: Float) {
        r = Rect(
            bounds.left,
            bounds.top,
            (bounds.right * factor).toInt(),
            bounds.bottom
        )
        invalidateSelf()
    }


    private val anim = ObjectAnimator.ofFloat(this, "width", 0.1f, 1f)
    private fun extendsWidth() {
        anim.startDelay = 100
        anim.duration = 300
        anim.interpolator = LinearInterpolator()
        anim.start()
    }
}

结论 (Conclusion)

It’s possible and not that hard to create a custom Drawable. But is it worth it? Is it better than creating a custom ViewGroup? Every solution in programming has its pros and cons. It’s always our own decision which way we choose.

创建自定义Drawable可能并没有那么困难。 但是这值得吗? 比创建自定义ViewGroup好吗? 编程中的每个解决方案都有其优缺点。 选择哪种方式始终是我们自己的决定。

A custom Drawable:

自定义Drawable:

  • Saves calculations of positioning

    节省定位计算
  • Doesn’t require to change all XMLs by adding a surrounding ViewGroup

    不需要通过添加周围的ViewGroup来更改所有XML
  • Supports only limited number of states, that are already recognized by a View and passed to a Drawable

    仅支持有限数量的状态,这些状态已被视图识别并传递给Drawable

A custom ViewGroup:

自定义ViewGroup:

  • Custom ViewGroup on the other hand can be used in an XML and a background Drawable can be added there rather than from the code

    另一方面,可以在XML中使用自定义ViewGroup,并且可以在其中添加背景Drawable,而不是从代码中添加
  • With custom ViewGroup you have more control by overriding onTouchEvent whereas custom Drawable only reacts to states

    使用自定义ViewGroup,您可以通过覆盖onTouchEvent来获得更多控制权,而自定义Drawable仅对状态做出React

There must be a reason why Google doesn’t let us create a custom Drawable easily. There are no tutorials, no option to create a custom node like <ripple> has. Maybe it’s because it’s not the best choice? Or maybe soon Google give us a tool to do just that?

Google不允许我们轻易创建自定义Drawable一定是有原因的。 没有教程,也没有创建像<ripple>这样的自定义节点的选项。 也许是因为这不是最佳选择? 也许不久之后Google会给我们提供一种工具来做到这一点?

翻译自: https://medium.com/swlh/custom-drawable-25f56044e8dc

flutter 自定义绘制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值