jetpack_从自定义视图到Jetpack Compose

jetpack

Creating a Jetpack Compose equivalents of the “traditional” UI libraries for Android will be probably necessary if you want to keep up with new trends. In this article, you will learn how we did it and what new we learned from this process.

如果您想跟上新趋势,则可能有必要为Android创建Jetpack Compose等效于“传统” UI库的内容。 在本文中,您将学习我们如何做到的以及从此过程中学到了什么。

When you’re done reading the article, you will learn that

阅读完本文后,您将了解到

  • creating a new component for Jetpack Compose is easy, once you will understand a little bit about its paradigm

    一旦您对它的范例有了一点了解,就可以轻松为Jetpack Compose创建一个新组件
  • reusing some things from the original implementation is good but don’t be afraid to write some things from the scratch

    重用原始实现中的某些内容很好,但不要害怕从头开始编写一些内容
  • adding custom animations to components is not as difficult as it may seem

    向组件添加自定义动画并不像看起来那样困难

It is always hard to keep up with new technologies, especially in the software development field. You always need to be prepared for new challenges and can’t be afraid of trying new things. One of these challenges for Android developers in the foreseeable future will be Jetpack Compose and its new paradigm of developing UI. That is one of the reasons why our company decided to do a little research a try to re-implement one of our UI libraries to the Jetpack Compose version to test its current capabilities.

总是很难跟上新技术,尤其是在软件开发领域。 您始终需要为新挑战做好准备,并且不要害怕尝试新事物。 在可预见的未来,Android开发人员面临的挑战之一将是Jetpack Compose及其开发UI的新范例。 这就是我们公司决定进行一些研究,以尝试将我们的一个UI库重新实现为Jetpack Compose版本以测试其当前功能的原因之一。

原始图书馆 (The original library)

Project Donut is a library that creates doughnut-like chart views. It is highly customizable with many features and support for animations. Thus it was our great candidate for re-implementation from an “old” custom view to a “new” composable view.

项目 甜甜圈 是创建甜甜圈状图表视图的库。 它是高度可定制的,具有许多功能和对动画的支持。 因此,它是重新实现从“ ”自定义视图到“ ”可组合视图的绝佳候选人。

The original Donut implementation 原始的Donut实施

分析和可重用性 (Analysis and reusability)

The initial step of the new implementation was to analyze the original version of the library and find what can be reused in the new one. In simple terms, the original version is using the canvas from the overridden onDraw method in the custom view. The sections of the chart are calculated as paths based on the relatively simple algorithm. Then they are drawn onto a canvas. After a quick overview, we find out that from this implementation, we could take an algorithm, the way how the sections of the chart are drawn to the canvas, and partially also an interface of the view used by the developers using this library.

新实现的第一步是分析库的原始版本,并找到可以在新库中重用的库。 简单来说,原始版本使用自定义视图中重写的onDraw方法中的画布。 根据相对简单的算法,将图表的各个部分计算为路径。 然后将它们绘制到画布上。 快速浏览之后,我们发现从该实现中,我们可以采用一种算法,一种将图表的各部分绘制到画布上的方式,以及部分使用该库的开发人员所使用的视图的接口。

The algorithm is basically a calculation of how much of the whole donut should each section occupy. I don’t want to go into details because it is not very important for this article. However, I made some changes to the algorithm, and instead of the path, I’m calculating the start and sweep angle of each section, which seems to be a little bit simpler.

该算法基本上是对每个部分应占据整个甜甜圈多少的计算。 我不想详细介绍,因为这对于本文而言不是很重要。 但是,我对算法进行了一些更改,而不是通过路径,而是在计算每个部分的开始角度和后掠角度,这似乎更简单一些。

The canvas class, in the original implementation, is using only a drawPath method used to draw sections of the chart. Since we adapted our algorithm a little bit to calculate the start and sweep angle instead of the whole path, we are going to use the only drawArc method from the canvas. The Jetpack Compose provides it’s own custom version of the canvas that is similar to the canvas from the “old” way of drawing custom views.

在原始实现中,canvas类仅使用用于绘制图表各部分的drawPath方法。 由于我们稍微调整了算法来计算起始角和后掠角,而不是整个路径,因此我们将使用画布上唯一的drawArc方法。 Jetpack Compose提供了自己的画布自定义版本,类似于“ ”绘制自定义视图方式中的画布。

从XML到数据类 (From XML to Data Class)

