Распутина Татьяна
Воспроизведение кадров во времени с некоторой трансформацией и сглаживанием (рендеринг)
Code ScaleAnimation:
view.startAnimation(
ScaleAnimation(0f, 1f, 0f, 1f, 0f, view.height.toFloat())
.apply {
duration = 300
interpolator = AccelerateInterpolator()
fillAfter = true
}
)
view.startAnimation(
AnimationUtils.loadAnimation(context, R.anim.pulse)
)
XML pulse animation:
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/animation_time_1500"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite"
android:repeatMode="reverse"
android:toXScale="0.9"
android:toYScale="0.9"/>
XML shake animation:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator">
<translate
android:duration="@integer/animation_time_50"
android:fromXDelta="-5"
android:repeatCount="2"
android:repeatMode="reverse"
android:toXDelta="5"/>
</set>
<animation-list android:id="@+id/icon" android:oneshot="false">
<item android:drawable="@drawable/icon0" android:duration="@integer/animation_fast" />
<item android:drawable="@drawable/icon1" android:duration="@integer/animation_fast" />
<item android:drawable="@drawable/icon2" android:duration="@integer/animation_fast" />
<item android:drawable="@drawable/icon3" android:duration="@integer/animation_fast" />
<item android:drawable="@drawable/icon4" android:duration="@integer/animation_fast" />
</animation-list>
val animationDuration = resources.getInteger(R.integer.animation_time_100).toLong()
val animator = ValueAnimator.ofInt(start, end)
.apply {
duration = animationDuration
repeatCount = 1
repeatMode = ValueAnimator.REVERSE
addUpdateListener { animation ->
view.setPadding(
view.paddingLeft,
animation.animatedValue as Int,
view.paddingRight,
view.paddingBottom
)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animator: Animator?) { }
override fun onAnimationEnd(animator: Animator?) { }
override fun onAnimationCancel(animator: Animator?) { }
override fun onAnimationRepeat(animator: Animator?) { }
})
}
.also { it.start() }
Material Interpolators - более плавные
Property:
XML:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially">
<objectAnimator
android:propertyName="alpha"
android:duration="500"
android:valueFrom="0f"
android:valueTo="1f"/>
<objectAnimator
android:propertyName="rotation"
android:duration="500"
android:valueType="floatType"
android:valueFrom="0f"
android:valueTo="90f" />
</set>
val animator = AnimatorInflater
.loadAnimator(context, R.animator.animator_set)
.let { it as AnimatorSet }
.apply { setTarget(view) }
.also { it.start() }
Code:
val pulseAnimator = ObjectAnimator
.ofPropertyValuesHolder(
view,
PropertyValuesHolder.ofFloat(View.SCALE_X, 1f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f)
)
.apply {
duration = resources.getInteger(R.integer.animation_time_500).toLong()
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
setAutoCancel(true)
}
.also { it.start() }
val start = view.translationY
val end = start + update
val animator = ObjectAnimator.ofFloat(view, "translationY", start, end)
.apply { duration = resources.getInteger(R.integer.animation_time_200).toLong() }
.also { it.start() }
Code:
val animator1 = AnimatorInflater.loadAnimator(context, R.animator.animator_1)
val animator2 = AnimatorInflater.loadAnimator(context, R.animator.animator_2)
val animator3 = AnimatorInflater.loadAnimator(context, R.animator.animator_3)
val animator4 = AnimatorInflater.loadAnimator(context, R.animator.animator_4)
val animator = AnimatorSet()
.apply {
play(animator1)
.before(animator2)
.with(animator3)
.after(animator4)
}
.also { it.start() }
view.animate().alpha(0f).alphaBy(1f).start()
view.animate().x(500f).y(500f)
// animate absolute position
// was x=100, y=100
// start1: x=500, y=500
// start2: x=500, y=500
view.animate().xBy(500f).yBy(500f)
// animate absolute position by value
// was x=100, y=100
// start1: x=100+500, y=100+500
// start2: x=100+500+500, y=100+500+500
view.animate().translationX(500f).translationY(500f)
// animate left-top position by value
// was x=100, y=100
// start1: x=100+500, y=100+500
// start2: x=100+500, y=100+500
view.animate().translationXBy(500f).translationYBy(500f)
// animate left-top position by value
// was x=100, y=100
// start1: x=100+500, y=100+500
// start2: x=100+500+500, y=100+500+500
Android 4.4+:
Android 5.+:
Android 6.+:
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="sequential">
<fade android:fadingMode="fade_out">
<targets>
<target android:targetId="@id/view" />
</targets>
</fade>
<changeBounds/>
<fade android:fadingMode="fade_in"/>
</transitionSet>
TransitionManager.beginDelayedTransition(
viewGroup,
TransitionInflater.from(viewGroup.context).inflateTransition(R.transition.transition_set)
)
val newSize = resources.getDimensionPixelSize(R.dimen.size_large)
view.layoutParams = view.layoutParams
.apply {
width = newSize
height = newSize
}
val transitions = TransitionSet()
.apply {
addTransition(Slide())
addTransition(ChangeBounds())
addTarget(R.id.view)
addListener(object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition?) {}
override fun onTransitionEnd(transition: Transition?) {}
override fun onTransitionResume(transition: Transition?) {}
override fun onTransitionPause(transition: Transition?) {}
override fun onTransitionCancel(transition: Transition?) {}
})
ordering = TransitionSet.ORDERING_TOGETHER // ORDERING_SEQUENTIAL
duration = resources.getInteger(R.integer.animation_time_500).toLong()
interpolator = AccelerateInterpolator()
}
TransitionManager.beginDelayedTransition(viewGroup, transitions)
res/layout/view_root.xml
<FrameLayout
android:id="@+id/viewGroup"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/view_scene1"/>
</FrameLayout>
Code:
val scene2: Scene = Scene
.getSceneForLayout(viewGroup, R.layout.view_scene2, this)
viewGroup.setOnClickListener {
val transitionSet = TransitionSet()
.apply {
addTransition(Fade())
addTransition(ChangeBounds())
addTransition(ChangeImageTransform())
ordering = TransitionSet.ORDERING_TOGETHER
duration = 1000L
interpolator = AccelerateInterpolator()
}
TransitionManager.go(scene2, transitionSet)
}
res/layout/view_scene1.xml
<FrameLayout
android:id="@+id/viewGroupScene1"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/targetView"
android:layout_width="@dimen/target_width_scene1"
android:layout_height="@dimen/target_height_scene1"
android:src="@drawable/ic_image"
android:scaleType="centerCrop"
android:layout_gravity="top|center_horizontal"/>
</FrameLayout>
res/layout/view_scene2.xml
<FrameLayout
android:id="@+id/viewGroupScene2"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/targetView"
android:layout_width="@dimen/target_width_scene2"
android:layout_height="@dimen/target_height_scene2"
android:src="@drawable/ic_image"
android:scaleType="fitXY"
android:layout_gravity="bottom|center_horizontal"/>
</FrameLayout>
val flingAnimation = FlingAnimation(view, DynamicAnimation.X)
.apply {
setStartVelocity(500f)
friction = 0.5f
}
.also { it.start() }
val springAnimation = SpringAnimation(view, DynamicAnimation.SCALE_X)
.apply {
spring = SpringForce()
.apply {
finalPosition = view.x
dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
stiffness = SpringForce.STIFFNESS_LOW
}
setStartVelocity(1000f)
}
.also { it.start() }
<animated-vector
android:drawable="@drawable/vector_drawable">
<target
android:name="start"
android:animation="@anim/start_animation" />
<target
android:name="end"
android:animation="@anim/end_animation"/>
</animated-vector>
In res/values/theme.xml:
<item name="android:windowContentTransitions">true</item>
<item name="android:windowEnterTransition">@transition/transition_fade</item>
<item name="android:windowExitTransition">@transition/transition_fade</item>
<item name="android:windowSharedElementEnterTransition">@transition/transition_fade</item>
<item name="android:windowSharedElementExitTransition">@transition/transition_fade</item>
In res/layout/activity_layout.xml
<ImageView
android:id="@+id/transitionView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:transitionName="transitionViewName"
... />
In ActivityStart.kt launch transition:
val activityOptionsCompat: ActivityOptionsCompat = ActivityOptionsCompat
.makeSceneTransitionAnimation(this, transitionView, "transitionViewName")
val intent = Intent(this, ActivityEnd::class.java)
startActivity(intent, activityOptionsCompat.toBundle())
In res/layout/activity_layout.xml
<ImageView
android:id="@+id/transitionView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:transitionName="transitionViewName"
... />
In StartFragment.kt:
val endFragment = EndFragment.newInstance()
// possibly need to check: Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
endFragment.setSharedElementEnterTransition(ChangeImageTransform())
endFragment.setSharedElementReturnTransition(ChangeImageTransform())
endFragment.setEnterTransition(Slide())
this.setExitTransition(Slide())
activity.supportFragmentManager
.beginTransaction()
.addSharedElement(transitionView, "transitionViewName")
.replace(R.id.container, endFragment)
.addToBackStack(null)
.commit()
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<fragment
android:id="@+id/drawerMenu"
android:name="com.intership.example.DrawerMenuFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
tools:layout="@layout/fragment_drawer_menu"/>
</androidx.drawerlayout.widget.DrawerLayout>
drawerLayout.openDrawer(drawerMenu)
drawerLayout.closeDrawer(drawerMenu)
drawerLayout.openDrawer(GravityCompat.START)
drawerLayout.closeDrawer(GravityCompat.START)
// проверить текущее состояние Drawer
drawerLayout.isDrawerOpen(drawerMenu)
drawerLayout.isDrawerOpen(GravityCompat.START)
// разблокировать для пользователя
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
// заблокировать в закрытом состоянии
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
// заблокировать в открытом состоянии
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN)
// заблокировать в состоянии по умолчанию
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED)
drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
override fun onDrawerStateChanged(newState: Int) {
// DrawerLayout.STATE_IDLE
// DrawerLayout.STATE_DRAGGING
// DrawerLayout.STATE_SETTLING
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
override fun onDrawerClosed(drawerView: View) {}
override fun onDrawerOpened(drawerView: View) {}
})
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/favorite_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="normal"
app:layout_anchor="@id/appBarLayout"
app:layout_anchorGravity="bottom|end"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:src="@drawable/ic_fab_image"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
A MotionLayout is a ConstraintLayout which allows you to animate layouts between various states. (© Docs)
Обратная совместимость: Android API >= 14 (IceCreamSandwich 4.0, 4.0.1, 4.0.2)
Полностью декларативный: можно описать сцены любой сложности в XML
<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/scene_main"
app:applyMotionScene="true"
app:progress="0.0"
app:currentState="@id/start"
app:motionDebug="SHOW_ALL"
tools:showPaths="true">
<View
android:id="@+id/cardBackgroundView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/shape_white_large_cornered_bottom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/cardLayoutGuideline"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/cardLayoutGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"/>
<View android:id="@+id/view" ... />
</androidx.constraintlayout.motion.widget.MotionLayout>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@id/start"
motion:constraintSetEnd="@id/end"
motion:motionInterpolator="linear"
motion:duration="1000">
<OnSwipe
motion:touchAnchorId="@id/cardBackgroundView"
motion:touchAnchorSide="bottom"
motion:dragDirection="dragUp"
motion:touchRegionId="@id/cardBackgroundView"/>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/cardLayoutGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
motion:layout_constraintGuide_percent="0.8"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/cardLayoutGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
motion:layout_constraintGuide_percent="0.05"/>
</ConstraintSet>
</MotionScene>
Важно: нужно определить CustomAttribute в start и end <ConstraintSet>
<CustomAttribute> supported types:
<Constraint
android:id="@id/cardBackgroundView" ...>
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="#0099ff"/>
</Constraint>
<android.support.constraint.utils.ImageFilterView
android:id="@+id/imageFilterView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/accent"
android:src="@drawable/ic_start"
app:altSrc="@drawable/ic_end" />
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/imageFilterView" ...>
<CustomAttribute
motion:attributeName="crossfade"
motion:customColorValue="0"/>
<CustomAttribute
motion:attributeName="saturation"
<-- 0 = grayscale; 1 = original, 2 = hyper saturated -->
motion:customFloatValue="1" />
<CustomAttribute
motion:attributeName="contrast"
<-- 0 = gray; 1 = original, 2 = hyper contrast -->
motion:customFloatValue="1" />
<CustomAttribute
motion:attributeName="warmth"
<-- 0.5 = cold (blue); 1 = original, 2 = warm (red) -->
motion:customFloatValue="1" />
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/imageFilterView" ...>
<-- exactly same list of <CustomAttribute> -->
</Constraint>
</ConstraintSet>
<Transition ...>
<KeyFrameSet>
<KeyPosition
motion:keyPositionType="parentRelative"
motion:percentY="0.25"
motion:framePosition="50"
motion:target="@id/imageView"/>
<KeyAttribute
motion:rotation="90"
motion:framePosition="100"
motion:motionTarget="@id/recyclerView"/>
<KeyAttribute
motion:alpha="0"
motion:framePosition="0"
motion:motionTarget="@id/view"/>
<KeyAttribute
motion:alpha="0"
motion:framePosition="75"
motion:motionTarget="@id/view"/>
<KeyAttribute
motion:alpha="1"
motion:framePosition="100"
motion:motionTarget="@id/view"/>
</KeyFrameSet>
</Transition>
From res/raw: lottie_rawRes
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_rawRes="@raw/animation"
<-- Loop indefinitely -->
app:lottie_loop="true"
<-- Start playing as soon as the animation is loaded -->
app:lottie_autoPlay="true" />
From assets/: lottie_fileName
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieAnimationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="animation.json"
<-- Repeat count: Integer -->
app:lottie_repeatCount="-1"
<-- Repeat mode: reverse / restart -->
app:lottie_repeatMode="reverse" />