├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── karumi │ │ └── androidanimations │ │ ├── MainActivity.kt │ │ ├── MainFragment.kt │ │ ├── base │ │ └── BaseFragment.kt │ │ ├── common │ │ ├── CircularReveal.kt │ │ ├── CircularTransform.kt │ │ ├── CircularView.kt │ │ ├── ColorEvaluator.kt │ │ ├── GlobalPositionTransition.kt │ │ ├── PaintView.kt │ │ ├── PathProperty.kt │ │ ├── RadiusTransition.kt │ │ └── TextResize.kt │ │ ├── coordinatorlayout │ │ ├── CoordinatorLayoutExample.kt │ │ ├── CoordinatorLayoutExerciseFragment.kt │ │ ├── CoordinatorLayoutFragment.kt │ │ ├── CustomBehaviorFragment.kt │ │ ├── DefaultBehaviorFragment.kt │ │ └── ParallaxBehavior.kt │ │ ├── drawablegraphicsanimation │ │ ├── DrawableGraphicsAnimationFragment.kt │ │ ├── DrawableGraphicsExerciseAnimation.kt │ │ ├── DrawableGraphicsLottieAnimation.kt │ │ └── DrawableGraphicsSimpleAnimation.kt │ │ ├── extensions │ │ ├── StringResourceResolver.kt │ │ ├── animatePath.kt │ │ ├── exhaustive.kt │ │ ├── pixel.kt │ │ └── transitions.kt │ │ ├── layouttransition │ │ ├── ExerciseLayoutTransition.kt │ │ ├── LayoutTransitionFragment.kt │ │ └── SimpleLayoutTransition.kt │ │ ├── pathdrawing │ │ └── PathDrawingFragment.kt │ │ ├── propertyanimator │ │ ├── PropertyAnimatorFragment.kt │ │ ├── PropertyExerciseAnimator.kt │ │ └── PropertySimpleAnimator.kt │ │ ├── sharedelements │ │ ├── SharedBackgroundPathTransition.kt │ │ ├── SharedBackgroundPathTransitionFragment.kt │ │ ├── SharedElementsExerciseTransition.kt │ │ ├── SharedElementsExerciseTransitionFragment.kt │ │ ├── SharedElementsFragment.kt │ │ ├── SharedImageTransition.kt │ │ └── SharedImageTransitionFragment.kt │ │ └── viewanimation │ │ ├── ViewAnimationFragment.kt │ │ ├── ViewExerciseAnimation.kt │ │ └── ViewSimpleAnimation.kt │ └── res │ ├── anim │ └── alpha_animation.xml │ ├── animator │ └── value_animator.xml │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── circle_item.xml │ ├── coordinator_exercise_item_background.xml │ ├── frame00.xml │ ├── frame01.xml │ ├── frame02.xml │ ├── frame03.xml │ ├── frame04.xml │ ├── frame05.xml │ ├── frame06.xml │ ├── frame07.xml │ ├── frame08.xml │ ├── frame09.xml │ ├── frame10.xml │ ├── frame11.xml │ ├── frame12.xml │ ├── frame13.xml │ ├── frame14.xml │ ├── frame15.xml │ ├── frame16.xml │ ├── frame17.xml │ ├── frame18.xml │ ├── frame19.xml │ ├── frame20.xml │ ├── frame21.xml │ ├── frame22.xml │ ├── frame23.xml │ ├── frame24.xml │ ├── frame25.xml │ ├── frame26.xml │ ├── frame27.xml │ ├── frame28.xml │ ├── frame29.xml │ ├── frame30.xml │ ├── ic_launcher_background.xml │ ├── keyframe_animation.xml │ ├── landscape.jpg │ ├── parallax_item_background.xml │ ├── property_animator_switch_background.xml │ ├── property_animator_switch_ball_background.xml │ ├── rounded_item.xml │ ├── username_gradient.xml │ ├── vector_landscape.jpg │ ├── vectorized_animation.xml │ └── view_section_background.xml │ ├── layout │ ├── activity_main.xml │ ├── fragment_animated_drawable_graphics.xml │ ├── fragment_coordinator_layout.xml │ ├── fragment_coordinator_layout_exercise.xml │ ├── fragment_custom_behavior.xml │ ├── fragment_default_behaviors.xml │ ├── fragment_layout_transition.xml │ ├── fragment_main.xml │ ├── fragment_path_drawing.xml │ ├── fragment_property_animator.xml │ ├── fragment_shared_background_path_transition.xml │ ├── fragment_shared_elements.xml │ ├── fragment_shared_elements_exercise_transition.xml │ ├── fragment_shared_image_transition.xml │ ├── fragment_view_animation.xml │ ├── view_coordinator_exercise.xml │ ├── view_coordinator_layout_example.xml │ ├── view_custom_behavior_item.xml │ ├── view_default_behaviors_item.xml │ ├── view_exercise_drawable_graphics_animation.xml │ ├── view_exercise_layout_transition.xml │ ├── view_exercise_property_animation.xml │ ├── view_exercise_view_animation.xml │ ├── view_layout_transition_end_state.xml │ ├── view_layout_transition_exercise_end_state.xml │ ├── view_layout_transition_start_state.xml │ ├── view_lottie_drawable_graphics_animation.xml │ ├── view_section.xml │ ├── view_shared_background_path_transition.xml │ ├── view_shared_elements_exercise_transition.xml │ ├── view_shared_image_element_transition.xml │ ├── view_simple_drawable_graphics_animation.xml │ ├── view_simple_layout_transition.xml │ ├── view_simple_property_animator.xml │ └── view_simple_view_animation.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── navigation │ └── navigation_graph.xml │ ├── raw │ └── material_wave_loading.json │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── ints.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: "AndroidAnimations" 2 | baseurl: /AndroidAnimations 3 | remote_theme: Karumi/KarumiJekyllTheme 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "kotlin-android" 3 | apply plugin: "kotlin-android-extensions" 4 | apply plugin: "androidx.navigation.safeargs.kotlin" 5 | 6 | android { 7 | compileSdkVersion 28 8 | defaultConfig { 9 | applicationId "com.karumi.androidanimations" 10 | minSdkVersion 19 11 | targetSdkVersion 28 12 | versionCode 1 13 | versionName "1.0" 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | vectorDrawables.useSupportLibrary = true 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: "libs", include: ["*.jar"]) 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 28 | implementation "androidx.appcompat:appcompat:1.1.0-alpha04" 29 | implementation "androidx.constraintlayout:constraintlayout:1.1.3" 30 | implementation "androidx.recyclerview:recyclerview:1.0.0" 31 | implementation "androidx.navigation:navigation-fragment-ktx:2.0.0" 32 | implementation "androidx.navigation:navigation-ui-ktx:2.0.0" 33 | implementation 'com.afollestad:recyclical:0.5.2' 34 | implementation 'com.airbnb.android:lottie:3.0.0' 35 | implementation 'com.squareup.picasso:picasso:2.71828' 36 | testImplementation "junit:junit:4.12" 37 | androidTestImplementation "com.android.support.test:runner:1.0.2" 38 | androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.2" 39 | } 40 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.navigation.findNavController 6 | import androidx.navigation.ui.AppBarConfiguration 7 | import androidx.navigation.ui.setupWithNavController 8 | import kotlinx.android.synthetic.main.activity_main.* 9 | 10 | class MainActivity : AppCompatActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(R.layout.activity_main) 14 | configureToolbar() 15 | } 16 | 17 | private fun configureToolbar() { 18 | val navController = findNavController(R.id.hostFragment) 19 | navController.addOnDestinationChangedListener { _, destination, _ -> 20 | collapsingToolbarLayout.isTitleEnabled = false 21 | collapsingToolbarLayout.title = destination.label 22 | } 23 | val appBarConfiguration = AppBarConfiguration(navController.graph) 24 | toolbar.setupWithNavController(navController, appBarConfiguration) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/MainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.navigation.fragment.findNavController 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import com.afollestad.recyclical.ViewHolder 11 | import com.afollestad.recyclical.datasource.dataSourceOf 12 | import com.afollestad.recyclical.setup 13 | import com.afollestad.recyclical.withItem 14 | import com.karumi.androidanimations.base.BaseFragment 15 | import kotlinx.android.synthetic.main.fragment_main.* 16 | 17 | class MainFragment : BaseFragment() { 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View? = inflater.inflate(R.layout.fragment_main, container, false) 24 | 25 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 26 | super.onViewCreated(view, savedInstanceState) 27 | configureSections() 28 | } 29 | 30 | private fun configureSections() { 31 | val layoutManager = LinearLayoutManager(requireContext()) 32 | val dataSource = dataSourceOf(*Section.values()) 33 | 34 | allSections.setup { 35 | withLayoutManager(layoutManager) 36 | withDataSource(dataSource) 37 | 38 | withItem
(R.layout.view_section) { 39 | onBind(::SectionViewHolder) { _, item -> 40 | name.text = when (item) { 41 | Section.PathDrawing -> R.string.section_path_drawing 42 | Section.ViewAnimation -> R.string.section_view_animation_framework 43 | Section.PropertyAnimation -> R.string.section_property_animation_framework 44 | Section.AnimatedVector -> R.string.section_animated_vector_drawable 45 | Section.LayoutTransition -> R.string.section_layout_transition 46 | Section.SharedElements -> R.string.section_shared_elements 47 | Section.CoordinatorLayout -> R.string.section_coordinator_layout 48 | }.resolve() 49 | } 50 | 51 | onClick { _, item -> 52 | val directions = when (item) { 53 | Section.PathDrawing -> MainFragmentDirections.actionMainFragmentToPathDrawingFragment() 54 | Section.ViewAnimation -> MainFragmentDirections.actionMainFragmentToViewAnimatorFragment() 55 | Section.PropertyAnimation -> MainFragmentDirections.actionMainFragmentToPropertyAnimationFragment() 56 | Section.AnimatedVector -> MainFragmentDirections.actionMainFragmentToDrawableGraphicsAnimationFragment() 57 | Section.LayoutTransition -> MainFragmentDirections.actionMainFragmentToLayoutTransitionFragment() 58 | Section.SharedElements -> MainFragmentDirections.actionMainFragmentToSharedElementsFragment() 59 | Section.CoordinatorLayout -> MainFragmentDirections.actionMainFragmentToCoordinatorLayoutFragment() 60 | } 61 | findNavController().navigate(directions) 62 | } 63 | } 64 | } 65 | } 66 | 67 | private enum class Section { 68 | PathDrawing, ViewAnimation, PropertyAnimation, AnimatedVector, LayoutTransition, 69 | SharedElements, CoordinatorLayout 70 | } 71 | 72 | private class SectionViewHolder(itemView: View) : ViewHolder(itemView) { 73 | val name: TextView = itemView.findViewById(R.id.sectionName) 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.base 2 | 3 | import androidx.fragment.app.Fragment 4 | import com.karumi.androidanimations.extensions.StringResourceResolver 5 | 6 | open class BaseFragment : Fragment(), StringResourceResolver { 7 | fun Int.resolve(): String = resolve(requireContext()) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/CircularReveal.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.animation.Animator 4 | import android.annotation.TargetApi 5 | import android.graphics.Rect 6 | import android.os.Build 7 | import android.view.View 8 | import android.view.ViewAnimationUtils 9 | import android.view.ViewGroup 10 | import androidx.transition.Transition 11 | import androidx.transition.TransitionValues 12 | 13 | 14 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 15 | class CircularReveal : Transition() { 16 | 17 | override fun getTransitionProperties(): Array? = PROPS 18 | 19 | override fun captureStartValues(transitionValues: TransitionValues) { 20 | captureValues(transitionValues) 21 | } 22 | 23 | override fun captureEndValues(transitionValues: TransitionValues) { 24 | captureValues(transitionValues) 25 | } 26 | 27 | private fun captureValues(values: TransitionValues) { 28 | val view = values.view 29 | val bounds = Rect() 30 | bounds.left = view.left 31 | bounds.right = view.right 32 | bounds.top = view.top 33 | bounds.bottom = view.bottom 34 | 35 | values.values[BOUNDS] = bounds 36 | } 37 | 38 | override fun createAnimator( 39 | sceneRoot: ViewGroup, 40 | startValues: TransitionValues?, 41 | endValues: TransitionValues? 42 | ): Animator? { 43 | startValues ?: return null 44 | endValues ?: return null 45 | 46 | val startRect = startValues.values[BOUNDS] as Rect 47 | val endRect = endValues.values[BOUNDS] as Rect 48 | 49 | val view = endValues.view 50 | 51 | val circularTransition: Animator 52 | return if (isReveal(startRect, endRect)) { 53 | circularTransition = createReveal(view, startRect, endRect) 54 | circularTransition 55 | } else { 56 | layout(startRect, view) 57 | circularTransition = createConceal(view, startRect, endRect) 58 | circularTransition 59 | } 60 | } 61 | 62 | private fun layout(startRect: Rect, view: View) { 63 | view.layout(startRect.left, startRect.top, startRect.right, startRect.bottom) 64 | } 65 | 66 | private fun createReveal(view: View, from: Rect, to: Rect): Animator { 67 | val centerX = from.centerX() 68 | val centerY = from.centerY() 69 | val finalRadius = Math.hypot(to.width().toDouble(), to.height().toDouble()).toFloat() 70 | 71 | return ViewAnimationUtils.createCircularReveal( 72 | view, centerX - from.left, centerY - from.top, 73 | from.width() / 2f, finalRadius 74 | ) 75 | } 76 | 77 | private fun createConceal(view: View, from: Rect, to: Rect): Animator { 78 | val centerX = to.centerX() 79 | val centerY = to.centerY() 80 | val initialRadius = Math.hypot(from.width().toDouble(), from.height().toDouble()).toFloat() 81 | 82 | return ViewAnimationUtils.createCircularReveal( 83 | view, centerX - to.left, centerY - to.top, 84 | initialRadius, to.width() / 2f 85 | ) 86 | } 87 | 88 | private fun isReveal(startRect: Rect, endRect: Rect): Boolean { 89 | return startRect.width() < endRect.width() 90 | } 91 | 92 | companion object { 93 | private const val BOUNDS = "viewBounds" 94 | 95 | private val PROPS = arrayOf(BOUNDS) 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/CircularTransform.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapShader 5 | import android.graphics.Canvas 6 | import android.graphics.Paint 7 | import android.graphics.Shader 8 | import com.squareup.picasso.Transformation 9 | 10 | 11 | class CircularTransform : Transformation { 12 | override fun transform(source: Bitmap): Bitmap { 13 | val size = Math.min(source.width, source.height) 14 | 15 | val x = (source.width - size) / 2 16 | val y = (source.height - size) / 2 17 | 18 | val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size) 19 | if (squaredBitmap != source) { 20 | source.recycle() 21 | } 22 | 23 | val bitmap = Bitmap.createBitmap(size, size, source.config) 24 | 25 | val canvas = Canvas(bitmap) 26 | val paint = Paint() 27 | val shader = BitmapShader( 28 | squaredBitmap, 29 | Shader.TileMode.CLAMP, Shader.TileMode.CLAMP 30 | ) 31 | paint.shader = shader 32 | paint.isAntiAlias = true 33 | 34 | val r = size / 2f 35 | canvas.drawCircle(r, r, r, paint) 36 | 37 | squaredBitmap.recycle() 38 | return bitmap 39 | } 40 | 41 | override fun key(): String { 42 | return "circular" 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/CircularView.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Path 6 | import android.graphics.PointF 7 | import android.util.AttributeSet 8 | import android.view.View 9 | 10 | class CircularView @JvmOverloads constructor( 11 | context: Context, 12 | attrs: AttributeSet? = null, 13 | defStyleAttr: Int = 0 14 | ) : View(context, attrs, defStyleAttr) { 15 | 16 | var radius: Float = 2000f 17 | set(value) { 18 | field = value 19 | invalidate() 20 | } 21 | var center: PointF = PointF(0f, 0f) 22 | set(value) { 23 | field = value 24 | invalidate() 25 | } 26 | 27 | var centerX: Float 28 | get() = center.x 29 | set(value) { 30 | center = PointF(value, center.y) 31 | } 32 | 33 | var centerY: Float 34 | get() = center.y 35 | set(value) { 36 | center = PointF(center.x, value) 37 | } 38 | 39 | override fun draw(canvas: Canvas?) { 40 | val path = Path().apply { 41 | addCircle(center.x, center.y, radius, Path.Direction.CW) 42 | } 43 | 44 | path.let { canvas?.clipPath(it) } 45 | super.draw(canvas) 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/ColorEvaluator.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.animation.TypeEvaluator 4 | import android.animation.ValueAnimator 5 | 6 | 7 | /* 8 | See https://gist.github.com/aritraroy/f5aac68cf42e83270d71f9bf58fac19c 9 | */ 10 | class ColorEvaluator : TypeEvaluator { 11 | 12 | override fun evaluate(fraction: Float, startValue: Int, endValue: Int): Int { 13 | val startA = (startValue shr 24 and 0xff) / 255.0f 14 | var startR = (startValue shr 16 and 0xff) / 255.0f 15 | var startG = (startValue shr 8 and 0xff) / 255.0f 16 | var startB = (startValue and 0xff) / 255.0f 17 | 18 | val endA = (endValue shr 24 and 0xff) / 255.0f 19 | var endR = (endValue shr 16 and 0xff) / 255.0f 20 | var endG = (endValue shr 8 and 0xff) / 255.0f 21 | var endB = (endValue and 0xff) / 255.0f 22 | 23 | // convert from sRGB to linear 24 | startR = eocfsRGB(startR) 25 | startG = eocfsRGB(startG) 26 | startB = eocfsRGB(startB) 27 | 28 | endR = eocfsRGB(endR) 29 | endG = eocfsRGB(endG) 30 | endB = eocfsRGB(endB) 31 | 32 | // compute the interpolated color in linear space 33 | var a = startA + fraction * (endA - startA) 34 | var r = startR + fraction * (endR - startR) 35 | var g = startG + fraction * (endG - startG) 36 | var b = startB + fraction * (endB - startB) 37 | 38 | // convert back to sRGB in the [0..255] range 39 | a *= 255.0f 40 | r = oecfsRGB(r) * 255.0f 41 | g = oecfsRGB(g) * 255.0f 42 | b = oecfsRGB(b) * 255.0f 43 | 44 | return Math.round(a) shl 24 or (Math.round(r) shl 16) or (Math.round(g) shl 8) or Math.round( 45 | b 46 | ) 47 | } 48 | 49 | companion object { 50 | 51 | /** 52 | * Returns an instance of `ColorEvaluator` that may be used in 53 | * [ValueAnimator.setEvaluator]. The same instance may 54 | * be used in multiple `Animator`s because it holds no state. 55 | * 56 | * @return An instance of `ColorEvaluator`. 57 | */ 58 | val instance = ColorEvaluator() 59 | 60 | /** 61 | * Opto-electronic conversion function for the sRGB color space 62 | * Takes a gamma-encoded sRGB value and converts it to a linear sRGB value 63 | */ 64 | internal fun oecfsRGB(linear: Float): Float { 65 | // IEC 61966-2-1:1999 66 | return if (linear <= 0.0031308f) 67 | linear * 12.92f 68 | else 69 | (Math.pow(linear.toDouble(), 1.0 / 2.4) * 1.055 - 0.055).toFloat() 70 | } 71 | 72 | /** 73 | * Electro-optical conversion function for the sRGB color space 74 | * Takes a linear sRGB value and converts it to a gamma-encoded sRGB value 75 | */ 76 | internal fun eocfsRGB(srgb: Float): Float { 77 | // IEC 61966-2-1:1999 78 | return if (srgb <= 0.04045f) srgb / 12.92f else Math.pow( 79 | (srgb + 0.055) / 1.055, 80 | 2.4 81 | ).toFloat() 82 | } 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/GlobalPositionTransition.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.animation.Animator 4 | import android.animation.ObjectAnimator 5 | import android.graphics.PointF 6 | import android.util.Property 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.transition.Transition 10 | import androidx.transition.TransitionValues 11 | 12 | class GlobalPositionTransition : Transition() { 13 | companion object { 14 | private const val POSITION_X = "RadiusTransition:positionX" 15 | private const val POSITION_Y = "RadiusTransition:positionY" 16 | private const val PARENT_WINDOW_POSITION_X = "RadiusTransition:parentPositionX" 17 | private const val PARENT_WINDOW_POSITION_Y = "RadiusTransition:parentPositionY" 18 | } 19 | 20 | override fun captureStartValues(transitionValues: TransitionValues) = 21 | captureValues(transitionValues) 22 | 23 | override fun captureEndValues(transitionValues: TransitionValues) = 24 | captureValues(transitionValues) 25 | 26 | override fun createAnimator( 27 | sceneRoot: ViewGroup, 28 | startValues: TransitionValues?, 29 | endValues: TransitionValues? 30 | ): Animator? { 31 | startValues ?: return null 32 | endValues ?: return null 33 | 34 | val startX = startValues.values[POSITION_X] as Float 35 | val startY = startValues.values[POSITION_Y] as Float 36 | val startParentWindowX = startValues.values[PARENT_WINDOW_POSITION_X] as Int 37 | val startParentWindowY = startValues.values[PARENT_WINDOW_POSITION_Y] as Int 38 | val endX = endValues.values[POSITION_X] as Float 39 | val endY = endValues.values[POSITION_Y] as Float 40 | val endParentWindowX = endValues.values[PARENT_WINDOW_POSITION_X] as Int 41 | val endParentWindowY = endValues.values[PARENT_WINDOW_POSITION_Y] as Int 42 | 43 | val startXInEndViewCoordinates = (startX + startParentWindowX) - endParentWindowX 44 | val startYInEndViewCoordinates = (startY + startParentWindowY) - endParentWindowY 45 | 46 | val path = pathMotion.getPath( 47 | startXInEndViewCoordinates, 48 | startYInEndViewCoordinates, 49 | endX, 50 | endY 51 | ) 52 | 53 | return ObjectAnimator.ofFloat( 54 | endValues.view, 55 | PathProperty(object : Property(PointF::class.java, "point") { 56 | override fun set(view: View?, value: PointF?) { 57 | view ?: return 58 | value ?: return 59 | 60 | view.x = value.x 61 | view.y = value.y 62 | } 63 | 64 | override fun get(view: View?): PointF? { 65 | view ?: return null 66 | return PointF(view.x, view.y) 67 | } 68 | }, path), 69 | 0f, 1f 70 | ) 71 | } 72 | 73 | private fun captureValues(transitionValues: TransitionValues) { 74 | val parent = transitionValues.view.parent as? ViewGroup ?: return 75 | 76 | val locationOfParentInWindow = IntArray(2) 77 | parent.getLocationInWindow(locationOfParentInWindow) 78 | 79 | transitionValues.values[POSITION_X] = transitionValues.view.x 80 | transitionValues.values[POSITION_Y] = transitionValues.view.y 81 | transitionValues.values[PARENT_WINDOW_POSITION_X] = locationOfParentInWindow[0] 82 | transitionValues.values[PARENT_WINDOW_POSITION_Y] = locationOfParentInWindow[1] 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/PaintView.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Path 7 | import android.util.AttributeSet 8 | import android.view.View 9 | 10 | class PaintView @JvmOverloads constructor( 11 | context: Context, 12 | attrs: AttributeSet? = null, 13 | defStyleAttr: Int = 0 14 | ) : View(context, attrs, defStyleAttr) { 15 | 16 | private val paths = mutableListOf>() 17 | 18 | operator fun plusAssign(pathAndColor: Pair) { 19 | paths.add(pathAndColor) 20 | invalidate() 21 | } 22 | 23 | private val paint: Paint = Paint().apply { 24 | style = Paint.Style.STROKE 25 | strokeWidth = 16f 26 | isAntiAlias = true 27 | strokeJoin = Paint.Join.ROUND 28 | } 29 | 30 | override fun onDraw(canvas: Canvas?) { 31 | super.onDraw(canvas) 32 | canvas ?: return 33 | 34 | paths.forEach { 35 | paint.color = it.second 36 | canvas.drawPath(it.first, paint) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/PathProperty.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.graphics.Path 4 | import android.graphics.PathMeasure 5 | import android.graphics.PointF 6 | import android.util.Property 7 | 8 | class PathProperty(private val mProperty: Property, path: Path) : 9 | Property(Float::class.java, mProperty.name) { 10 | private val pathMeasure: PathMeasure = PathMeasure(path, false) 11 | private val pathLength: Float 12 | private val position = FloatArray(2) 13 | private val pointF = PointF() 14 | private var currentFraction: Float = 0.toFloat() 15 | 16 | init { 17 | pathLength = pathMeasure.length 18 | } 19 | 20 | override fun get(target: T): Float? { 21 | return currentFraction 22 | } 23 | 24 | override fun set(target: T, fraction: Float?) { 25 | currentFraction = fraction!! 26 | pathMeasure.getPosTan(pathLength * fraction, position, null) 27 | pointF.x = position[0] 28 | pointF.y = position[1] 29 | mProperty.set(target, pointF) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/common/RadiusTransition.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.common 2 | 3 | import android.animation.Animator 4 | import android.animation.ObjectAnimator 5 | import android.animation.PropertyValuesHolder 6 | import android.graphics.PointF 7 | import android.view.ViewGroup 8 | import androidx.transition.Transition 9 | import androidx.transition.TransitionValues 10 | 11 | class RadiusTransition : Transition() { 12 | companion object { 13 | private const val RADIUS = "RadiusTransition:radius" 14 | private const val CENTER_X = "RadiusTransition:centerX" 15 | private const val CENTER_Y = "RadiusTransition:centerY" 16 | } 17 | 18 | override fun captureStartValues(transitionValues: TransitionValues) = 19 | captureValues(transitionValues) 20 | 21 | override fun captureEndValues(transitionValues: TransitionValues) = 22 | captureValues(transitionValues) 23 | 24 | override fun createAnimator( 25 | sceneRoot: ViewGroup, 26 | startValues: TransitionValues?, 27 | endValues: TransitionValues? 28 | ): Animator? { 29 | startValues ?: return null 30 | endValues ?: return null 31 | val circularView = endValues.view as? CircularView ?: return null 32 | 33 | val startRadius = startValues.values[RADIUS] as Float 34 | val endRadius = endValues.values[RADIUS] as Float 35 | 36 | val startCenterX = startValues.values[CENTER_X] as Float 37 | val endCenterX = endValues.values[CENTER_X] as Float 38 | 39 | val startCenterY = startValues.values[CENTER_Y] as Float 40 | val endCenterY = endValues.values[CENTER_Y] as Float 41 | 42 | val radius = PropertyValuesHolder.ofFloat("radius", startRadius, endRadius) 43 | val centerX = PropertyValuesHolder.ofFloat("centerX", startCenterX, endCenterX) 44 | val centerY = PropertyValuesHolder.ofFloat("centerY", startCenterY, endCenterY) 45 | 46 | circularView.radius = startRadius 47 | circularView.center = PointF(startCenterX, startCenterY) 48 | 49 | return ObjectAnimator.ofPropertyValuesHolder(circularView, radius, centerX, centerY) 50 | } 51 | 52 | private fun captureValues(transitionValues: TransitionValues) { 53 | val circularView = transitionValues.view as? CircularView ?: return 54 | transitionValues.values[RADIUS] = circularView.radius 55 | transitionValues.values[CENTER_X] = circularView.center.x 56 | transitionValues.values[CENTER_Y] = circularView.center.y 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/coordinatorlayout/CoordinatorLayoutExample.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.coordinatorlayout 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.TextView 6 | import com.karumi.androidanimations.R 7 | import com.karumi.androidanimations.coordinatorlayout.CoordinatorLayoutFragment.Example.Custom 8 | import com.karumi.androidanimations.coordinatorlayout.CoordinatorLayoutFragment.Example.Default 9 | import com.karumi.androidanimations.coordinatorlayout.CoordinatorLayoutFragment.Example.Exercise 10 | 11 | 12 | interface CoordinatorLayoutExample { 13 | class VH(itemView: View) : com.afollestad.recyclical.ViewHolder(itemView) { 14 | val row: View = itemView.findViewById(R.id.row) 15 | val exampleName: TextView = itemView.findViewById(R.id.exampleName) 16 | } 17 | 18 | class Binder( 19 | val getContext: () -> Context 20 | ) { 21 | operator fun invoke( 22 | receiver: VH, 23 | item: CoordinatorLayoutFragment.Example, 24 | onClick: () -> Unit 25 | ) = receiver.bind(item, onClick) 26 | 27 | private fun VH.bind(item: CoordinatorLayoutFragment.Example, onClick: () -> Unit) { 28 | exampleName.text = when (item) { 29 | Default -> R.string.coordinator_layout_default 30 | Custom -> R.string.coordinator_custom_behavior 31 | Exercise -> R.string.coordinator_exercise 32 | }.let { getContext().getString(it) } 33 | row.setOnClickListener { onClick() } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/coordinatorlayout/CoordinatorLayoutExerciseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.coordinatorlayout 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.Rect 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import androidx.recyclerview.widget.DividerItemDecoration 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.afollestad.recyclical.RecyclicalSetup 14 | import com.afollestad.recyclical.ViewHolder 15 | import com.afollestad.recyclical.datasource.dataSourceOf 16 | import com.afollestad.recyclical.setup 17 | import com.afollestad.recyclical.withItem 18 | import com.airbnb.lottie.LottieDrawable 19 | import com.karumi.androidanimations.R 20 | import com.karumi.androidanimations.base.BaseFragment 21 | import kotlinx.android.synthetic.main.fragment_coordinator_layout_exercise.* 22 | 23 | class CoordinatorLayoutExerciseFragment : BaseFragment() { 24 | override fun onCreateView( 25 | inflater: LayoutInflater, 26 | container: ViewGroup?, 27 | savedInstanceState: Bundle? 28 | ): View? = inflater.inflate(R.layout.fragment_coordinator_layout_exercise, container, false) 29 | 30 | @SuppressLint("SetTextI18n") 31 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 32 | super.onViewCreated(view, savedInstanceState) 33 | 34 | animationView.setAnimation(R.raw.material_wave_loading) 35 | animationView.repeatMode = LottieDrawable.RESTART 36 | animationView.repeatCount = LottieDrawable.INFINITE 37 | 38 | val layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) 39 | val dataSource = dataSourceOf((0..20).map { Item(it) }) 40 | 41 | allItems.setup { 42 | withLayoutManager(layoutManager) 43 | withDataSource(dataSource) 44 | withItem(R.layout.view_coordinator_exercise) { 45 | onBind(::VH) { _, item -> name.text = "#${item.id}" } 46 | } 47 | withVerticalOffset(layoutManager) 48 | } 49 | } 50 | 51 | private fun RecyclicalSetup.withVerticalOffset(layoutManager: LinearLayoutManager) { 52 | val dividerItemDecoration = object : DividerItemDecoration( 53 | requireContext(), 54 | layoutManager.orientation 55 | ) { 56 | override fun getItemOffsets( 57 | outRect: Rect, 58 | view: View, 59 | parent: RecyclerView, 60 | state: RecyclerView.State 61 | ) { 62 | super.getItemOffsets(outRect, view, parent, state) 63 | outRect.bottom = 64 | requireContext().resources.getDimension(R.dimen.margin2x).toInt() 65 | } 66 | } 67 | allItems.addItemDecoration(dividerItemDecoration) 68 | } 69 | 70 | class VH(itemView: View) : ViewHolder(itemView) { 71 | val name: TextView = itemView.findViewById(R.id.name) 72 | } 73 | 74 | data class Item(val id: Int) 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/coordinatorlayout/CoordinatorLayoutFragment.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.coordinatorlayout 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.navigation.fragment.findNavController 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import com.afollestad.recyclical.datasource.dataSourceOf 10 | import com.afollestad.recyclical.setup 11 | import com.afollestad.recyclical.withItem 12 | import com.karumi.androidanimations.R 13 | import com.karumi.androidanimations.base.BaseFragment 14 | import kotlinx.android.synthetic.main.fragment_view_animation.* 15 | 16 | class CoordinatorLayoutFragment : BaseFragment() { 17 | 18 | private val coordinatorLayoutExample = CoordinatorLayoutExample.Binder(::requireContext) 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View? = inflater.inflate(R.layout.fragment_coordinator_layout, container, false) 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | configureExamplesList() 29 | } 30 | 31 | private fun configureExamplesList() { 32 | val layoutManager = LinearLayoutManager(requireContext()) 33 | val dataSource = dataSourceOf( 34 | *Example.values() 35 | ) 36 | 37 | allAnimations.setup { 38 | withLayoutManager(layoutManager) 39 | withDataSource(dataSource) 40 | 41 | withItem(R.layout.view_coordinator_layout_example) { 42 | onBind(CoordinatorLayoutExample::VH) { _, item -> 43 | coordinatorLayoutExample(this, item) { 44 | val directions = when (item) { 45 | Example.Default -> CoordinatorLayoutFragmentDirections 46 | .actionCoordinatorLayoutFragmentToDefaultBehaviorFragment() 47 | Example.Custom -> CoordinatorLayoutFragmentDirections 48 | .actionCoordinatorLayoutFragmentToCustomBehaviorFragment() 49 | Example.Exercise -> CoordinatorLayoutFragmentDirections 50 | .actionCoordinatorLayoutFragmentToCoordinatorLayoutExerciseFragment() 51 | } 52 | findNavController().navigate(directions) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | enum class Example { 60 | Default, Custom, Exercise 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/coordinatorlayout/CustomBehaviorFragment.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.coordinatorlayout 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.Rect 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import androidx.recyclerview.widget.DividerItemDecoration 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.afollestad.recyclical.RecyclicalSetup 14 | import com.afollestad.recyclical.ViewHolder 15 | import com.afollestad.recyclical.datasource.dataSourceOf 16 | import com.afollestad.recyclical.setup 17 | import com.afollestad.recyclical.withItem 18 | import com.karumi.androidanimations.R 19 | import com.karumi.androidanimations.base.BaseFragment 20 | import kotlinx.android.synthetic.main.fragment_custom_behavior.* 21 | 22 | 23 | @SuppressLint("RestrictedApi") 24 | class CustomBehaviorFragment : BaseFragment() { 25 | override fun onCreateView( 26 | inflater: LayoutInflater, 27 | container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View? = inflater.inflate(R.layout.fragment_custom_behavior, container, false) 30 | 31 | @SuppressLint("SetTextI18n") 32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 33 | super.onViewCreated(view, savedInstanceState) 34 | 35 | val layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) 36 | val dataSource = dataSourceOf((0..10).map { Item(it) }) 37 | 38 | allItems.setup { 39 | withLayoutManager(layoutManager) 40 | withDataSource(dataSource) 41 | withItem(R.layout.view_custom_behavior_item) { 42 | onBind(::VH) { _, item -> name.text = "#${item.id}" } 43 | } 44 | withHorizontalOffset(layoutManager) 45 | } 46 | 47 | } 48 | 49 | private fun RecyclicalSetup.withHorizontalOffset(layoutManager: LinearLayoutManager) { 50 | val dividerItemDecoration = object : DividerItemDecoration( 51 | requireContext(), 52 | layoutManager.orientation 53 | ) { 54 | override fun getItemOffsets( 55 | outRect: Rect, 56 | view: View, 57 | parent: RecyclerView, 58 | state: RecyclerView.State 59 | ) { 60 | super.getItemOffsets(outRect, view, parent, state) 61 | outRect.right = 62 | requireContext().resources.getDimension(R.dimen.margin2x).toInt() 63 | } 64 | } 65 | allItems.addItemDecoration(dividerItemDecoration) 66 | } 67 | 68 | class VH(itemView: View) : ViewHolder(itemView) { 69 | val name: TextView = itemView.findViewById(R.id.name) 70 | } 71 | 72 | data class Item(val id: Int) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/coordinatorlayout/DefaultBehaviorFragment.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.coordinatorlayout 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 9 | import androidx.coordinatorlayout.widget.CoordinatorLayout 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import com.afollestad.recyclical.ViewHolder 12 | import com.afollestad.recyclical.datasource.dataSourceOf 13 | import com.afollestad.recyclical.setup 14 | import com.afollestad.recyclical.withItem 15 | import com.google.android.material.appbar.AppBarLayout 16 | import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS 17 | import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 18 | import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP 19 | import com.karumi.androidanimations.R 20 | import com.karumi.androidanimations.base.BaseFragment 21 | import com.karumi.androidanimations.extensions.px 22 | import kotlinx.android.synthetic.main.activity_main.* 23 | import kotlinx.android.synthetic.main.fragment_default_behaviors.* 24 | 25 | @SuppressLint("RestrictedApi") 26 | class DefaultBehaviorFragment : BaseFragment() { 27 | override fun onCreateView( 28 | inflater: LayoutInflater, 29 | container: ViewGroup?, 30 | savedInstanceState: Bundle? 31 | ): View? = inflater.inflate(R.layout.fragment_default_behaviors, container, false).also { 32 | setToolbarScrollFlags(SCROLL_FLAG_SCROLL or SCROLL_FLAG_SNAP or SCROLL_FLAG_ENTER_ALWAYS) 33 | setToolbarHeight(200.px) 34 | requireActivity().floatingActionButton.visibility = View.VISIBLE 35 | } 36 | 37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 38 | super.onViewCreated(view, savedInstanceState) 39 | configureItems() 40 | } 41 | 42 | override fun onDestroyView() { 43 | setToolbarScrollFlags(0) 44 | setToolbarHeight(WRAP_CONTENT) 45 | requireActivity().floatingActionButton.visibility = View.GONE 46 | super.onDestroyView() 47 | } 48 | 49 | private fun setToolbarScrollFlags(flags: Int) { 50 | val toolbar = requireActivity().collapsingToolbarLayout 51 | val params = toolbar.layoutParams as AppBarLayout.LayoutParams 52 | toolbar.isTitleEnabled = true 53 | params.scrollFlags = flags 54 | toolbar.layoutParams = params 55 | } 56 | 57 | private fun setToolbarHeight(heightInPx: Int) { 58 | val appBarLayout = requireActivity().appBarLayout 59 | val params = appBarLayout.layoutParams as CoordinatorLayout.LayoutParams 60 | params.height = heightInPx 61 | appBarLayout.layoutParams = params 62 | } 63 | 64 | private fun configureItems() { 65 | val layoutManager = LinearLayoutManager(requireContext()) 66 | val dataSource = dataSourceOf((0..100).map { Item }) 67 | 68 | allItems.setup { 69 | withLayoutManager(layoutManager) 70 | withDataSource(dataSource) 71 | 72 | withItem(R.layout.view_default_behaviors_item) { 73 | onBind(::VH) { _, _ -> } 74 | } 75 | } 76 | } 77 | 78 | class VH(itemView: View) : ViewHolder(itemView) 79 | object Item 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/coordinatorlayout/ParallaxBehavior.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.coordinatorlayout 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.graphics.Matrix 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import android.widget.ImageView 9 | import android.widget.Scroller 10 | import androidx.coordinatorlayout.widget.CoordinatorLayout 11 | 12 | class ParallaxBehavior( 13 | context: Context, 14 | attrs: AttributeSet 15 | ) : CoordinatorLayout.Behavior(context, attrs) { 16 | 17 | private val parallaxFactor = 0.2f 18 | private var isAnimatingFling = false 19 | private var previousFlingX = 0 20 | private var previousFlingY = 0 21 | private val scroller = Scroller(context) 22 | 23 | override fun onStartNestedScroll( 24 | coordinatorLayout: CoordinatorLayout, 25 | child: ImageView, 26 | directTargetChild: View, 27 | target: View, 28 | axes: Int, 29 | type: Int 30 | ): Boolean { 31 | // We stop fling animations on any scrolling stop 32 | isAnimatingFling = false 33 | return true 34 | } 35 | 36 | override fun onNestedScroll( 37 | coordinatorLayout: CoordinatorLayout, 38 | child: ImageView, 39 | target: View, 40 | dxConsumed: Int, 41 | dyConsumed: Int, 42 | dxUnconsumed: Int, 43 | dyUnconsumed: Int, 44 | type: Int 45 | ) { 46 | child.imageMatrix = child.imageMatrix 47 | .translateWithParallax(dxConsumed.toFloat(), dyConsumed.toFloat()) 48 | } 49 | 50 | private fun Matrix.translateWithParallax( 51 | dx: Float, 52 | dy: Float 53 | ): Matrix = Matrix(this) 54 | .apply { postTranslate(-parallaxFactor * dx, -parallaxFactor * dy) } 55 | 56 | override fun onNestedFling( 57 | coordinatorLayout: CoordinatorLayout, 58 | child: ImageView, 59 | target: View, 60 | velocityX: Float, 61 | velocityY: Float, 62 | consumed: Boolean 63 | ): Boolean { 64 | val canScroll = if (Math.abs(velocityX) > 0) { 65 | target.canScrollHorizontally(velocityX.toInt()) 66 | } else { 67 | target.canScrollVertically(velocityX.toInt()) 68 | } 69 | 70 | if (!canScroll) { 71 | return false 72 | } 73 | 74 | scroller.fling( 75 | 0, 76 | 0, 77 | velocityX.toInt(), 78 | velocityY.toInt(), 79 | 0, 80 | 0, 81 | Int.MIN_VALUE, 82 | Int.MAX_VALUE 83 | ) 84 | 85 | previousFlingX = 0 86 | previousFlingY = 0 87 | isAnimatingFling = true 88 | ValueAnimator.ofFloat(0f, 1f).apply { 89 | addUpdateListener { animateImageViewParallax(it, child) } 90 | duration = scroller.duration.toLong() 91 | start() 92 | } 93 | return true 94 | } 95 | 96 | private fun animateImageViewParallax( 97 | animator: ValueAnimator, 98 | child: ImageView 99 | ) { 100 | if (!isAnimatingFling) { 101 | animator.cancel() 102 | return 103 | } 104 | 105 | scroller.computeScrollOffset() 106 | val currX = scroller.currX 107 | val currY = scroller.currY 108 | val xDiff = currX - previousFlingX.toFloat() 109 | val yDiff = currY - previousFlingY.toFloat() 110 | previousFlingX = currX 111 | previousFlingY = currY 112 | child.imageMatrix = child.imageMatrix.translateWithParallax(xDiff, yDiff) 113 | } 114 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/drawablegraphicsanimation/DrawableGraphicsAnimationFragment.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.drawablegraphicsanimation 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import com.afollestad.recyclical.datasource.dataSourceOf 9 | import com.afollestad.recyclical.setup 10 | import com.afollestad.recyclical.withItem 11 | import com.karumi.androidanimations.R 12 | import com.karumi.androidanimations.base.BaseFragment 13 | import kotlinx.android.synthetic.main.fragment_animated_drawable_graphics.* 14 | 15 | class DrawableGraphicsAnimationFragment : BaseFragment() { 16 | 17 | val drawableGraphicsSimpleAnimationBinder = 18 | DrawableGraphicsSimpleAnimation.Binder(::requireContext) 19 | val drawableGraphicsExerciseAnimationBinder = 20 | DrawableGraphicsExerciseAnimation.Binder(::requireContext) 21 | val drawableGraphicsLottieAnimationBinder = 22 | DrawableGraphicsLottieAnimation.Binder(::requireContext) 23 | 24 | override fun onCreateView( 25 | inflater: LayoutInflater, 26 | container: ViewGroup?, 27 | savedInstanceState: Bundle? 28 | ): View? = inflater.inflate(R.layout.fragment_animated_drawable_graphics, container, false) 29 | 30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 31 | super.onViewCreated(view, savedInstanceState) 32 | configureAllAnimations() 33 | } 34 | 35 | private fun configureAllAnimations() { 36 | val layoutManager = LinearLayoutManager(requireContext()) 37 | val dataSource = dataSourceOf( 38 | *DrawableGraphicsAnimation.values(), 39 | Lottie, 40 | Exercise 41 | ) 42 | 43 | allAnimations.setup { 44 | withLayoutManager(layoutManager) 45 | withDataSource(dataSource) 46 | 47 | withItem(R.layout.view_simple_drawable_graphics_animation) { 48 | onBind(DrawableGraphicsSimpleAnimation::VH) { _, item -> 49 | drawableGraphicsSimpleAnimationBinder(this, item) 50 | } 51 | } 52 | withItem(R.layout.view_lottie_drawable_graphics_animation) { 53 | onBind(DrawableGraphicsLottieAnimation::VH) { _, _ -> 54 | drawableGraphicsLottieAnimationBinder(this) 55 | } 56 | } 57 | withItem(R.layout.view_exercise_drawable_graphics_animation) { 58 | onBind(DrawableGraphicsExerciseAnimation::VH) { _, _ -> 59 | drawableGraphicsExerciseAnimationBinder(this) 60 | } 61 | } 62 | } 63 | } 64 | 65 | enum class DrawableGraphicsAnimation { 66 | Keyframe, Vector 67 | } 68 | 69 | object Lottie 70 | object Exercise 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/drawablegraphicsanimation/DrawableGraphicsExerciseAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.drawablegraphicsanimation 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.Button 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import com.karumi.androidanimations.R 9 | 10 | interface DrawableGraphicsExerciseAnimation { 11 | class VH(itemView: View) : com.afollestad.recyclical.ViewHolder(itemView) { 12 | val name: TextView = itemView.findViewById(R.id.animationName) 13 | val keyframeAnimationView: ImageView = itemView.findViewById(R.id.keyframeAnimationView) 14 | val vectorizedAnimationView: ImageView = itemView.findViewById(R.id.vectorizedAnimationView) 15 | val lottieAnimationView: ImageView = itemView.findViewById(R.id.lottieAnimationView) 16 | val playButton: Button = itemView.findViewById(R.id.playButton) 17 | val stopButton: Button = itemView.findViewById(R.id.stopButton) 18 | } 19 | 20 | class Binder(val getContext: () -> Context) { 21 | operator fun invoke(receiver: VH) = receiver.bind() 22 | 23 | private fun VH.bind() { 24 | name.text = R.string.drawable_graphics_animation_exercise 25 | .let { getContext().getString(it) } 26 | 27 | TODO( 28 | """ 29 | | Render the animation that can be controlled with two buttons, one to play 30 | | and one to stop. 31 | | You have the same animation in all formats: keyframe, vectorized and 32 | | lottie. Configure all of them one by one. 33 | | """.trimMargin() 34 | ) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/drawablegraphicsanimation/DrawableGraphicsLottieAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.drawablegraphicsanimation 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.widget.TextView 6 | import com.airbnb.lottie.LottieAnimationView 7 | import com.airbnb.lottie.LottieDrawable 8 | import com.karumi.androidanimations.R 9 | 10 | interface DrawableGraphicsLottieAnimation { 11 | class VH(itemView: View) : com.afollestad.recyclical.ViewHolder(itemView) { 12 | val name: TextView = itemView.findViewById(R.id.animationName) 13 | val animationView: LottieAnimationView = itemView.findViewById(R.id.animationView) 14 | } 15 | 16 | class Binder(val getContext: () -> Context) { 17 | operator fun invoke(receiver: VH) = receiver.bind() 18 | 19 | private fun VH.bind() { 20 | name.text = 21 | R.string.drawable_graphics_animation_with_lottie.let { getContext().getString(it) } 22 | animate(animationView) 23 | } 24 | 25 | private fun animate(view: LottieAnimationView) { 26 | view.setAnimation(R.raw.material_wave_loading) 27 | view.repeatMode = LottieDrawable.RESTART 28 | view.repeatCount = LottieDrawable.INFINITE 29 | view.playAnimation() 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/drawablegraphicsanimation/DrawableGraphicsSimpleAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.drawablegraphicsanimation 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.AnimationDrawable 5 | import android.graphics.drawable.Drawable 6 | import android.view.View 7 | import android.widget.TextView 8 | import androidx.appcompat.widget.AppCompatImageView 9 | import androidx.vectordrawable.graphics.drawable.Animatable2Compat 10 | import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat 11 | import com.karumi.androidanimations.R 12 | import com.karumi.androidanimations.drawablegraphicsanimation.DrawableGraphicsAnimationFragment.DrawableGraphicsAnimation 13 | import com.karumi.androidanimations.extensions.exhaustive 14 | 15 | interface DrawableGraphicsSimpleAnimation { 16 | class VH(itemView: View) : com.afollestad.recyclical.ViewHolder(itemView) { 17 | val name: TextView = itemView.findViewById(R.id.animationName) 18 | val animationView: AppCompatImageView = itemView.findViewById(R.id.animationView) 19 | } 20 | 21 | class Binder(val getContext: () -> Context) { 22 | operator fun invoke( 23 | receiver: VH, 24 | item: DrawableGraphicsAnimation 25 | ) = receiver.bind(item) 26 | 27 | private fun VH.bind(item: DrawableGraphicsAnimation) { 28 | name.text = getAnimationName(item) 29 | animate(item, animationView) 30 | } 31 | 32 | private fun getAnimationName(item: DrawableGraphicsAnimation): String = when (item) { 33 | DrawableGraphicsAnimation.Keyframe -> R.string.drawable_graphics_animation_by_keyframe 34 | DrawableGraphicsAnimation.Vector -> R.string.drawable_graphics_animation_with_vector 35 | }.let { getContext().getString(it) } 36 | 37 | private fun animate(item: DrawableGraphicsAnimation, view: AppCompatImageView) { 38 | when (item) { 39 | DrawableGraphicsAnimation.Keyframe -> { 40 | view.setBackgroundResource(R.drawable.keyframe_animation) 41 | (view.background as AnimationDrawable).start() 42 | } 43 | DrawableGraphicsAnimation.Vector -> { 44 | val animation = AnimatedVectorDrawableCompat.create( 45 | getContext(), 46 | R.drawable.vectorized_animation 47 | )?.apply { 48 | repeatAnimation() 49 | start() 50 | } 51 | view.setImageDrawable(animation) 52 | } 53 | }.exhaustive 54 | } 55 | 56 | private fun AnimatedVectorDrawableCompat.repeatAnimation() { 57 | registerAnimationCallback(object : Animatable2Compat.AnimationCallback() { 58 | override fun onAnimationEnd(drawable: Drawable?) { 59 | start() 60 | } 61 | }) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/extensions/StringResourceResolver.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.extensions 2 | 3 | import android.content.Context 4 | 5 | interface StringResourceResolver { 6 | fun Int.resolve(context: Context): String = context.getString(this) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/extensions/animatePath.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.extensions 2 | 3 | import android.animation.ObjectAnimator 4 | import android.animation.ValueAnimator 5 | import android.graphics.Path 6 | import android.graphics.PathMeasure 7 | import android.os.Build 8 | import android.view.View 9 | 10 | fun animatePath(view: View, path: Path): ValueAnimator = 11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 12 | ObjectAnimator.ofFloat(view, View.X, View.Y, path) 13 | } else { 14 | val coordinates = FloatArray(2) 15 | val pathMeasure = PathMeasure(path, true) 16 | ValueAnimator.ofFloat(0f, pathMeasure.length).apply { 17 | addUpdateListener { 18 | val distance = it.animatedValue as Float 19 | pathMeasure.getPosTan(distance, coordinates, null) 20 | view.translationX = coordinates[0] 21 | view.translationY = coordinates[1] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/extensions/exhaustive.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.extensions 2 | 3 | val Any?.exhaustive: Unit get() = Unit -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/extensions/pixel.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.extensions 2 | 3 | import android.content.res.Resources 4 | 5 | val Int.dp: Int 6 | get() = (this / Resources.getSystem().displayMetrics.density).toInt() 7 | 8 | val Int.px: Int 9 | get() = (this * Resources.getSystem().displayMetrics.density).toInt() -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/extensions/transitions.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.extensions 2 | 3 | import android.view.View 4 | import androidx.core.view.ViewCompat 5 | import androidx.navigation.fragment.FragmentNavigator 6 | import androidx.navigation.fragment.FragmentNavigatorExtras 7 | 8 | fun sharedElements(vararg views: View): FragmentNavigator.Extras = 9 | FragmentNavigatorExtras(*views.map { it to it.transitionNameCompat }.toTypedArray()) 10 | 11 | val View.transitionNameCompat: String 12 | get() = ViewCompat.getTransitionName(this) ?: "" -------------------------------------------------------------------------------- /app/src/main/java/com/karumi/androidanimations/layouttransition/ExerciseLayoutTransition.kt: -------------------------------------------------------------------------------- 1 | package com.karumi.androidanimations.layouttransition 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.Button 7 | import androidx.annotation.LayoutRes 8 | import androidx.transition.Scene 9 | import com.karumi.androidanimations.R 10 | 11 | interface ExerciseLayoutTransition { 12 | class VH(itemView: View) : com.afollestad.recyclical.ViewHolder(itemView) { 13 | val masterScene: ViewGroup = itemView.findViewById(R.id.masterScene) 14 | } 15 | 16 | class Binder(val getContext: () -> Context) { 17 | operator fun invoke(receiver: VH) = 18 | receiver.bind() 19 | 20 | private fun VH.bind() { 21 | masterScene.findViewById