Composable views provide a completely different paradigm of dealing with UI changes. From the interface of the original implementation, we have reused the names of the properties and replaced their implementation in the XML and the custom view with a one simple model class.

可组合视图为处理UI更改提供了完全不同的范例。 从原始实现的接口开始,我们重用了属性名称,并使用一个简单的模型类替换了XML和自定义视图中的属性实现。

The XML definitions, setters, getters and properties of the original implementation 原始实现的XML定义,设置器,获取器和属性

All these XML attributes, getters, setters, and properties were transformed into the following class.

所有这些XML属性,获取器,设置器和属性都转换为以下类。

@Model data class DonutModel(
    var cap: Float,
    var masterProgress: Float = 1f,
    var gapWidthDegrees: Float = 90f,
    var gapAngleDegrees: Float = 90f,
    var strokeWidth: Float = 30f,
    var backgroundLineColor: Color = Color.LightGray,
    var sections: List
)


@Model data class DonutSection(
    var amount: Float,
    var color: Color
)

The first big advantage of this change, which is visible in the previous code samples, is the reduced amount of the code (most of it is boilerplate code) required to define all these properties. We don’t have to define getters, setters, XML definitions, or attributes obtaining. So 18 lines of the code used just for the definition of the strokeWidth property with all necessary methods became just 1 line.

在以前的代码示例中可以看到,此更改的第一个主要优点是减少了定义所有这些属性所需的代码量(大部分是样板代码)。 我们不必定义getter,setter,XML定义或属性获取。 因此,仅用于定义strokeWidth属性以及所有必要方法的18行代码就变成了1行。

DonutData class doesn’t contain anything particularly new for someone who knows a little bit of Kotlin except for @Model annotation. This annotation transforms data class into an observable class that will cause a recomposition of the layouts, which are observing its properties, whenever any of them is changed. Eg. When we change gapWidthDegrees property, then the donut view will automatically recompose views that are dependant on these properties and will draw a new layout.

DonutData类除了@Model注释外,对于@Model Kotlin的人来说,其中不包含任何特别新的东西。 此批注将数据类转换为可观察的类,这将导致布局的重新组合,无论何时更改布局,这些布局都会观察其属性。 例如。 当我们更改gapWidthDegrees属性时,甜甜圈视图将自动重新组合依赖于这些属性的视图并绘制新的布局。

潜入@Composable世界 (Dive into @Composable world)

The next step is the definition of the composable view. It is just a plain method with the @Composable annotation that is accepting DonutModel as a parameter.

下一步是可组合视图的定义。 它只是带有@Composable批注的普通方法,该批注接受DonutModel作为参数。

@Composable
fun DonutProgress(model: DonutModel) {
    // 1. Calculate start angle and sweep angle for each section
    val sections = calculateSections(model)


    // 2. Draw Donut from the calculated values
    DrawDonutInternal(data, sections)
}

The body of this method consists of two basic things. The algorithm (1), which is just a plain Kotlin function that calculates and returns all necessary values required for the drawing of the donut (angles, position, rotations, etc). And another composable private function (2) responsible for drawing of the donut from previously calculated data to the canvas.

此方法的主体包含两个基本内容。 算法(1)只是普通的Kotlin函数,它计算并返回绘制甜甜圈所需的所有必要值(角度,位置,旋转度等)。 另一个可组合的私有函数(2)负责将甜甜圈从先前计算的数据绘制到画布上。

@Composable
private fun DrawDonutInternal(model: DonutModel, sections: DonutSections) {
    // 1. Get paint cached with the remember { ... } function
    val paint = getPaintFromCache(data.strokeWidth)


    // 2. Create Canvas composable element
    Canvas(modifier = Modifier.fillMaxSize(), onCanvas = {


        // 3. Draw background line
        drawDonutSegment(this, size, paint, sections.masterPathData)


        // 4. Draw each section line
        sections.entriesPathData.forEach { pathData ->
            drawDonutSegment(this, size, paint, pathData)
        }
    })
}

DrawDonutInternal consists of four main parts. We obtain a Paint object from the “cache” via the remember { … } function (1) (we are going to get into details of this in the few lines). Then, we create a canvas object used to draw a donut (2). Within the onCanvas method, we draw a background line of the donut (3), and finally, we draw all sections of the donut (4).

DrawDonutInternal由四个主要部分组成。 我们通过remember { … }函数(1)从“缓存”中获取一个Paint对象(我们将在几行中对此进行详细介绍)。 然后,我们创建一个用于绘制甜甜圈(2)的画布对象。 在onCanvas方法中,绘制甜甜圈(3)的背景线,最后绘制甜甜圈(4)的所有部分。

