I'm currently trying to make a universal method for an animation overlay. Works this way: pass activity as a parameter -> method attaches required views onto Activity's rootview, invalidates it and starts in this case rotate animation. However the animation's pivot doesn't apply to view correctly, everything works except that, so it just rotates from View's X and Y, which is incorrect.
The method:
fun createLoading(activity: Activity, activityWidth: Int): Animation {
val loadingAnimation = AnimationUtils.loadAnimation(activity, R.anim.animation_loading_rotate)
val parent = activity.window.decorView.findViewById<ViewGroup>(android.R.id.content)
val loadingBg = ImageView(activity)
loadingBg.tag = "customLoadingBg"
loadingBg.layoutParams = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT)
loadingBg.visibility = View.VISIBLE
loadingBg.setImageResource(R.drawable.darken_background)
val loadingImage = ImageView(activity)
loadingImage.tag = "customLoadingImage"
loadingImage.setImageResource(R.drawable.icon_web)
loadingImage.visibility = View.VISIBLE
loadingImage.layoutParams = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT) //TODO pivot doesn't work. Again...
loadingImage.layoutParams.width = (activityWidth * 0.1).toInt()
loadingImage.layoutParams.height = (activityWidth * 0.1).toInt()
loadingImage.pivotX = (activityWidth * 0.05).toFloat()
loadingImage.pivotY = (activityWidth * 0.05).toFloat()
loadingImage.x = ((activityWidth / 2 - (activityWidth * 0.1 / 2).toInt()).toFloat())
loadingImage.y = 0f
parent.addView(loadingBg)
parent.addView(loadingImage)
loadingAnimation!!.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
parent.removeView(parent.findViewWithTag<ImageView>("customLoadingImage"))
parent.removeView(parent.findViewWithTag<FrameLayout>("customLoadingBg"))
}
override fun onAnimationStart(animation: Animation?) {
loadingBg.alpha = 0.5f
loadingImage.pivotX = (activityWidth * 0.05).toFloat()
loadingImage.pivotY = (activityWidth * 0.05).toFloat()
Log.d("loadingimg_pivotX", loadingImage.pivotX.toString())
loadingBg.bringToFront()
loadingImage.bringToFront()
}
})
loadingImage.post {
activity.runOnUiThread {
parent.invalidate()
parent.getChildAt(parent.childCount - 1).startAnimation(loadingAnimation)
}
}
return loadingAnimation
}
As you can see I even tried to put it into the animation's onAnimationStart method, but doesn't work.
I even tried using a predefined XML animation and applying to the view, but neither of those worked out for me.
Appreciate any ideas.
EDIT:
Declaration of the R.anim.animation_loading_rotate
android:duration="750"
android:fromDegrees="0"
android:interpolator="#android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite"
android:toDegrees="360"
Would be nice to have sample, to check it locally. I'm not sure, about which pivot you are talking about in this sample. Pivot in our case, should be relative to the View under animation, not parent. So in our case, if it's rotation, the right changes would be next.
loadingImage.pivotX = (loadingImage.getWidth() * 0.5).toFloat()
loadingImage.pivotY = (loadingImage.getHeight() * 0.5).toFloat()
Last note, I strongly recommend to stick with Android Property Animation not View Animation. You can simply replace your sample with ObjectAnimator, and probably you issue could be resolved. Because ObjectAnimator not just animate but navigates View changes, where simple View animation just redrawing object. Check more detail from the documentation.
Related
I want to observe onFling Function to detect Velocityx and Velocity Means Swiping force/Velocity. In android we attach this to the android view.But donot find a way how to call this in Jetpack compose or alternative function for this in jetpack compose...?
Please take the Animation-in-Compose codelab to understand better, but for now, here's how you can achieve something similar
private fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
// This `Animatable` stores the horizontal offset for the element.
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// Wait for a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
// Interrupt any ongoing animation.
offsetX.stop()
// Prepare for drag events and record velocity of a fling.
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Record the position after offset
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
// Overwrite the `Animatable` value while the element is dragged.
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
change.consumePositionChange()
}
}
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Calculate where the element eventually settles after the fling animation.
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
// The animation should end as soon as it reaches these bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back to the default position.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
}
}
}
// Apply the horizontal offset to the element.
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
I would not recommend trying to comprehend this code just from here, you should probably refer to the documentation at hand, while taking the codelab as well. This implements a swipe-to-dismiss functionality on a simple list item in a compose-sample app, the code to which you will find linked to in the start of the codelab. Here are all the sample apps published for compose, for your reference.
I'm writing an Android app where a user has to select an option using a ChipGroup and Chips.
Everything is working fine, it's just a bit clunky as there is no animation except the default ripple when selecting a Chip.
I've read the Material Design 3 Docs and found this video showing a nice animation that I'd like to implement, but I don't know how.
I've tried:
enabling
android:animateLayoutChanges="true"
but that only animates the adding and removing of a Chip, not the checking and unchecking.
using
TransitionManager.beginDelayedTransition(chipGroup);
and that works fine on the chipGroup but the content of the Chip (tick appearing and text rescaling) does not animate.
Please tell me if I'm doing something wrong, here is also the method I use to add and select those Chips:
ChipAdapter adapter = new ChipAdapter(getContext());
for(int i = 0; i < adapter.getCount(); i++){
View chip = adapter.getView(i, chipGroup, chipGroup);
if(chip instanceof Chip) {
chip.setId(i);
chip.setOnClickListener(v -> {
for(int p = 0; p < chipGroup.getChildCount(); p++){
chipGroup.getChildAt(p).setSelected(false);
}
chip.setSelected(true);
});
chipGroup.addView(chip);
}
}
Update: Appended Jetpack Compose answer.
With XML
So as far as I can tell there is no embed way to simply enable this animation, but I found two ways to mimic the animation shown in your linked video.
Results
Option 1
Option 2
Code & Explaination
Option 1
This option works by enabling the animateLayoutChanges option of the ChipGroup that contains your chips
android:animateLayoutChanges="true"
and adding the following code for your ChipGroup:
for (view in chipGroup.children) {
val chip = view as Chip
chip.setOnCheckedChangeListener { buttonView, _ ->
val index = chipGroup.indexOfChild(buttonView)
chipGroup.removeView(buttonView)
chipGroup.addView(buttonView, index)
}
}
This code will automatically remove the chip and instantly add it back to the ChipGroup whenever the selection state of a chip changes.
Drawbacks
The animation is rather a transition of the form stateBefore -> invisible -> stateAfter than stateBefore -> stateAfter what results in the chip "flashing".
Option 2
For this option add the following custom Chip class (Kotlin) to your project and change your chips to be instances of CheckAnimationChip instead of com.google.android.material.chip.Chip:
import android.animation.ObjectAnimator
import android.content.Context
import android.util.AttributeSet
import androidx.core.animation.doOnEnd
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
private const val CHIP_ICON_SIZE_PROPERTY_NAME = "chipIconSize"
// A value of '0f' would be interpreted as 'use the default size' by the ChipDrawable, so use a slightly larger value.
private const val INVISIBLE_CHIP_ICON_SIZE = 0.00001f
/**
* Custom Chip class which will animate transition between the [isChecked] states.
*/
class CheckAnimationChip #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.chipStyle
) : Chip(context, attrs, defStyleAttr) {
private var onCheckedChangeListener: OnCheckedChangeListener? = null
private var _chipDrawable: ChipDrawable
private var defaultCheckedIconSize: Float
private var currentlyScalingDown = false
var animationDuration = 200L
init {
// Set default values for this category of chip.
isCheckable = true
isCheckedIconVisible = true
_chipDrawable = chipDrawable as ChipDrawable
defaultCheckedIconSize = _chipDrawable.chipIconSize
super.setOnCheckedChangeListener { buttonView, isChecked ->
if (currentlyScalingDown) {
// Block the changes caused by the scaling-down animation.
return#setOnCheckedChangeListener
}
onCheckedChangeListener?.onCheckedChanged(buttonView, isChecked)
if (isChecked) {
scaleCheckedIconUp()
} else if (!isChecked) {
scaleCheckedIconDown()
}
}
}
/**
* Scale the size of the Checked-Icon from invisible to its default size.
*/
private fun scaleCheckedIconUp() {
ObjectAnimator.ofFloat(_chipDrawable, CHIP_ICON_SIZE_PROPERTY_NAME,
INVISIBLE_CHIP_ICON_SIZE, defaultCheckedIconSize)
.apply {
duration = animationDuration
start()
doOnEnd {
_chipDrawable.chipIconSize = defaultCheckedIconSize
}
}
}
/**
* Scale the size of the Checked-Icon from its default size down to invisible. To achieve this, the
* [isChecked] property needs to be manipulated. It is set to be true till the animation has ended.
*/
private fun scaleCheckedIconDown() {
currentlyScalingDown = true
isChecked = true
ObjectAnimator.ofFloat(_chipDrawable, CHIP_ICON_SIZE_PROPERTY_NAME,
defaultCheckedIconSize, INVISIBLE_CHIP_ICON_SIZE)
.apply {
duration = animationDuration
start()
doOnEnd {
isChecked = false
currentlyScalingDown = false
_chipDrawable.chipIconSize = defaultCheckedIconSize
}
}
}
override fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) {
onCheckedChangeListener = listener
}
}
This class changes the size of the chip's icon by using an ObjectAnimator. Therefore it accesses the ChipDrawable of the chip and changes the chipIconSize property with the animator.
Drawbacks (rather picky)
This will only animate the icon size and not a complete transition between the drawables of the chip like in the linked video (e.g. there is no smooth transition of the border or background in this implementation).
You can observe a flickering of adjacent chips during the animation (see chip "Last 4 Weeks" in the gif), however I could only observe this problem on the emulator and did not notice it on a physical device.
Jetpack Compose
In Jetpack Compose you can make use of the animateConentSize() Modifier:
FilterChip(
selected = selected,
onClick = { /* Handle Click */ },
leadingIcon = {
Box(
Modifier.animateContentSize(keyframes { durationMillis = 200 })
) {
if (selected) {
Icon(
imageVector = Icons.Default.Done,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
}
},
label = { /* Text */ }
)
The important part here is to always have a composable (here the Box) for the leadingIcon that holds the check-icon if the chip is selected and is empty if not. This composable can then be animated smoothly with the animateContentSize() modifier.
I Have the below layout.
<CoordinatorLayout>
<AppBarLayout>
<CollapsingToolbarLayout>
<Toolbar>
</CollapsingToolbarLayout>
<View1>
<View2>
</AppBarLayout>
<RecyclerView>(appbar_scrolling_view_behavior)
On one use case, I use an empty state viewholder to show that there are empty items from api.Also I have to hardcode the height of the recyclerview relative to the screenheight. As a result, on scrolling of the recyclerview fully up there is some space left below the empty state of the recyclerview.I do not want this space.Can anyone please help me?
recyclerview.scrollToPosition(0)
val params = appbar.layoutParams as CoordinatorLayout.LayoutParams
val behavior = params.behavior as AppBarLayout.Behavior?
if (behavior != null) {
val valueAnimator: ValueAnimator = ValueAnimator.ofInt()
valueAnimator.interpolator = DecelerateInterpolator()
valueAnimator.addUpdateListener { animation ->
behavior.topAndBottomOffset = (animation.animatedValue as Int)
appbar.requestLayout()
}
valueAnimator.setIntValues(
0, -appbar.totalScrollRange + view2.measuredHeight
)
valueAnimator.duration = 50
valueAnimator.start()
}
Found this approach to work after trying other approaches.
I'm using a ValueAnimator within animateChange() of my custom ItemAnimator to update the height of both the newHolder and oldHolder via setting oldHolder.itemView.layoutParams and newHolder.itemView.layoutParams with updated params.
The newHolder's view animates correctly. The oldHolder's view is stuck in a state where isLayoutRequested() == true and never redraws when its new layout parameters are set.
Does anyone know what's going on?
ValueAnimator.ofInt(INITIAL_HEIGHT, FINAL_HEIGHT).apply {
duration = ANIMATION_DURATION
addUpdateListener {
val newHeight = animatedValue as Int
val oldLp = oldHolder.itemView.layoutParams
oldLp.height = newHeight
oldHolder.itemView.layoutParams = oldLp
val newLp = newHolder.itemView.layoutParams
newLp.height = newHeight
newHolder.itemView.layoutParams = newLp
}
start()
}
I've left out some boilerplate for clarity. I extend DefaultItemAnimator and do dispatch change start/end in line with the animation beginning and ending.
I'm using Transitions-Everywhere for my app, and I wonder if I can set the transition speed/time of the transitions..
I have something like this:
TransitionManager().beginDelayedTransition(animLayout, new ChangeBounds());
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) findViewById(R.id.myId).getLayoutParams();
layoutParams.height = (int) myHeight;
layoutParams.width = (int) myWidth;
myLayout.setLayoutParams(layoutParams);
In short, I want the transition to be slower than the default, but I just can't figure out how to set the speed of the transition!
You can set the duration on the Transition you pass into beginDelayedTransition(). In your case that would be ChangeBounds. So try something like this:
final ChangeBounds transition = new ChangeBounds();
transition.setDuration(600L); // Sets a duration of 600 milliseconds
TransitionManager().beginDelayedTransition(animLayout, transition);
By default if no duration is set a Transition falls back to the default animation duration which is 300ms. So for example if you want the transition to take twice as long, use 600ms.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
this.defaultPeekHeight = Resources.getSystem().displayMetrics.heightPixels / 4
bottomSheetBehavior.peekHeight = defaultPeekHeight
presenter.loadItems()
}
override fun onItemsLoaded(
items: List<RecyclerItem<*>>
) {
updateItems(items)
b.root.doOnLayout {
//animate expanding of bottom sheet
val transition = ChangeBounds()
transition.duration = ANIMATION_DURATION
TransitionManager.beginDelayedTransition(b.draggableBottomSheetLayout, transition)
bottomSheetBehavior.peekHeight = max(b.root.height, defaultPeekHeight)
}}