Memory leak using Lollipop's transitions - android

I'm trying to implement transition between fragment (that attached to the MainActivity) with RecyclerView and the DetailActivity.
In my fragment, I've added RecyclerView listener in onStart() method:
#Override
public void onStart() {
super.onStart();
mRecyclerItemClickListener = new RecyclerItemClickListener(getActivity(), (view, position) -> {
Intent intent = new Intent(getActivity(), DetailActivity.class);
intent.putExtra(ConstantsManager.POSITION_ID_KEY, mFilmsAdapter.getImdbIdByPosition(position));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ActivityOptionsCompat optionsCompat = ActivityOptionsCompat
.makeSceneTransitionAnimation(getActivity());
ActivityCompat.startActivity(getActivity(), intent, optionsCompat.toBundle());
} else {
startActivity(intent);
}
});
mRecyclerView.addOnItemTouchListener(mRecyclerItemClickListener);
mSwipeRefreshLayout.setOnRefreshListener(this);
mTopFilmsPresenter.attachView(this);
mTopFilmsPresenter.getFilms();
}
You can see, that the transition begins in the if block. In the DetailActivity I have the following method which I call in onCreate():
#TargetApi(Build.VERSION_CODES.KITKAT)
private void setupWindowAnimations() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Explode explode = new Explode();
explode.setDuration(ConstantsManager.TRANSITION_DURATION);
getWindow().setEnterTransition(explode);
getWindow().setExitTransition(explode);
}
}
And in the MainActivity I have almost a similar method, which I also call in onCreate():
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setWindowAnimations() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Explode explode = new Explode();
explode.setDuration(ConstantsManager.TRANSITION_DURATION);
getWindow().setEnterTransition(explode);
getWindow().setExitTransition(explode);
getWindow().setReenterTransition(explode);
getWindow().setReturnTransition(explode);
}
}
I've also implemented Transition.TransitionListener interface in the DetailActivity, because documentation says:
A transition listener receives notifications from a transition. Notifications indicate transition lifecycle events.
So I'm trying to remove listener in the onTransitionEnd(Transition transition), onTransitionCancel(Transition transition) and onTransitionPause(Transition transition) callbacks:
#Override
public void onTransitionStart(Transition transition) {
}
#TargetApi(Build.VERSION_CODES.KITKAT)
#Override
public void onTransitionEnd(Transition transition) {
transition.removeListener(this);
}
#TargetApi(Build.VERSION_CODES.KITKAT)
#Override
public void onTransitionCancel(Transition transition) {
transition.removeListener(this);
}
#TargetApi(Build.VERSION_CODES.KITKAT)
#Override
public void onTransitionPause(Transition transition) {
transition.removeListener(this);
}
#Override
public void onTransitionResume(Transition transition) {
}
I'm using LeakCanary for memory leaks detections and it detects a memory leak after transition:
So I am wondering how can I remove transition listener(s) to prevent this memory leak?

Ensure that you release the Activity reference since you are strongly holding onto it within the Transition class. A simple Transition.removeListener(this) (since your activity implements the interface) in it's onDestroy() method should prevent memory leaks.

Thanks Albert Vila for the answer. The problem lies in TransitionManager.sRunningTransitions according to this topic.

Related

Callback when task goes into background or comes into foreground?

I have an activity A, it launches custom-tab. I need to know while the custom tab is open, if the task (of which the activity is part of) goes to background or comes to foreground.
I am aware of this question How to detect when an Android app goes to the background and come back to the foreground . The solutions mentioned for this question don't work for me because as soon as custom tab is launched, the onbackground callback is received, which is not what I want. I want onbackground callback, when the task containing the activity A goes to background.
Using the CustomTabsCallback you can listen when the Tab becomes hidden (goes into background) using the TAB_HIDDEN callback or TAB_SHOWN callback when the Tab becomes visible (goes into foreground).
From the Documentation:
TAB_HIDDEN
Sent when the tab becomes hidden.
TAB_SHOWN
Sent when the tab becomes visible.
Below is a full working example of how you can use the above callbacks:
public class CustomTabsActivity extends AppCompatActivity {
private CustomTabsServiceConnection mCustomTabsServiceConnection;
private CustomTabsClient mCustomTabsClient;
private CustomTabsSession mCustomTabsSession;
private CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.customTabsButton).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
showCustomTabs();
}
});
initCustomTabs();
}
#Override
protected void onStart() {
super.onStart();
CustomTabsClient.bindCustomTabsService(this, "com.android.chrome", mCustomTabsServiceConnection);
}
#Override
protected void onResume() {
super.onResume();
}
#Override
protected void onStop() {
super.onStop();
}
#Override
protected void onDestroy() {
super.onDestroy();
}
private void initCustomTabs() {
mCustomTabsServiceConnection = new CustomTabsServiceConnection()
{
#Override
public void onCustomTabsServiceConnected(#NotNull ComponentName componentName, #NotNull CustomTabsClient customTabsClient)
{
mCustomTabsClient = customTabsClient;
mCustomTabsClient.warmup(0L);
mCustomTabsSession = mCustomTabsClient.newSession(new CustomTabsCallback()
{
#Override
public void onNavigationEvent(int navigationEvent, Bundle extras) {
switch (navigationEvent)
{
case CustomTabsCallback.TAB_SHOWN:
//Sent when the tab becomes visible (goes into foreground)
break;
case CustomTabsCallback.TAB_HIDDEN:
//Sent when the tab becomes hidden (goes into background)
break;
}
}
});
builder.setSession(mCustomTabsSession);
}
#Override
public void onServiceDisconnected(ComponentName name) {
mCustomTabsClient = null;
}
};
CustomTabsClient.bindCustomTabsService(this, "com.android.chrome", mCustomTabsServiceConnection);
}
private void showCustomTabs(){
builder.setShowTitle(true);
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl(this, Uri.parse("https://stackoverflow.com/"));
}
}
The relationship between your activity and chrome custom tabs depends on the launchMode. You can launch the custom tab in current stack or a new stack.