The implementation of the getPaintFromCache method looks like this:

getPaintFromCache方法的实现如下所示:

@Composable
private fun getPaintFromCache(strokeWidth: Float): Paint {
    return remember {
        Paint().apply {
            strokeCap = StrokeCap.round
            style = PaintingStyle.stroke
        }
    }.apply {
        this.strokeWidth = strokeWidth
    }
}

The remember { … } function initializes the Paint object only once, during the first composition. This value is cached and every time during recomposition of this composable function it will return the same instance of the Paint object. The only thing that is dynamically changed is the strokeWidth parameter since it is an animatable property. Without the remember { … } function, the Paint object would be created for each recomposition, and that could cause unnecessary memory overhead.

在第一个合成过程中, remember { … }函数仅初始化一次Paint对象。 缓存此值,并且每次在重新组合此可组合函数期间,它将返回Paint对象的相同实例。 唯一可以动态更改的是strokeWidth参数,因为它是可设置动画的属性。 如果没有remember { … }函数,则将为每次重组创建Paint对象,这可能会导致不必要的内存开销。

The implementation of the drawDonutSegment function looks like this:

drawDonutSegment函数的实现如下所示:

private fun drawDonutSegment(canvas: Canvas, parentSize: PxSize, paint: Paint, data: DonutSingleSection) {
    // val rect = calculateRect(...)


    canvas.drawArc(rect, data.startAngle, data.sweepAngle, false, paint)
}

First, we calculate the bounds of the donut defined as a Rect (rectangle) object. Then we draw an arc of the donut section onto the canvas within the area of these bounds.

首先,我们计算定义为Rect (矩形)对象的甜甜圈的边界。 然后,在这些边界区域内,将甜甜圈部分的弧线绘制到画布上。

我们到了一半 (We are halfway there)

In this state, the new implementation of Donut works as expected. The following code creates a new doughnut-like chart

在这种状态下,Donut的新实现按预期工作。 以下代码创建一个新的类似于甜甜圈的图表chart

@Composable
fun SampleComposeScreen(model: DonutModel): Paint {
    Box(Modifier.fillMaxWidth() + Modifier.height(240.dp), gravity = ContentGravity.Center) {
        DonutProgress(model)
    }
}

When we add some more controls and overview in the middle, we get the following result:

当我们在中间添加更多控件和概述时,将得到以下结果:

However, there is one “small” thing missing: animations.

但是,缺少一个“ ”东西:动画。

动画设置 (Animation setup)

Original Donut implementation allows us to animate only a few most important features. In our new implementation, we want it to extend it even more further and enable developers to use animations even more extensively. We know that everybody likes pretty and smooth animations, but we also know that sometimes you need to disable or customize them to get a perfect result. For this purpose, we create a new DonutConfig class and annotate it with @Model annotation. It contains properties for enabling animations (Boolean) and customization of animations (AnimationBuilder) for each property from the DonutData that we wish to have an ability to be animated. In our case, it is every property of DonutData ‍♂ Some of these properties may seem irrelevant, and animation won’t make sense for them at first sight. Still, it’s better to leave it to the developers and their creativity.

原始的Donut实现使我们只能为几个最重要的功能设置动画。 在我们的新实现中,我们希望它能够进一步扩展它,并使开发人员能够更广泛地使用动画。 我们知道每个人都喜欢漂亮而流畅的动画,但是我们也知道有时候您需要禁用或自定义它们以获得完美的效果。 为此,我们创建一个新的DonutConfig类,并使用@Model注释对其进行注释。 它包含用于为DonutData中的每个我们希望具有动画功能的属性启用动画( Boolean )和自定义动画( AnimationBuilder )的属性。 在我们的例子中,这是DonutData每个属性DonutData这些属性中的一些似乎无关紧要,而动画乍一看对它们没有任何意义。 不过,最好还是将其留给开发人员及其创造力。

@Model 
data class DonutConfig(
    var isGapAngleAnimationEnabled: Boolean = true,
    var gapAngleAnimationBuilder: AnimationBuilder = getDefaultFloatAnimationBuilder(),
    var isMasterProgressAnimationEnabled: Boolean = true,
    var masterProgressAnimationBuilder: AnimationBuilder = getDefaultFloatAnimationBuilder()
    // Remaining properties
}

