I want to implement shared element transition in my app, when one activity's recycler view item transforms into another activity like here: https://storage.googleapis.com/spec-host-backup/mio-design%2Fassets%2F15N3n1xwTt0briEbfIvFUG01pMv2d_xaT%2F02-focus-focalelement-do.mp4. (source: https://material.io/design/motion/choreography.html#using-a-focal-element)
Namely, the item is fading out and changes bounds then the new activity fade in. As far as I understand it is simple AutoTransition, but it doesn't work. Simple fading doesn't work as well.
Thus, for now I achieve only that the item gets background of new activity an then changes its bounds.
So, I ended up by adding recycler view item's layout in the resulting activity layout. The data (e.g. the title, etc.) of the clicked item is transferred to the next activity with intent.putExtra(). Shared elements in such case will be of course the item's root view and resulting activity's root view. When activity starts I set the data of the item to matching views in activity via SharedElementCallback, e.g.:
setEnterSharedElementCallback(
object : SharedElementCallback() {
override fun onSharedElementStart(...) {
val title = intent.getStringExtra(TITLE)
activity_item_title.text = title
........
}
override fun onSharedElementEnd(...) {
}
})
This allows to show exactly the same item view at the beginning of the transition. Then it should start change its bounds, fading out this item's view at the same time. And at some moment (e.g. in the middle of the transition) when the initial view completely fades out, the laouyt of the activity shows up, fading in gradually. To do this we need to hide item's view in the middle of the transition (View.visibility = View.GONE) and make activity views visible. Probably this is not the best way, but I solve this by adding a listener to shared element enter transition and used Handler().postDelayed:
window.sharedElementEnterTransition.addListener(
object : android.transition.Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {}
override fun onTransitionResume(transition: Transition) {}
override fun onTransitionPause(transition: Transition) {}
override fun onTransitionCancel(transition: Transition) {}
override fun onTransitionStart(transition: Transition) {
Handler().postDelayed({
activity_item.visibility = View.GONE
activity_view_1.visibility = View.VISIBLE
activity_view_2.visibility = View.VISIBLE
.............
.............
// Also you could e.g. set the background to your activity here, ets.
activity_view_root.background = backgroundDrawable
}, 150) //suppuse that the whole transition length is 300ms
}
}
})
The transition animations themselves could look as follows:
<transitionSet>
<targets>
<target android:targetId="#id/activity_root_view"/>
</targets>
<transition
class="com.organization.app.utils.FadeOutTransition"
android:duration="150"/>
<transition
class="com.organization.app.utils.FadeInTransition"
android:startDelay="150"/>
<changeBounds android:duration="300"/>
</transitionSet>
Here, custom FadeOutTransition and FadeInTransition were used since simple android <fade/> animation doesn't work with shared elements. These classes are similar to that given in the answer here: Why Fade transition doesn't work on Shared Element.
The steps for creating return transition are similar.
Related
I'm trying to animate an ImageView with a list of pictures, iterating through them quickly, so that
it creates the impression of an animation. Sadly it does give the alert: Skipped 391 frames! The application may be doing too much work on its main thread.
This is my Main-Activity which implements the Animator-interface.
override fun onCreate(savedInstanceState: Bundle?) {
GlobalScope.launch {
show(Action.Idle)
}
}
My Animator show() function (runs the Runnables with android.os.Handler):
fun show(action: Action){
getHandler().removeCallbacksAndMessages(null)
when (action) {
Action.MoveRight -> getHandler().post(getRight())
Action.MoveUp -> getHandler().post(getUp())
Action.MoveDown -> getHandler().post(getDown())
Action.MoveLeft -> getHandler().post(getLeft())
Action.Idle -> getHandler().post(idle())
}
}
My idle function (anim.idle is an array of Ints/Drawables):
fun idle() = object : Runnable{
override fun run() {
val anim = getAnimation()
repeat(anim.idle!!.size){
getActivity().runOnUiThread {
binding.figure.setImageResource(anim.idle[it])
binding.figure.invalidate()
}
Thread.sleep(500)
}
getHandler().postDelayed(this,5000)
}
}
You can use AnimationDrawable to have the ImageView show a sequence of pictures, see also the guide to Animate drawable graphics
In a nutshell, you declare a animation-list drawable resource and set it as the View background (or foreground, according to your requirements). You can then obtain the AnimationDrawable from the View at runtime and use it to start the animation.
In my small example, I used some vector drawables with different tints
to create the animation-list in res/drawable/colored_androids.xml:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
<item android:drawable="#drawable/ic_android_blue_24dp" android:duration="250"/>
<item android:drawable="#drawable/ic_android_mint_24dp" android:duration="250"/>
<item android:drawable="#drawable/ic_android_green_24dp" android:duration="250"/>
<item android:drawable="#drawable/ic_android_mint_24dp" android:duration="250"/>
</animation-list>
My Activity has a field for the AnimationDrawable
private lateinit var coloredAndroidsAnimation: AnimationDrawable
and in onCreate() the following lines are required for the setup:
findViewById<ImageView>(R.id.iv_animated).apply {
setBackgroundResource(R.drawable.colored_androids)
coloredAndroidsAnimation = background as AnimationDrawable
setOnClickListener{
coloredAndroidsAnimation.start()
}
}
While I don't think you can change a view that is attached to the UI from a background thread, you can create/edit views on a background thread (I do this lots)
So a possible solution might be to programmatically create a series of ImageViews in the background. It might then be possible to measure them with the same parameters as the UI in the background thread as measurement can be costly (not sure if they would be re-measured when added to the UI)
Then in the UI thread your loop would add and remove Views from the parent view.
I am trying to create a progress bar that will display while an image is downloading from a server. This image is loaded into a custom view. (I need it to be custom because I draw on the image.)
My solution was to add the custom view into the XML under the layout of the fragment, and mark its visibility as Visibility.GONE. This worked in the XML editor, as the progress bar took up the full space. Invisible did not work as it's position was still displayed.
The issue comes when the image path is given to my custom view. It would seem that setting Visibility.GONE on a view means that the view is not measured. But I need the dimensions of the view to measure how large the bitmap should be.
// Create the observer which updates the UI.
val photoObserver = Observer<String?> { photoPath ->
spinner.visibility = View.GONE
thumbnailFrame.visibility = View.VISIBLE
thumbnailFrame.invalidate()
thumbnailFrame.setImage(photoPath)
Looking at the Logs from the custom view, it is calling onMeasured() but it is doing it too late. I need onMeasure() to be called before setImage(). Is there a better way of handling this and if not is there a way to force the code to wait until I know the view has finished its measuring process?
Solved using a basic listener pattern with an anonymous class inline. I'm not sure if there is a better way but this way works just fine. Delay is not much of an issue since the view draws quite fast anyways.
* Set a listener to notify parent fragment/activity when view has been measured
*/
fun setViewReadyListener(thumbnailHolder: ViewReadyListener) {
holder = thumbnailHolder
}
interface ViewReadyListener {
fun onViewSet()
}
private fun notifyViewReadyListener() {
holder?.onViewSet()
}
spinner.visibility = View.INVISIBLE
thumbnailFrame.visibility = View.VISIBLE
//We have to make sure that the view is finished measuring before we attempt to put in a picture
thumbnailFrame.setViewReadyListener(object : ThumbnailFrame.ViewReadyListener {
override fun onViewSet() {
thumbnailFrame.setImage(photoPath)
//If we have a previous saved state, load it here
val radius = viewModel.thumbnailRadius
val xPosit = viewModel.thumbnailXPosit
val yPosit = viewModel.thumbnailYPosit
if (radius != null) {
thumbnailFrame.setRadius(radius)
}
if (xPosit != null) {
thumbnailFrame.setRadius(xPosit)
}
if (yPosit != null) {
thumbnailFrame.setRadius(yPosit)
}
}
})
}
I have following transition in my app, but the animation to the new destination lags because of extensive loading into recyclerview and other graphic elements.
I have tried to use postDelayed when loading elements on the recyclerview, but on some devices you feel you are waiting after transition and others it take longer time to do the transition (device dependent). So I landed on 700ms, but it is not good for all...
Is there a way (i.e. callbacks) where I can detect when the transition has actually confirmed ended.
My code is farly simple, when clicking a button in one fragment I call this:
private fun onDetailClick(stationId: String, itemView: View)
{
(itemView.context as MainActivity).bottom_navigation.visibility = View.GONE
StationDetailRepository.readStationCurrentPrices2(stationId)
val direction = StationListFragmentDirections.actionStationListFragmentToStationDetailFragment(stationId)
try {
Log.i(TAG,"trns: before navigate")
itemView.findNavController()?.navigate(direction)
Log.i(TAG,"trns: after navigate")
}catch(iae: IllegalArgumentException)
{
iae.printStackTrace()
}
catch (ise: IllegalStateException)
{
ise.printStackTrace()
}
}
Simply, how can I detect that this navigation actually has ended, and there set a viewModel share which I can observe in the other Fragment, so can trigger the loading of elements in a sane manner ?
RG
I've implemented shared element transitions for my app, where the transition begins on an image from a Fragment (with RecyclerView) inside a ViewPager on the home screen and expands into full screen gallery view, again within a Fragment in a ViewPager. This is all working fine except that if the image is not fully visible it goes on top of the TabBar before expanding into full screen. Here's what's happening:
My enter transition looks like this:
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android">
<targets>
<target android:excludeId="#android:id/statusBarBackground"/>
<target android:excludeId="#android:id/navigationBarBackground"/>
</targets>
</fade>
And exit:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together"
android:duration="500">
<fade>
<targets>
<target android:excludeId="#android:id/statusBarBackground" />
<target android:excludeId="#android:id/navigationBarBackground" />
</targets>
</fade>
</transitionSet>
And in the shared element callback of the calling activity I've got this:
View navigationBar = activity.findViewById(android.R.id.navigationBarBackground);
View statusBar = activity.findViewById(android.R.id.statusBarBackground);
if (navigationBar != null) {
names.add(navigationBar.getTransitionName());
sharedElements.put(navigationBar.getTransitionName(), navigationBar);
}
if (statusBar != null) {
names.add(statusBar.getTransitionName());
sharedElements.put(statusBar.getTransitionName(), statusBar);
}
Finally in styles.xml for the activity theme:
<item name="android:windowContentTransitions">true</item>
<item name="android:windowEnterTransition">#transition/details_window_enter_transition</item>
<item name="android:windowReturnTransition">#transition/details_window_return_transition</item>
I don't really understand how the toolbar (or actionbar) can be excluded by the transition without getting this overlap. Perhaps a way to do it would be to somehow force the image to be clipped at the top part so that it doesn't become fully visible when under the ToolBar and expands only from the visible rectangle.
I've tried adding <target android:excludeId="#id/action_bar_container"/> to the targets of the animation but the same thing happens still.
Any suggestions are welcome.
I found a similar problem in my project.Add below code in your style.
<item name="android:windowSharedElementsUseOverlay">false</item>
It works for me.
I came up with a temporary workaround. Before the shared element transition is executed, the calling activity checks whether the target view is within the bounds of the RecyclerView. If not, the RecyclerView is smooth-scrolled to show the full view and once that is finished the transition runs. If the view is fully visible, then transitions runs normally.
// Scroll to view and run animation when scrolling completes.
recycler.smoothScrollToPosition(adapterPosition);
recycler.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
recycler.getViewTreeObserver().removeOnPreDrawListener(this);
// Open activity here.
return true;
}
});
Here's the result, not too bad until I find a better solution:
I guess i have the proper answer for this question. This issue happening because on the second activity you do not have toolbar at all, so it can not be added to transition as shared element. So in my case i added some 'fake toolbar' to my second activity, which have 0dp height. Then you can add toolbar from first activity as a shared element, and give him change bounds animation, so toolbar will collapse at the same time as image and image will no longer be 'over' toolbar.
My 'fake toolbar' view:
<View
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#color/colorPrimary"
android:elevation="10dp"
android:outlineProvider="none"
android:transitionName="toolbar_transition" />
important notes:
-view have to have non-transparent background
-i added elevation to be sure that my view i 'over' image
-elevation causes shadow, whick i did not wanted, so i set outilenProvider as none
Next all you have to do is add your toolbar to shared elements
sharedElements.add(new Pair<>(toolbar, "toolbar_transition"));
I searched everywhere and couldn't find any solution so I figured myself. Here is a recorded demo (at half the speed) to show the result (with and without the fix).
Please checkout the full working demo here:
https://github.com/me-abhinav/shared-element-overlap-demo
Steps
Let us say we have two activities viz. MainActivity which has a scrolling container with a grid/list of thumbnails, and we have a SecondActivity which shows the image in a slideshow in fullscreen.
Please checkout the full code to completely understand the solution.
Inside your MainActivity which hosts the scrolling container, set a click listener on your thumbnail to open SecondActivity:
ImageView imageView = findViewById(R.id.image_view);
imageView.setOnClickListener(v -> {
// Set the transition name. We could also do it in the xml layout but this is to demo
// that we can choose any name generated dynamically.
String transitionName = getString(R.string.transition_name);
imageView.setTransitionName(transitionName);
// This part is important. We first need to clip this view to only its visible part.
// We will also clip the corresponding view in the SecondActivity using shared element
// callbacks.
Rect localVisibleRect = new Rect();
imageView.getLocalVisibleRect(localVisibleRect);
imageView.setClipBounds(localVisibleRect);
mClippedView = imageView;
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
intent.putExtra(SecondActivity.EXTRA_TRANSITION_NAME, transitionName);
intent.putExtra(SecondActivity.EXTRA_CLIP_RECT, localVisibleRect);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
MainActivity.this,
Pair.create(imageView, transitionName));
startActivity(intent, options.toBundle());
});
Restore the clip in onResume() of your MainActivity.
#Override
protected void onResume() {
super.onResume();
// This is also important. When we come back to this activity, we need to reset the clip.
if (mClippedView != null) {
mClippedView.setClipBounds(null);
}
}
Create transition resource in your res folder like this:
app/src/main/res/transition/shared_element_transition.xml
The contents should be similar to this:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="375"
android:interpolator="#android:interpolator/fast_out_slow_in"
android:transitionOrdering="together">
<!-- This is needed to clip the invisible part of the view being transitioned. Otherwise we
will see weird transitions when the image is partially hidden behind appbar or any other
view. -->
<changeClipBounds/>
<changeTransform/>
<changeBounds/>
</transitionSet>
Set the transition in your SecondActivity.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
// Setup transition
Transition transition =
TransitionInflater.from(this)
.inflateTransition(R.transition.shared_element_transition);
getWindow().setSharedElementEnterTransition(transition);
// Postpone the transition. We will start it when the slideshow is ready.
ActivityCompat.postponeEnterTransition(this);
// more code ...
// See next step below.
}
Now we need to clip the shared view in the SecondActivity as well.
// Setup the clips
String transitionName = getIntent().getStringExtra(EXTRA_TRANSITION_NAME);
Rect clipRect = getIntent().getParcelableExtra(EXTRA_CLIP_RECT);
setEnterSharedElementCallback(new SharedElementCallback() {
#Override
public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
for (int i = 0; i < sharedElementNames.size(); i++) {
if (Objects.equals(transitionName, sharedElementNames.get(i))) {
View view = sharedElements.get(i);
view.setClipBounds(clipRect);
}
}
super.onSharedElementStart(sharedElementNames, sharedElements, sharedElementSnapshots);
}
#Override
public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
for (int i = 0; i < sharedElementNames.size(); i++) {
if (Objects.equals(transitionName, sharedElementNames.get(i))) {
View view = sharedElements.get(i);
view.setClipBounds(null);
}
}
super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots);
}
});
Is it possible to override Android's LayoutAnimationController in such a way that only certain child Views that I specify inside a ViewGroup will animate? My goal is to choose an arbitrary set of the child views based on the current state of the Activity and animate them with the same animation at exactly the same time, then at a later time choose a different arbitrary set of child views and do the same thing. I would like to be able to do this continually until the Activity is finished running.
So far I have looked at and dismissed a couple of options:
Calling startAnimation(Animation) the specific child views
individually, however there is not a guarantee that they will all
start and end at exactly the same time, especially if the number of
views in the arbitrary set is large.
Overriding LayoutAnimationController.getAnimationForView() seemed
like it would be the easiest way, but the method is final and cannot
be overridden.
I have been scratching my head for some time on this and figured I would give Stack Overflow a shot.
I wasn't able to find a way to change Android's LayoutAnimationController, but did come up with a simple solution that does what I want. I created a View subclass that can selectively ignore calls to run an animation if I so choose.
public class AnimationAverseView extends View {
private boolean mIsAnimatable = true;
public void setAnimatible(boolean isAnimatable) {
if (!isAnimatable) {
clearAnimation();
}
mIsAnimatable = isAnimatable;
}
#Override
public void startAnimation(Animation animation) {
if (mIsAnimatable) {
super.startAnimation(animation);
}
}
}
I did not worry about any other possible animation-related methods, since I only animate the Views via a LayoutAnimationController.
I see this question was asked 7 years and 6 months ago, but today I have encountered it myself and creating custom child views seems a bit cumbersome to me.
I found that using a custom outer layout seems to do the trick, where you have to override the attachLayoutAnimationParameters. This method is called for every child view that is part of a layout animation, and it is called when the layout animation is actually triggered within the ViewGroup. It is attaching AnimationLayoutParams to the child, in which case it is animated. So simply said, if you do not attach any LayoutAnimation Parameters to a child, it will not be part of the layout animation.
class CustomLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val excludeForLayoutAnimation by lazy {
listOfNotNull(findViewById(R.id.text), findViewById(R.id.fab))
}
override fun attachLayoutAnimationParameters(
child: View?,
params: ViewGroup.LayoutParams?,
index: Int,
count: Int
) {
if (child !in excludeForLayoutAnimation) {
super.attachLayoutAnimationParameters(child, params, index, count)
}
}
}
If you now simply call this on your outer custom view, it will exclude the children that you specify:
customView?.apply {
layoutAnimation = AnimationUtils.loadLayoutAnimation(
context,
R.anim.layout_animation_fade_in
)
scheduleLayoutAnimation() // or maybe startLayoutAnimation()
}
I cannot add a comment to the accepted answer, so adding some additional information here. The comment from happydude (Feb 17) was correct for me as well. I had a FrameLayout as the outermost View for my ListView header, and I needed to override the setAnimation() method to prevent the header from being animated with the rest of the list. So you should check out which other animation methods you might need to override.