Skip to content

Commit

Permalink
Add interruptible animation feature to the Histogram example
Browse files Browse the repository at this point in the history
  • Loading branch information
aquagray committed Jan 10, 2020
1 parent 7dbbca0 commit 35edc28
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 The Android Open Source Project
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,19 +16,19 @@

package com.google.androidstudio.motionlayoutexample.histogramdemo

import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Bundle
import android.view.View
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.Switch
import androidx.annotation.Nullable
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener
import androidx.core.content.ContextCompat
import com.google.androidstudio.motionlayoutexample.R
import kotlinx.android.synthetic.main.histogram_layout.*
import java.lang.RuntimeException
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList

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

// Animation guard
private val animating = AtomicBoolean(false)
private val animationListener: TransitionListener = object : TransitionListener {
override fun onTransitionStarted(motionLayout: MotionLayout, startId: Int, endId: Int) {
animating.set(true)
}

override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
animating.set(false)
}

override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { }
}
private val animationGuard: HistogramAnimationGuard = HistogramAnimationGuard()

override fun onCreate(@Nullable savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.histogram_layout)
widget = histogram
restoreView(savedInstanceState)
widget!!.setTransitionListener(animationListener)
widget!!.setTransitionListener(animationGuard.animationListener)
}

/**
* Add random data to the histogram.
*/
fun onClickAdd(view: View?) {
if (animating.get()) {
if (animationGuard.wait) {
return
}
add()
widget!!.animateWidget()
}

fun onClickSort(view: View?) {
if (animating.get()) {
if (animationGuard.wait) {
return
}
bars = widget!!.sort()
widget!!.animateWidget()
}

fun onClickRandom(view: View) {
if (animating.get()) {
if (animationGuard.wait) {
return
}
add()
Expand Down Expand Up @@ -148,4 +135,33 @@ class HistogramActivity : AppCompatActivity() {
}
})
}

fun onClickSwitch(view: View) {
val animationInterruptible = (view as Switch).isChecked
animationGuard.setInterruptible(animationInterruptible)
/**
* TODO: The current histogram widget does not support interruptible sort to keep it short.
* This can be a fun exercise to implement yourself.
*
* To support this feature, you'll want to animate from:
* - the current x position to
* - the new x position (after sorted)
*
* for each bars. It means you cannot use chain feature of constraint layout. You'll need
* to calculate the after-sorted x location of each bars manually and animate them.
*/
sort.setEnabledAndChangeColor(!animationInterruptible)
both.setEnabledAndChangeColor(!animationInterruptible)
}
}

fun View.setEnabledAndChangeColor(enabled: Boolean) {
if (!enabled) {
background.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN)
isClickable = false
} else {
background.colorFilter = null
isClickable = true
}
invalidate()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.androidstudio.motionlayoutexample.histogramdemo

import androidx.constraintlayout.motion.widget.MotionLayout
import java.util.concurrent.atomic.AtomicBoolean

/**
* Animation guard helper.
*/
class HistogramAnimationGuard {

/**
* @return true if animation should wait. False otherwise.
*/
val wait: Boolean
get() {
if (interruptible) {
return false
}
return animating.get()
}

private var interruptible: Boolean = false
private val animating = AtomicBoolean(false)

val animationListener: MotionLayout.TransitionListener = object : MotionLayout.TransitionListener {
override fun onTransitionStarted(motionLayout: MotionLayout, startId: Int, endId: Int) {
animating.set(true)
}

override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
animating.set(false)
}

override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { }
}

/**
* @param interruptible true if animation is interruptible. I.e. Animation does not need to be
* finished before the new one starts.
* False if animation must complete before new one can start.
*/
fun setInterruptible(interruptible: Boolean) {
this.interruptible = interruptible
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 The Android Open Source Project
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -31,6 +31,9 @@ import androidx.core.content.ContextCompat
import com.google.androidstudio.motionlayoutexample.R
import java.util.*

/**
* Histogram widget that can animate between programmatically generated data.
*/
class HistogramWidget : MotionLayout {
companion object {
private const val TAG = "HistogramWidget"
Expand Down Expand Up @@ -89,7 +92,7 @@ class HistogramWidget : MotionLayout {
barTransition = createTransition(scene)

/**
* The name is unintuitive due to legacy support.
* The order matters here.
* [MotionScene.addTransition] adds the transition to the scene while
* [MotionScene.setTransition] sets the transition to be the current transition.
*/
Expand Down Expand Up @@ -121,26 +124,31 @@ class HistogramWidget : MotionLayout {
*/
fun setData(newData: List<HistogramBarMetaData>) {
val startSet: ConstraintSet = getConstraintSet(barTransition!!.startConstraintSetId)
updateConstraintSet(startSet, currentBars)
updateConstraintSet(startSet, currentBars, false)
val endSet: ConstraintSet = getConstraintSet(barTransition!!.endConstraintSetId)
updateConstraintSet(endSet, newData)
nextBars = ArrayList(newData)
}

/**
* Update the constraint set with the bar metadata.
* @param useHeightFromMetaData if true use the meta data height. If false use the current
* view heights.
*/
private fun updateConstraintSet(
set: ConstraintSet,
list: List<HistogramBarMetaData>) {
list: List<HistogramBarMetaData>,
useHeightFromMetaData: Boolean = true) {
list.forEach { metadata ->
val view = bars[metadata.id]!!
val height: Float = metadata.height * height
val height: Int =
if (useHeightFromMetaData) (metadata.height * height).toInt()
else bars[metadata.id]!!.height
view.setTextColor(metadata.barTextColour)
view.text = metadata.name

// These are attributes we wish to animate. We set them through ConstraintSet.
set.constrainHeight(view.id, height.toInt())
set.constrainHeight(view.id, height)
set.setColorValue(view.id, "BackgroundColor", metadata.barColour)
}
}
Expand Down
Loading

0 comments on commit 35edc28

Please sign in to comment.