DonutConfig contains a new AnimationBuilder class that serves as some kind of animation interpolator (but it is actually much more). Do you want to use some predefined or custom easing? TweenBuilder is there for you. Do you want to use spring animations? PhysicsBuilder is there also for you. Do you want just to snap an animated value to the target value right away?SnapBuilder. You have a lot of options here that allow you to customize each animation separately.

DonutConfig包含一个新的AnimationBuilder类,该类可用作某种动画插值器(但实际上更多)。 您是否要使用一些预定义的或自定义的缓动? TweenBuilder在那里为您服务。 您要使用Spring动画吗? PhysicsBuilder也为您提供。 您是否只想立即将动画值捕捉到目标值? SnapBuilder 。 您这里有很多选项,可让您分别自定义每个动画。

DonutConfig is implemented, and it is time to update our composable function DonutProgress.

DonutConfig已实现,现在该更新可组合函数DonutProgress

@Composable
fun DonutProgress(model: DonutModel, config: DonutConfig = DonutConfig()) {
    // ...
}

We have added a default value for the config property with all animations enabled since we do not want to bother a developer with these things unless he decides that he wants to change them.

我们为config属性添加了一个启用了所有动画的默认值,因为我们不想让开发人员麻烦这些事情,除非他决定他要更改它们。

动画实现 (Animation implementation)

Now we are going to implement the animations. Since many things in the Jetpack Compose are still in development and only poor documentation is provided so far, we must dive into source code and samples to find out how to deal with this issue. I found three possible ways of handling animations:

现在,我们将实现动画。 由于Jetpack Compose中的许多内容仍在开发中,并且到目前为止仅提供了较差的文档,因此我们必须深入研究源代码和示例以了解如何处理此问题。 我发现了三种处理动画的方法:

  • From the outside by dynamically changing values in the model (Bad solution. You are moving all these responsibilities and work to the developer and thus making his life harder, not easier)

    通过动态更改模型中的值从外部(错误的解决方案。您将所有这些责任移交给开发人员,从而使开发人员的工作更加艰辛,而不是更加轻松)

  • Transitions (Good solution for animating predefined states but that is not very suitable for our purpose)

    过渡(用于预定义状态动画的良好解决方案,但不太适合我们的目的)

  • AnimatedValue (Bingo )

    AnimatedValue( 宾果游戏 )

The AnimatedValue is a class that can animate its generic internal value. It is annotated with @Model annotation. It means that each change of the value causes a recomposition of the layout that is using this value. The animation is started with the animateTo method. Once this method is called, the class starts to observe the animation clock and recalculates animated value on each animation frame defined by the clock, so all the animation magic happens inside of this class. Jetpack Compose takes care of observing these changes and recomposing layout dependent on them.

AnimatedValue是一个可以为其通用内部值制作动画的类。 它带有@Model注释。 这意味着该值的每次更改都会导致使用该值的布局重新组合。 动画从animateTo方法开始。 调用此方法后,该类将开始观察动画时钟并在该时钟定义的每个动画帧上重新计算动画值,因此所有动画魔术都发生在此类内部。 Jetpack Compose会注意观察这些更改并重新构造依赖于它们的布局。

动画背后的魔力 (Magic behind animatedFloat)

Each animatable property is transformed with the animatedFloat (or animatedColor) method in order to initialize AnimatedValue.

每个animable属性都使用animatedFloat (或animatedColor )方法进行转换,以初始化AnimatedValue

val animatedGapAngle = animatedFloat(initVal = model.gapAngleDegrees)

The animatedFloat is a composable function that returns a remembered (cached) AnimatedFloat object. The AnimatedFloat object returned from this method always has the same instance even after layout recomposition. It holds the initial value even when the gapAngleDegrees property is changed. In order to change the value of this object, we have to set a new target value to which the object animates its internal value. Until the animation process is not started with animateTo or some other animation method, the value of the animatedGapAngle won’t be changed even when initial data are changed. This method is a part of the Jetpack Compose and is implemented this way:

animatedFloat是可组合的函数,它返回一个已记住(缓存)的AnimatedFloat对象。 从此方法返回的AnimatedFloat对象即使在布局重新组合后也始终具有相同的实例。 即使更改了gapAngleDegrees属性,它也会保留初始值。 为了更改该对象的值,我们必须设置一个新的目标值,该对象将其内部值设置为动画目标。 直到动画过程不与启动animateTo或一些其它的动画的方法,所述的值animatedGapAngle不会即使当初始数据被改变改变。 此方法是Jetpack Compose的一部分,并通过以下方式实现:

