Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Commit 35edc28

Browse files
committed
Add interruptible animation feature to the Histogram example
To see it in action see: https://drive.google.com/a/google.com/file/d/11VLWRDBKzLPdsC1w1tCw9ZShOKuDGRyj/view?usp=sharing
1 parent 7dbbca0 commit 35edc28

5 files changed

Lines changed: 320 additions & 29 deletions

File tree

ConstraintLayoutExamples/motionlayout/src/main/java/com/google/androidstudio/motionlayoutexample/histogramdemo/HistogramActivity.kt

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2019 The Android Open Source Project
2+
* Copyright (C) 2020 The Android Open Source Project
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,19 +16,19 @@
1616

1717
package com.google.androidstudio.motionlayoutexample.histogramdemo
1818

19+
import android.graphics.Color
20+
import android.graphics.PorterDuff
1921
import android.os.Bundle
2022
import android.view.View
2123
import android.view.ViewTreeObserver.OnGlobalLayoutListener
24+
import android.widget.Switch
2225
import androidx.annotation.Nullable
2326
import androidx.appcompat.app.AppCompatActivity
24-
import androidx.constraintlayout.motion.widget.MotionLayout
25-
import androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener
2627
import androidx.core.content.ContextCompat
2728
import com.google.androidstudio.motionlayoutexample.R
2829
import kotlinx.android.synthetic.main.histogram_layout.*
2930
import java.lang.RuntimeException
3031
import java.util.*
31-
import java.util.concurrent.atomic.AtomicBoolean
3232
import kotlin.collections.ArrayList
3333

3434
class HistogramActivity : AppCompatActivity() {
@@ -44,50 +44,37 @@ class HistogramActivity : AppCompatActivity() {
4444
// The main widget
4545
private var widget: HistogramWidget? = null
4646

47-
// Animation guard
48-
private val animating = AtomicBoolean(false)
49-
private val animationListener: TransitionListener = object : TransitionListener {
50-
override fun onTransitionStarted(motionLayout: MotionLayout, startId: Int, endId: Int) {
51-
animating.set(true)
52-
}
53-
54-
override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
55-
animating.set(false)
56-
}
57-
58-
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
59-
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { }
60-
}
47+
private val animationGuard: HistogramAnimationGuard = HistogramAnimationGuard()
6148

6249
override fun onCreate(@Nullable savedInstanceState: Bundle?) {
6350
super.onCreate(savedInstanceState)
6451
setContentView(R.layout.histogram_layout)
6552
widget = histogram
6653
restoreView(savedInstanceState)
67-
widget!!.setTransitionListener(animationListener)
54+
widget!!.setTransitionListener(animationGuard.animationListener)
6855
}
6956

7057
/**
7158
* Add random data to the histogram.
7259
*/
7360
fun onClickAdd(view: View?) {
74-
if (animating.get()) {
61+
if (animationGuard.wait) {
7562
return
7663
}
7764
add()
7865
widget!!.animateWidget()
7966
}
8067

8168
fun onClickSort(view: View?) {
82-
if (animating.get()) {
69+
if (animationGuard.wait) {
8370
return
8471
}
8572
bars = widget!!.sort()
8673
widget!!.animateWidget()
8774
}
8875

8976
fun onClickRandom(view: View) {
90-
if (animating.get()) {
77+
if (animationGuard.wait) {
9178
return
9279
}
9380
add()
@@ -148,4 +135,33 @@ class HistogramActivity : AppCompatActivity() {
148135
}
149136
})
150137
}
138+
139+
fun onClickSwitch(view: View) {
140+
val animationInterruptible = (view as Switch).isChecked
141+
animationGuard.setInterruptible(animationInterruptible)
142+
/**
143+
* TODO: The current histogram widget does not support interruptible sort to keep it short.
144+
* This can be a fun exercise to implement yourself.
145+
*
146+
* To support this feature, you'll want to animate from:
147+
* - the current x position to
148+
* - the new x position (after sorted)
149+
*
150+
* for each bars. It means you cannot use chain feature of constraint layout. You'll need
151+
* to calculate the after-sorted x location of each bars manually and animate them.
152+
*/
153+
sort.setEnabledAndChangeColor(!animationInterruptible)
154+
both.setEnabledAndChangeColor(!animationInterruptible)
155+
}
156+
}
157+
158+
fun View.setEnabledAndChangeColor(enabled: Boolean) {
159+
if (!enabled) {
160+
background.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN)
161+
isClickable = false
162+
} else {
163+
background.colorFilter = null
164+
isClickable = true
165+
}
166+
invalidate()
151167
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (C) 2020 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.androidstudio.motionlayoutexample.histogramdemo
18+
19+
import androidx.constraintlayout.motion.widget.MotionLayout
20+
import java.util.concurrent.atomic.AtomicBoolean
21+
22+
/**
23+
* Animation guard helper.
24+
*/
25+
class HistogramAnimationGuard {
26+
27+
/**
28+
* @return true if animation should wait. False otherwise.
29+
*/
30+
val wait: Boolean
31+
get() {
32+
if (interruptible) {
33+
return false
34+
}
35+
return animating.get()
36+
}
37+
38+
private var interruptible: Boolean = false
39+
private val animating = AtomicBoolean(false)
40+
41+
val animationListener: MotionLayout.TransitionListener = object : MotionLayout.TransitionListener {
42+
override fun onTransitionStarted(motionLayout: MotionLayout, startId: Int, endId: Int) {
43+
animating.set(true)
44+
}
45+
46+
override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
47+
animating.set(false)
48+
}
49+
50+
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
51+
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { }
52+
}
53+
54+
/**
55+
* @param interruptible true if animation is interruptible. I.e. Animation does not need to be
56+
* finished before the new one starts.
57+
* False if animation must complete before new one can start.
58+
*/
59+
fun setInterruptible(interruptible: Boolean) {
60+
this.interruptible = interruptible
61+
}
62+
}