Understanding Window#getSharedElementTransition()

I have two Activities A and B which have a SharedElement. If Activity A starts Activity B and listens to the the transition, both the listener for exit and reenter are called.
Here the code for the calling Activity A:
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getWindow().getSharedElementReenterTransition().addListener(new Transition.TransitionListener() {
#Override
public void onTransitionStart(Transition transition) {
Log.i("Log", "A REENTER");
}
...
});
getWindow().getSharedElementExitTransition().addListener(new Transition.TransitionListener() {
#Override
public void onTransitionStart(Transition transition) {
Log.i("Log", "A EXIT");
}
...
});
getWindow().getSharedElementEnterTransition().addListener(new Transition.TransitionListener() {
#Override
public void onTransitionStart(Transition transition) {
Log.i("TestApp", "A ENTER");
}
...
});
getWindow().getSharedElementReturnTransition().addListener(new Transition.TransitionListener() {
#Override
public void onTransitionStart(Transition transition) {
Log.i("TestApp", "A RETURN");
}
...
});
}
public void onClick(View v){
Intent intent = new Intent(this, Act2.class);
Pair<View, String> pair1 = Pair.create(findViewById(R.id.textView), findViewById(R.id.textView).getTransitionName());
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, pair1);
startActivity(intent, options.toBundle());
}
}
If I now execute onClick() (to start Activity B) and then hit the back button to return to Activity A, the Log will be as follows:
A REENTER
A EXIT
B ENTER
B RETURN
B ENTER
B RETURN
A REENTER
A EXIT
I would expect it to be
A EXIT
B ENTER
B RETURN
A REENTER
By default, the same transition is used for both the exit and reenter transitions as well as the enter and return transitions. If you explicitly set them, they will be different.
I believe that you are adding listeners to the same transition, so they are all being called.
I ran into the similar problem and found a similar question
There is a bug in Lollipop that causes the shared element return transition to be interrupted if it takes longer than the reenter
transition duration. If you adjust your reenter transition duration
(on the calling Activity), that should fix the interruption problem.
You better use the enter and return shared element transitions.

How to know when enter transition ended on Fragment?