@Composable
fun animatedFloat(
    initVal: Float,
    clock: AnimationClockObservable = AnimationClockAmbient.current
): AnimatedFloat = clock.asDisposableClock().let { disposableClock ->
    remember(disposableClock) { AnimatedFloatModel(initVal, disposableClock) }
}

It has a few important things that we should notice and understand to find out how this animation technique is working. AnimationClockAmbient, rememberand AnimatedFloatValue.

我们需要注意并了解一些重要的内容,以了解这种动画技术是如何工作的。 AnimationClockAmbientrememberAnimatedFloatValue

In quick terms, an Ambient in the Jetpack Compose is a mechanism that lets you pass some kind of dependencies through the layout tree without having to put them explicitly in the method signature. It’s just there in the background waiting for you to use it whenever you need it. (Eg. ContextAmbient.current returns Context, so you can call it from anywhere within composable functions where it is needed). AnimationClockAmbient provides a clock that notifies us whenever new animated value should be recalculated. remember { … }, as we already learned, provides some kind of cache mechanism that makes sure that the AnimatedFloatValue is instantiated only once and with an initial value. AnimatedFloatValue is a subclass of AnimatedValue that animates float values.

简而言之,Jetpack Compose中的Ambient是一种机制,可让您通过布局树传递某种依赖关系,而不必将它们明确地放置在方法签名中。 它只是在后台等待您在需要时使用它。 (例如ContextAmbient.current返回Context ,因此您可以在需要的可组合函数中的任何位置调用它)。 AnimationClockAmbient提供了一个时钟,该时钟在每当需要重新计算新的动画值时通知我们。 remember { … } ,正如我们已经了解的那样,它提供了某种缓存机制,可确保AnimatedFloatValue仅被实例化一次并带有初始值。 AnimatedFloatValue是的一个子类AnimatedValue该动画float值。

扩展BaseAnimatedValue (Extending BaseAnimatedValue)

AnimatedValue has two public methods that are interesting for us animateTo and snapTo. Since we want to animate our properties only when they are changed and enabled, we use the following extension method that helps us with this logic.

AnimatedValue具有两个我们感兴趣的公共方法animateTosnapTo 。 由于我们只想在更改和启用属性后对其进行动画处理,因此我们使用以下扩展方法来帮助我们实现此逻辑。

internal fun  BaseAnimatedValue.animateOrSnapDistinctValues(
    newValue: VALUE,
    isAnimationEnabled: Boolean,
    animationBuilder: AnimationBuilder
) {
    if (newValue != targetValue) {
        if (isAnimationEnabled) {
            animateTo(newValue, animationBuilder)
        } else {
            snapTo(newValue)
        }
    }
}

If the new value is the same as the target value, then it does nothing since we do not want to trigger the same animation again. If the value is not the same, then it checks if the animation is enabled, and if it is, then it starts an animation process. Otherwise, it just snaps it to the final value.

如果新值与目标值相同,则它将不执行任何操作,因为我们不想再次触发相同的动画。 如果值不相同,则检查是否启用了动画,如果启用,则开始动画处理。 否则,它只会将其捕捉到最终值。

粘在一起 (Gluing it together)

Now, we need to glue all these things together. If we would like to animate only one property, then the implementation of DonutProgress could look somehow like this:

现在,我们需要将所有这些东西粘合在一起。 如果我们只想为一个属性设置动画,那么DonutProgress的实现可能看起来像这样:

@Composable
fun DonutProgress(model: DonutModel, config: DonutConfig = DonutConfig()) {
    // 1. Create animated float
    val animatedGapAngle = animatedFloat(model.gapAngleDegrees)


    // 2. Start animation if gapAngleDegrees is changed
    animatedGapAngle.animateOrSnapDistinctValues(
        newValue = model.gapAngleDegrees,
        isAnimationEnabled = config.isGapAngleAnimationEnabled,
        animationBuilder = config.gapAngleAnimationBuilder
    )


    // 3. Draw donut with animated value
    DrawDonutInternal(model, animatedGapAngle)
}

The change of the gapAngleDegrees property causes DonutProgress to recompose. That causes the execution of the animateTo method of animatedGapAngle, which means that the animation is started. That change is observed by Jetpack Compose and it causes a recomposition of the DrawDonut composable function. It is recomposed multiple times with an updated animated value until the animation is stopped. This approach allows us to animate multiple properties at the same time with very little management and without worrying about interfering with each other.