ConstraintLayoutExamples/motionlayout/src/main/java/com/google/androidstudio/motionlayoutexample/histogramdemo/HistogramWidget.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2019 The Android Open Source Project
2+
* Copyright (C) 2020 The Android Open Source Project
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,9 @@ import androidx.core.content.ContextCompat
3131
import com.google.androidstudio.motionlayoutexample.R
3232
import java.util.*
3333

34+
/**
35+
* Histogram widget that can animate between programmatically generated data.
36+
*/
3437
class HistogramWidget : MotionLayout {
3538
companion object {
3639
private const val TAG = "HistogramWidget"
@@ -89,7 +92,7 @@ class HistogramWidget : MotionLayout {
8992
barTransition = createTransition(scene)
9093

9194
/**
92-
* The name is unintuitive due to legacy support.
95+
* The order matters here.
9396
* [MotionScene.addTransition] adds the transition to the scene while
9497
* [MotionScene.setTransition] sets the transition to be the current transition.
9598
*/
@@ -121,26 +124,31 @@ class HistogramWidget : MotionLayout {
121124
*/
122125
fun setData(newData: List<HistogramBarMetaData>) {
123126
val startSet: ConstraintSet = getConstraintSet(barTransition!!.startConstraintSetId)
124-
updateConstraintSet(startSet, currentBars)
127+
updateConstraintSet(startSet, currentBars, false)
125128
val endSet: ConstraintSet = getConstraintSet(barTransition!!.endConstraintSetId)
126129
updateConstraintSet(endSet, newData)
127130
nextBars = ArrayList(newData)
128131
}
129132

130133
/**
131134
* Update the constraint set with the bar metadata.
135+
* @param useHeightFromMetaData if true use the meta data height. If false use the current
136+
* view heights.
132137
*/
133138
private fun updateConstraintSet(
134139
set: ConstraintSet,
135-
list: List<HistogramBarMetaData>) {
140+
list: List<HistogramBarMetaData>,
141+
useHeightFromMetaData: Boolean = true) {
136142
list.forEach { metadata ->
137143
val view = bars[metadata.id]!!
138-
val height: Float = metadata.height * height
144+
val height: Int =
145+
if (useHeightFromMetaData) (metadata.height * height).toInt()
146+
else bars[metadata.id]!!.height
139147
view.setTextColor(metadata.barTextColour)
140148
view.text = metadata.name
141149

142150
// These are attributes we wish to animate. We set them through ConstraintSet.
143-
set.constrainHeight(view.id, height.toInt())
151+
set.constrainHeight(view.id, height)
144152
set.setColorValue(view.id, "BackgroundColor", metadata.barColour)
145153
}
146154
}

0 commit comments

Comments
 (0)