I have a fragment, which shows enter animation, I set transition by
this.setEnterTransition(transition);
After that I want to show another animation. But I need to know when transition animation ends to start the second one.
For activity there is a callback like onEnterAnimationComplete() but it is not called when Fragment's transition ends.
Is there any way to know when enter transition ends for the Fragment?
transition.addListener(new Transition.TransitionListener() {
#Override
public void onTransitionStart(Transition transition) {}
#Override
public void onTransitionEnd(Transition transition) {}
#Override
public void onTransitionCancel(Transition transition) {}
#Override
public void onTransitionPause(Transition transition) {}
#Override
public void onTransitionResume(Transition transition) {}
});
this.setEnterTransition(transition);
If you have the following setup:
FragmentA calls FragmentB with a SharedElementEnterTransition like
private final TransitionSet transition = new TransitionSet()
.addTransition(new ChangeBounds());
//...
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction()
.replace(R.id.container, fragment, fragment.getClass().getSimpleName());
transaction.addSharedElement(view, view.getTransitionName());
fragment.setSharedElementEnterTransition(transition);
fragment.setSharedElementReturnTransition(transition);
transaction.commit();
to listen for the end of the SharedElementTransition in your second Fragment. Then you have to get the SharedElementEnterTransition in your FragmentB's onAttach like:
#Override
public void onAttach(Context context) {
super.onAttach(context);
TransitionSet transitionSet = (TransitionSet) getSharedElementEnterTransition();
if (transitionSet != null) {
transitionSet.addListener(new Transition.TransitionListener() {
#Override
public void onTransitionEnd(#NonNull Transition transition) {
// remove listener as otherwise there are side-effects
transition.removeListener(this);
// do something here
}
#Override
public void onTransitionStart(#NonNull Transition transition) {}
#Override
public void onTransitionCancel(#NonNull Transition transition) {}
#Override
public void onTransitionPause(#NonNull Transition transition) {}
#Override
public void onTransitionResume(#NonNull Transition transition) {}
});
}
}
As pointed out in the comments of this answer there is a bug when not setting the listener in onAttach().

Listening to default activity transition

I'm trying to add TransitionListener to default activity transition like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().getEnterTransition().addListener(new TransitionAdapter());
}
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private class TransitionAdapter implements Transition.TransitionListener {
#Override
public void onTransitionStart(Transition transition) {
Log.i("transition", "onTransitionStart");
}
#Override
public void onTransitionEnd(Transition transition) {
Log.i("transition", "onTransitionEnd");
}
#Override
public void onTransitionCancel(Transition transition) {
Log.i("transition", "onTransitionCancel");
}
#Override
public void onTransitionPause(Transition transition) {
Log.i("transition", "onTransitionPause");
}
#Override
public void onTransitionResume(Transition transition) {
Log.i("transition", "onTransitionResume");
}
}
This how I start activity:
Intent intent = new Intent(activity, LoginActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Bundle options = ActivityOptions.makeSceneTransitionAnimation(activity).toBundle();
activity.startActivityForResult(intent, RequestCodes.SIGN_IN, options);
} else {
activity.startActivityForResult(intent, RequestCodes.SIGN_IN);
}
The problem is no callback is ever called on real device. It works on genymotion though. Is there some additional setup required for that?
DISCLAIMER: I test it on lolipop running device
Found out it doesn't work on 5.0 but got fixed on 5.1 so it's clearly an android bug. I don't know a workaround for this though. I've restricted transitions to minimum API 22.

Android app using cpu when app closed with back or home - animations in ViewPager

I have an app that has animations in fragments in a ViewPager. I have the ViewPager displaying on the empty option of a list fragment. The animations are NineOldAndroids ObjectAnimators combined in AnimatorSets some of which animate SVGs shown with svg-android.
When I change page on the ViewPager the animations stop using this code in the fragment:
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
if (myAnimationView != null) {
myAnimationView.restartAnimation();
}
} else {
if (myAnimationView != null) {
myAnimationView.stopAnimation();
}
}
}
And this code in the View implements ValueAnimator.AnimatorUpdateListener:
public void stopAnimation() {
endAnimCalled = true;
myAnimatorSet.end();
}
public void restartAnimation() {
endAnimCalled = false;
if (!myAnimatorSet.isStarted()) {
myAnimatorSet.start();
}
}
If pressing back to exit when not on the animation page, the app uses 1-5% CPU even after several hours. If back is pressed on the page when animating, the app uses 10-30% CPU when running in the background.
Is there a good way to pass through the fragments that onPause has been called? Any ideas why the app still uses 1-5% CPU when the animations have stopped?
I have found this impossible to replicate in an app small enough to be reasonable to post on Stack Exchange.
Making sure that the animation ends is the only solution I have found for the CPU usage. I have set it to only repeat the animation 10 times. I also considered using a BroadcastReceiver to pass a broadcast from the onPause() of the activity to call endAnimation() in the fragments.
public void startAnimation() {
createAnimation();
animRepeats = 0;
if (!myAnimatorSet.isStarted()) {
myAnimatorSet.start();
}
AnimatorListener myAnimListen = new AnimatorListener() {
#Override
public void onAnimationEnd(Animator animator) {
if (!endAnimCalled && animRepeats < 10) {
myAnimatorSet.start();
animRepeats++;
}
}
#Override public void onAnimationRepeat(Animator animator) {}
#Override public void onAnimationStart(Animator animator) {}
#Override public void onAnimationCancel(Animator animation) {}
};
myAnimatorSet.addListener(myAnimListen);
}
public void stopAnimation() {
endAnimCalled = true;
myAnimatorSet.end();
}
public void restartAnimation() {
endAnimCalled = false;
if (!myAnimatorSet.isStarted()) {
animRepeats = 0;
myAnimatorSet.start();
}
}

Categories

Resources