gapAngleDegrees属性的更改将导致DonutProgress重新组成。 这将导致执行animatedGapAngleanimateTo方法,这意味着动画已开始。 Jetpack Compose会观察到该更改,并导致DrawDonut可组合功能的重新组合。 使用更新的动画值对其进行多次重组,直到动画停止。 这种方法使我们可以在几乎没有管理的情况下同时为多个属性设置动画,而不必担心相互干扰。

Since we have multiple animated properties, the actual implementation looks more like this:

由于我们具有多个动画属性,因此实际的实现看起来更像这样:

@Composable
fun DonutProgress(model: DonutModel, config: DonutConfig = DonutConfig()) {
    // 1. Calculate sections and wrap properties with animatedFloat and animatedColor
    val donutProgressValues = createDonutProgressValues(model)


    // 2. Animate distinct properties
    animateOrSnapDistinctValues(model, config, donutProgressValues)


    // 3. Draw donut from the animated properties
    DrawDonutInternal(model, donutProgressValues)
}

All animated properties are wrapped in the wrapper class (1.), then they are animated if necessary (2.), and then they are passed to the DrawDonut, which takes care of drawing it onto canvas (3.).

所有动画属性都包装在包装类中(1.),然后在必要时进行动画处理(2.),然后将它们传递到DrawDonut,由它负责将其绘制到画布上(3.)。

Now, we need to update our algorithm inside DrawDonut to work with the animated values instead of the values directly provided by DonutData.

现在,我们需要在DrawDonut更新我们的算法以使用动画值,而不是DonutData直接提供的值。

@Composable
private fun DrawDonutInternal(model: DonutModel, donutProgressValues: DonutProgressValues) {
    // val wholeDonutAngle = 360f - data.gapWidthDegrees
    val wholeDonutAngle = 360f - donutProgressValues.animatedGapWidthDegrees.value


    // Remaining calculations and drawing methods
}

It is a very simple change. Each direct access of properties from DonutData is just replaced with their animated counterpart.

这是一个非常简单的更改。 从DonutData对属性的每次直接访问都将替换为其动画对象。

结果 (The result)

That’s All Folks Jetpack Compose implementation is successfully finished.

这就是All FolksJetpack Compose的实现已成功完成。

The Jetpack Compose Donut implementation Jetpack Compose Donut实施

We successfully implemented a delicious donut in Jetpack Compose with extensive customization and animation support. A new UI paradigm forced us to implement it differently, but we were able to reuse some parts of the logic from the original implementation also. It took some time and effort to figure out how Jetpack Compose works internally, but once we got into it and understood it, then the work became very intuitive. We also learned that creating animations in Jetpack Compose is not as difficult as it seems at first glance. And last but not least we transformed all these XML definitions and custom methods of our library to much more compact, readable, and simpler definitions.

我们通过广泛的自定义和动画支持在Jetpack Compose中成功实现了美味的甜甜圈。 一个新的UI范式迫使我们以不同的方式实现它,但是我们也能够重用原始实现中的某些逻辑部分。 花了一些时间和精力来弄清Jetpack Compose的内部工作方式,但是一旦我们了解并理解了它,工作就变得非常直观。 我们还了解到,在Jetpack Compose中创建动画并不像乍看起来那样困难。 最后但并非最不重要的一点是,我们将库的所有这些XML定义和自定义方法都转换为更紧凑,可读性和更简单的定义。

Donut definition for traditional Android view:

传统Android视图的甜甜圈定义:

Traditional Android view 传统的Android视图

Donut definition for Jetpack Compose:

Jetpack Compose的甜甜圈定义:

val model = DonutModel(
    cap = 8f,
    masterProgress = 1f,
    sections = listOf(
        DonutSection(amount = 1f, color = Color.Cyan),
        DonutSection(amount = 1f, color = Color.Red),
        DonutSection(amount = 1f, color = Color.Green),
        DonutSection(amount = 0f, color = Color.Blue)
    )
)


DonutProgress(model = model)

For more details on Donut library, check our Github page and for more details about the details of the new Jetpack Compose implementation check, this file with all implementation details.

有关Donut库的更多详细信息,请查看我们的Github页面,以及有关新的Jetpack Compose实现检查的详细信息, 该文件包含所有实现的详细信息。

翻译自: https://blog.thefuntasty.com/from-custom-view-to-jetpack-compose-5a95e8d76a9a

jetpack

你可能感兴趣的:(java)