Shared element activity transition on android 5 - android

I wanted to setup a shared element transition when going from one Activity to another.
The first Activity has a RecyclerView with items. When an item is clicked that item should animate to the new activity.
So i've set a
android:transitionName="item" on both the final activity views, as wel as the recycler-view item views.
I'm also using this code when going to the next activity:
this.startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, itemView, "boomrang_item").toBundle());
When clicking an item, it transitions properly and the new view is shown. It is really nice.
However when i click the back button. Sometimes it works fine, but most of the time my activity crashes with the following stacktrace:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.ViewGroup.transformMatrixToGlobal(android.graphics.Matrix)' on a null object reference
at android.view.GhostView.calculateMatrix(GhostView.java:95)
at android.app.ActivityTransitionCoordinator$GhostViewListeners.onPreDraw(ActivityTransitionCoordinator.java:845)
at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:847)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1956)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1054)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5779)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:767)
at android.view.Choreographer.doCallbacks(Choreographer.java:580)
at android.view.Choreographer.doFrame(Choreographer.java:550)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:753)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5221)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
What am i doing wrong?
It looks like a bug in android 5

I encounter the same issue, and notice the crash happens if the original shared element is no longer visible on the previous screen when you go back (probably it is the last element on screen in portrait, but once switched to landscape it's no longer visible), and thus the transition has nowhere to put back the shared element.
My workaround is to remove the return transition (in the 2nd activity) if the screen has been rotated before going back, but I'm sure there must be a better way to handle this:
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mOrientationChanged = !mOrientationChanged;
}
#Override
public void supportFinishAfterTransition() {
if (mOrientationChanged) {
/**
* if orientation changed, finishing activity with shared element
* transition may cause NPE if the original element is not visible in the returned
* activity due to new orientation, we just finish without transition here
*/
finish();
} else {
super.supportFinishAfterTransition();
}
}

If you're using Proguard then try adding this into your rules file. I had the same issue and it appears to work?
-keep public class android.app.ActivityTransitionCoordinator

Try removing any merge xml tags that you might have on the final activity's view. I have noticed that transitioning to a view, that contains a merge tag, in which the transitioning element is a direct child of the merge tag, will cause this error, but should I replace the merge tag with a different container like CardView, the animation works just fine. Also make sure that there is a 1:1 relationship between the transitionNames in the views.
UPDATE:
I experienced this issue once more when doing an activity transition, clicking the back button to return to the initial activity, and then trying the transition again. I was accessing the direct parent of the 'transition component', (A RelativeLayout) by id, with a findViewById() call, and then calling removeAllViews(). I ended up changing the code to call 'removeAllViews()' on a greater ancestor than the parent, also removed a tag from the element that was to take the place of the 'transition component' after page load. This alleviated my issue.

Make sure the View you are Transitioning to in the Second Activity is not the root layout.
You can just wrap it in a FrameLayout with a transparent windowBackground.

I had this same issue, for me it was being caused by the recyclerview executing updates after/during the first exit transition. I think the shared element view was then sometimes getting recycled, meaning it would no longer be available for the transition animation, hence the crash (normally on the return transition but sometimes on the exit transition). I solved it by blocking updates if the activity is paused (used an isRunning flag) - note it was pausing but not stopping as it was still visible in the background. Additionally I blocked the update process if the transition was running. I found it enough to listen to this callback:
Transition sharedElementExitTransition = getWindow().getSharedElementExitTransition();
if (sharedElementExitTransition != null) {
sharedElementExitTransition.addListener(.....);
}
As a final measure, although i'm not sure if this made a difference, I also did recyclerView.setLayoutFrozen(true) / recyclerView.setLayoutFrozen(false) in the onTransitionStart / onTransitionEnd.

Be sure the "itemView" you are passing in the transition is the view clicked (received on your onClick() callback)

I have faced the same issue, actually I used firebase and I have list of information and when user tap it will call detailActivity with sharedAnimation in this activity I was updating it as seen using firebase so firebase event updating the list item as seen, in this case this problem is invoking because recycler view that screen layout was getting effected.
and it invoke an exception because that transition id which one we have passed it was no more, so I solve this issue using this method.
onPause() I have frozen the layout and onResume() set it as false;
#Override
public void onPause() {
super.onPause();
mRecycler.setLayoutFrozen(true);
}
#Override
public void onResume() {
super.onResume();
mRecycler.setLayoutFrozen(false);
}
And it's working.

What I came up with is to avoid transitioning back to Activity with RecyclerView, or changing back transition with something else.
Disable all return transitions:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public void finishAfterTransition() {
finish();
}
Or, if you want to disable only shared elements return transition, and be able to set your own return transition:
// Track if finishAfterTransition() was called
private boolean mFinishingAfterTransition;
#Override
protected void onCreate(#Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFinishingAfterTransition = false;
}
public boolean isFinishingAfterTransition() {
return mFinishingAfterTransition;
}
#Override
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void finishAfterTransition() {
mFinishingAfterTransition = true;
super.finishAfterTransition();
}
public void clearSharedElementsOnReturn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
TransitionUtilsLollipop.clearSharedElementsOnReturn(this);
}
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static final class TransitionUtilsLollipop {
private TransitionUtilsLollipop() {
throw new UnsupportedOperationException();
}
static void clearSharedElementsOnReturn(#NonNull final BaseActivity activity) {
activity.setEnterSharedElementCallback(new SharedElementCallback() {
#Override
public void onMapSharedElements(final List<String> names,
final Map<String, View> sharedElements) {
super.onMapSharedElements(names, sharedElements);
if (activity.isFinishingAfterTransition()) {
names.clear();
sharedElements.clear();
}
}
});
}
With that implemented in base activity, you can easily use it in onCreate()
#Override
protected void onCreate(#Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
clearSharedElementsOnReturn(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// set your own transition
getWindow().setReturnTransition(new VerticalGateTransition());
}
}

I had this same error, mine was caused by the same reasoning behind hidro's answer but was caused by the keyboard hiding the shared element that the transition was going back to.
My workaround was to programmatically close the keyboard right before finishing the activity so the shared element on the previous activity isn't obscured.
View view = this.getCurrentFocus();
if (view != null) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
supportFinishAfterTransition();

As #Fabio Rocha said, make sure that the itemView is retrieved from the ViewHolder.
You can get the ViewHolder by position via
mRecyclerView.findViewHolderForAdapterPosition(position);

The reason for this is actually quite simple:
When you Navigate back to the parent Activity or Fragment, the View is not there yet (could be for many reasons).
So, what you want to do is to postpone the Enter Transition until the View is available.
My work around is to call the following function in onCreate() in my Fragment (but works in Activity too):
private void checkBeforeTransition() {
// Postpone the transition until the window's decor view has
// finished its layout.
getActivity().supportPostponeEnterTransition();
final View decor = getActivity().getWindow().getDecorView();
decor.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
decor.getViewTreeObserver().removeOnPreDrawListener(this);
getActivity().supportStartPostponedEnterTransition();
return true;
}
});
}

Got same issue, and it caused by recycler view updating in background, the recycler view will recreate view when notifyItemChanged(int index), so the share view was recycled and it got crash when come back.
My solution is call recyclerView.setItemAnimator(null);, and it will prevent recycler view from recreating view.

Related

How to preserve AutoCompleteTextView's DropDown state when gets back from launched Activity

Currently, when I
Launch a new Activity by clicking on AutoCompleteTextView's drop down
Close the launched Activity
AutoCompleteTextView's drop down is hidden.
I would like to preserve AutoCompleteTextView's drop-down state which includes
Drop down should not be hidden when gets back from launched Activity
Drop down's scroll position should be preserved.
I'm not exactly sure the reason why AutoCompleteTextView's dropdown will be hidden when I back from launched Activity. Hence, I had tried 2 things
Change windowSoftInputMode of launched Activity from stateAlwaysHidden to stateUnchanged.
In onActivityResult, when the launched Activity is closed, perform mSearchSrcTextView.showDropDown(); explicitly.
However, I am still facing the issue. The previous scroll position of AutoCompleteTextView's dropdown is not preserved. It is reset back to top of the list.
Here's the screen-shot to better illustrate the problem I am facing.
(Current AutoCompleteTextView's dropdown is scrolled to the end. I click on the last item and launch a new Activity)
(New Activity is launched. Now, I click on the BACK soft key twice, to close the keyboard and then close the Activity)
(Due to the explicit call of mSearchSrcTextView.showDropDown(); in onActivityResult, the drop down is shown again. However, its previous scrolled position is not being preserved. Start of list is being shown instead of end of list)
I was wondering, is there any way to preserved the AutoCompleteTextView's DropDown state, when closing a previous launched Activity?
For AutoCompleteTextView, it has a method called dismissDropDown(). I believe when back from newly launched activity, this function is being triggered. So we workaround this problem by extending AutoCompleteTextView & override it's dismissDropDown().
We add a boolean flag temporaryIgnoreDismissDropDown, to indicate whether to temporarily ignore dismissDropDown.
public class MyAutoCompleteTextView extends AutoCompleteTextView {
private boolean temporaryIgnoreDismissDropDown = false;
.....
#Override
public void dismissDropDown() {
if (this.temporaryIgnoreDismissDropDown) {
this.temporaryIgnoreDismissDropDown = false;
return;
}
super.dismissDropDown();
}
public void setTemporaryIgnoreDismissDropDown(boolean flag) {
this.temporaryIgnoreDismissDropDown = flag;
}
}
Before launching new Activity, we set dismissDropDown to true. After coming back from launched activity, dismissDropDown is called. The override method checks if temporaryIgnoreDismissDropDown is true, just set it to false & do nothing. So the real dismissDropDown is skipped.
// myAutoCompleteTextView is instance of MyAutoCompleteTextView
myAutoCompleteTextView.setTemporaryIgnoreDismissDropDown(true);
// launch new Activity
startActivity(....);
Hope this help, good luck!
After an hour of coding, much trying and a lot of googling around, I've put together a solution that does just what you want. It uses reflection to access the ListView within the Dropdown menu and to access the dropdown state when you leave the activity.
The code for this is kinda long, so I'll walk you through all the parts. Firstly, I have some variables we will need:
boolean wasDropdownOpen;
int oldDropdownY;
Handler handler;
The handler will be neccessary for later, as we have to do a little trick in the onResume() method. Initialize it as usual in your onCreate() method:
handler = new Handler(getMainLooper());
Now, let's get to the tricky part.
You need to call the following method before you start any activity. It can't be done in onPause() since the Dropdown menu is already closed when this method is called. In my test code I've overridden the startActivity() and startActivityForResult() method, and called it there, but you can do this however you like.
private void processBeforeStart() {
ListPopupWindow window = getWindow(textView);
if(window == null) return;
wasDropdownOpen = window.isShowing();
ListView lv = getListView(window);
if(lv == null) return;
View view = lv.getChildAt(0);
oldDropdownY = -view.getTop() + lv.getFirstVisiblePosition() * view.getHeight();
}
This will save your dropdown ListView's state for later. Now, we will load it. This is the onResume() method we will need for this:
#Override
protected void onResume() {
super.onResume();
if (wasDropdownOpen)
textView.showDropDown();
handler.postDelayed(new Runnable() {
#Override
public void run() {
ListView lv = getListView(getWindow(textView));
if (lv != null)
scrollToY(lv, oldDropdownY);
}
}, 150);
}
First of all, let me explain this method. We saved the state if the dropdown was open, so we reopen the menu if it was. Simple. The next part is the scrolling. We need to do this in a Handler because the UI is not yet fully loaded when onResume() is called and therefore the ListView is still inaccessible.
The scrollToY() method you see there is a modified version of the code from this post, as Android's ListView does not have an inbuilt method to set the scroll position as precisely as we want it here.
The implementation of this method is as follows:
private void scrollToY(ListView lv, int position) {
int itemHeight = lv.getChildAt(0).getHeight();
int item = (int) Math.floor(position / itemHeight);
int scroll = (item * itemHeight) - position;
lv.setSelectionFromTop(item, scroll);// Important
}
Now, you've probably seen the getWindow() and getListView() methods I've used above. These are the reflection methods, which we have to use because Android does not expose a public API to access the ListView within the ListPopupWindow of the AutoCompleteTextView. Additionally, the DropDownListView, a subclass of ListView that is actually used within this object, is not visible to the oudside as well, so we have to use Reflection once again.
Here is the implementation of my two helper methods:
private ListView getListView(ListPopupWindow window) {
for (Field field : window.getClass().getDeclaredFields()) {
if (field.getType().getName().equals("android.widget.DropDownListView")) {
field.setAccessible(true);
try {
return (ListView) field.get(window);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
private ListPopupWindow getWindow(AutoCompleteTextView tv) {
Class realClass = tv.getClass().getName().contains("support") ? tv.getClass().getSuperclass() : tv.getClass();
for (Field field : realClass.getDeclaredFields()) {
if (field.getType().getName().equals(ListPopupWindow.class.getName())) {
field.setAccessible(true);
try {
return (ListPopupWindow) field.get(tv);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
I've tested this on Android O (API level 26) and it works just as you described you want it to work.
I hope that the effort I put into this answer gets me a chance on the Bounty ;-)
It sounds like you've already figured out how to show the drop-down on demand (via showDropDown()), so I'll only address how to restore the scroll position of the dropdown.
You can access the first visible position of the dropdown like this:
autocomplete.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int firstVisiblePosition = parent.getFirstVisiblePosition();
// save this value somehow
}
});
Save the value of this int however you'd like (in memory, via onSaveInstanceState(), pass it through to the started activity so that it can pass it back via onActivityResult(), etc). Then, wherever you re-show the dropdown, do this:
autocomplete.showDropDown();
autocomplete.setListSelection(firstVisiblePosition);
The shortcoming of this technique is that it makes the item at firstVisiblePosition completely visible, so if it was halfway scrolled out of view, the list position won't be restored perfectly. Unfortunately, I don't believe there's any way to save/restore this partial-view offset.

create Button instance throws NullPointerException in AnimatorSet.clone

I have an issue creating a Button programmatically. The button is supposed to be inserted in a pre-existing layout.
And since I need the dimensions of a specific container I created a global layout listener for that container and in the onGlobalLayout callback i check for a valid size and then instantiate a new Button.
The context used is the context from the container.
final View container = activity.findViewById(...);
container.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
if (container.getWidth()>0 && container.getHeight()>0) {
Button button = new Button(container.getContext());
}
}
});
However, in rare cases - specifically when switching from one activity to another - the Button instanciation fails with a NullPointerException in the Android framework code.
java.lang.NullPointerException: Attempt to read from field 'android.animation.Animator android.animation.AnimatorSet$Node.mAnimation' on a null object reference
at android.animation.AnimatorSet.clone(AnimatorSet.java:725)
at android.animation.AnimatorSet.clone(AnimatorSet.java:682)
at android.animation.StateListAnimator.clone(StateListAnimator.java:148)
at android.animation.StateListAnimator$StateListAnimatorConstantState.newInstance(StateListAnimator.java:328)
at android.animation.StateListAnimator$StateListAnimatorConstantState.newInstance(StateListAnimator.java:327)
at android.content.res.ConstantState.newInstance(ConstantState.java:53)
at android.content.res.ConstantState.newInstance(ConstantState.java:61)
at android.content.res.ConfigurationBoundResourceCache.getInstance(ConfigurationBoundResourceCache.java:40)
at android.animation.AnimatorInflater.loadStateListAnimator(AnimatorInflater.java:163)
at android.view.View.<init>(View.java:4815)
at android.widget.TextView.<init>(TextView.java:995)
at android.widget.Button.<init>(Button.java:113)
at android.widget.Button.<init>(Button.java:106)
at android.widget.Button.<init>(Button.java:102)
at android.widget.Button.<init>(Button.java:98)
My assumption is that somehow the Context ist not valid any more but I can't put my finger on it..
I do remove the listener when the activity gets deactivated.
Any ideas?
As docs says OnGlobalLayoutListener
Interface definition for a callback to be invoked when the global layout state or the visibility of views within the view tree changes.
So when UI is destroing, you are getting "dying" View that causes NPE. You can try to unregister listener in onStop() to prevent that. Or if you need just to handle fully created View use
container.post(new Runnable() {
#Override
public void run() {
Button button = new Button(container.getContext());
}
});
The only solution I've found so far is to not create the button by calling
Button button = new Button(container.getContext());
but by creating a small layout xml file only containing the button and then instantiate the button like so:
LayoutInflater.from(context).inflate(R.layout.button, null)

How to save scroll position of RecyclerView in Android?

I have Recycler view which lays inside of SwipeRefreshLayout. Also, have ability to open each item in another activity.
After returning back to Recycler I need scroll to chosen item, or to previous Y.
How to do that?
Yes, I googled, found articles in StackOverFlow about saving instance of layout manager, like this one: RecyclerView store / restore state between activities.
But, it doesn't help me.
UPDATE
Right now I have this kind of resolving problem, but, of course, it also doesn't work.
private int scrollPosition;
...//onViewCreated - it is fragment
recyclerView.setHasFixedSize(true);
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(llm);
data = new ArrayList<>();
adapter.setData(getActivity(), data);
recyclerView.setAdapter(adapter);
...
#Override
public void onResume() {
super.onResume();
recyclerView.setScrollY(scrollPosition);
}
#Override
public void onPause() {
super.onPause();
scrollPosition = recyclerView.getScrollY();
}
Yes, I have tried scrollTo(int, int) - doen't work.
Now I tried just scroll, for example, to Y = 100, but it doesn't scrolling at all.
Save the current state of recycle view position #onPause:
positionIndex= llManager.findFirstVisibleItemPosition();
View startView = rv.getChildAt(0);
topView = (startView == null) ? 0 : (startView.getTop() - rv.getPaddingTop());
Restore the scroll position #onResume:
if (positionIndex!= -1) {
llManager.scrollToPositionWithOffset(positionIndex, topView);
}
or another way can be #onPause:
long currentVisiblePosition = 0;
currentVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
restore #onResume:
((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(currentVisiblePosition);
currentVisiblePosition = 0;
A lot of these answers seem to be over complicating it.
The LayoutManager supports onRestoreInstanceState out of the box so there is no need to save scroll positions etc. The built in method already saves pixel perfect positions.
example fragment code (null checking etc removed for clarity):
private Parcelable listState;
private RecyclerView list;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
listState=savedInstanceState.getParcelable("ListState");
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable("ListState", list.getLayoutManager().onSaveInstanceState());
}
then just call
list.getLayoutManager().onRestoreInstanceState(listState);
once your data has been reattached to your RecyclerView
Beginning from version 1.2.0-alpha02 of androidx recyclerView library, it is now automatically managed. Just add it with:
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
And use:
adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
The StateRestorationPolicy enum has 3 options:
ALLOW — the default state, that restores the RecyclerView state immediately, in the next layout pass
PREVENT_WHEN_EMPTY — restores the RecyclerView state only when the adapter is not empty (adapter.getItemCount() > 0). If your data is loaded async, the RecyclerView waits until data is loaded and only then the state is restored. If you have default items, like headers or load progress indicators as part of your Adapter, then you should use the PREVENT option, unless the default items are added using MergeAdapter. MergeAdapter waits for all of its adapters to be ready and only then it restores the state.
PREVENT — all state restoration is deferred until you set ALLOW or PREVENT_WHEN_EMPTY.
Note that at the time of this answer, recyclerView library is still in alpha03, but alpha phase is not suitable for production purposes.
User your recycler view linearlayoutmanager for getting scroll position
int position = 0;
if (linearLayoutManager != null) {
scrollPosition = inearLayoutManager.findFirstVisibleItemPosition();
}
and when restoring use following code
if (linearLayoutManager != null) {
cardRecyclerView.scrollToPosition(mScrollPosition);
}
Hope this helps you
to save position to Preferences, add this to your onStop()
int currentVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
getPreferences(MODE_PRIVATE).edit().putInt("listPosition", currentVisiblePosition).apply();
then restore position like this
if (getItemCount() == 0) {
int savedListPosition = getPreferences(MODE_PRIVATE).getInt("listPosition", 0);
recyclerView.getLayoutManager().scrollToPosition(savedListPosition); }
this last code should be added inside an event of the Adapter (not sure witch event but in my case was onEvent() - com.google.firebase.firestore.EventListener)
For some reason there are a lot of quite misleading tips/suggestions on how to save and restore scroll position in your_scrolling_container upon orientation changes.
Taking current scroll position and saving it in Activity’s onSaveInstanceState
Extending a certain scrollable View to do same there
Preventing Activity from being destroyed on rotation
And yeah, they are working fine, but…
But in fact, everything is much simpler, because Android is already doing it for you!
If you take a closer look at
RecyclerView/ListView/ScrollView/NestedScrollView sources, you’ll see that each of them is saving its scroll position in onSaveInstanceState. And during the first layout pass they are trying to scroll to this position in onLayout method.
There are only 2 things you need to do, to make sure it’s gonna work fine:
Set an id for your scrollable view, which is probably already done. Otherwise Android won’t be able to save View state automatically.
Provide a data before the first layout pass, to have the same scroll boundaries you had before rotation. That’s the step where developers usually have some issues.
The easiest and transition compatible way I found is:
#Override
public void onPause() {
super.onPause();
recyclerView.setLayoutFrozen(true);
}
#Override
public void onResume() {
super.onResume();
recyclerView.setLayoutFrozen(false);
}
in onSaveInstanceState() method of fragment you can save the scroll position of RecycleView
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
LinearLayoutManager layoutManager = (LinearLayoutManager)
recyclerView.getLayoutManager();
outState.putInt("scrolled_position",
layoutManager.findFirstCompletelyVisibleItemPosition());
}
then you can retrieve saved scroll position in onViewStateRestored() method
#Override
public void onViewStateRestored(#Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
int scrollPosition = savedInstanceState.getInt("scrolled_position");
recyclerView.scrollToPosition(scrollPosition);
}
}
You can use scrollToPosition or smoothScrollToPosition to scroll to any item position in RecyclerView.
If you want to scroll to item position in adapter, then you would have to use adapter's scrollToPosition or smoothScrollToPosition.

How do I call a fragment from inside a custom view

DogActivity is using a custom View. The custom view handles some logic and so has fields. When a particular field reaches a certain value, I want to start a fragment whose parent is DogActivity. How would I do that?
Is it advisable to put a callback inside a custom view so that it calls its parent activity? Or is there a simpler way?
When programming you should always look for consistency, i.e. look around you and see how similar stuff to what you want to do is done. The Android SDK makes heavy use of callback listeners, so they are the way to go here.
In fact we don't even need to know what kind of View your CustomView really is, we can build a general purpose solution. Don't forget to adapt/optimize according to your specific surroundings however. And think about abstraction and generalisation once you get to a point where all your Views are spammed with listeners!
You will need 3 things:
A listener interface
public interface OnCountReachedListener {
public void onCountReached();
}
A place to accept the listener and a place to alert the listener in your CustomView
public class CustomView extends View {
private int theCount;
private OnCountReachedListener mListener;
public void setOnCountReachedListener(OnCountReachedListener listener) {
mListener = listener;
}
private void doSomething() {
while (theCount < 100) {
theCount++;
}
// The count is where we want it, alert the listener!
if (mListener != null) {
mListener.onCountReached();
}
}
An implementation of the interface in your Activity
public class DogActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
View myView = new CustomView();
myView.setOnCountReachedListener(new OnCountReachedListener() {
#Override
public void onCountReached() {
Log.w("tag", "COUNT REACHED!");
// START YOUR FRAGMENT TRANSACTION HERE
}
});
}
}
For further information look at the source code of the View class and all the On**XY**Listener interfaces in the Android SDK. They should give you plenty to think about
What is the type of the field? Is it an EditText? SeekBar? Depending on the View, you'll be able to specify different listeners/callbacks to determine when they have changed and if they've reached a certain threshold. I would attach these listeners within onCreate of DogActivity. When the threshold is reached, use a FragmentTransaction to add your Fragment as the child of a container View in DogActivity.

Fragment's OnClickListener called after onDestroyView

I have an issue where ListFragment.onListItemClick is called after onDestroyView. I'm getting lots of error reports in the field (10-20 per day of ~1000 active users), but the only way I found to reproduce it is to hammer the back button while clicking all over the screen. Are hundreds of users really doing this?
This is the trace:
java.lang.IllegalStateException: Content view not yet created
at au.com.example.activity.ListFragment.ensureList(ListFragment.java:860)
at au.com.example.activity.ListFragment.getListView(ListFragment.java:695)
at au.com.example.activity.MyFragment.onListItemClick(MyFragment.java:1290)
at au.com.example.activity.ListFragment$2.onItemClick(ListFragment.java:90)
at android.widget.AdapterView.performItemClick(AdapterView.java:301)
at android.widget.AbsListView.performItemClick(AbsListView.java:1519)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:3278)
at android.widget.AbsListView$1.run(AbsListView.java:4327)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5293)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1102)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:869)
at dalvik.system.NativeStart.main(Native Method)
Caused from calling getListView().getItemAtPosition in MyFragment.onListItemClick (MyFragment:1290). How can getView return null during a click handler callback? I also determined the fragment was detached at this stage, isAdded() was false, and getActivity was null.
One workaround would be to replace getListView with the listView passed in from the callback
public void onListItemClick(ListView listView, View v, int position, long id), but other functions will still need to update other parts of the UI, so this would just move the problem somewhere else.
Instead, I nulled the callback in onDestroyView:
public void onDestroyView() {
mHandler.removeCallbacks(mRequestFocus);
if(mList!=null){
mList.setOnItemClickListener(null);
}
mList = null;
mListShown = false;
mEmptyView = mProgressContainer = mListContainer = null;
mStandardEmptyView = null;
super.onDestroyView();
}
But I still have this onClick problem in other (non-list) fragments too. How exactly does the framework suppress these callbacks normally when the fragment is removed (eg in onBackPressed -> popBackStackImmediate())?
In onDestroyView, I null out extra views that I created in onCreateView. Do I need to manually clear every listener I've set like this?
This is a similar issue to the unanswered q: Fragment's getView() returning null in a OnClickListener callback
I'm using setOnRetainInstance(true) in my fragments, btw.
You really haven't given very much information, but based off what you've given, it sounds like Fragment pending Transactions might be your issue.
In Android, whenever you are changing, or instantiating fragments, it's all done through Pending Transactions unless told to do otherwise. It's essentially a race condition.
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new ExampleFragment()
.commit();
The UI Thread has a queue of work that it needs to do at any given time. Even though you've committed the FragmentTransaction after running the above code, it's actually been queued on the UI Thread at the end of the queue, to happen after everything that is currently pending has been finished. What this means is that if click events happen while the transaction is pending (which can easily happen, i.e. you spamming clicks on the screen, or clicking with multiple fingers), those click events will be placed on the UI Threads queue after the FragmentTransaction.
The end result is that the Fragment Transaction is processed, your fragment View is destroyed, and then you call getView() and it returns null.
You could try a few things:
getSupportFragmentManager().executePendingTransactions() This will execute all pending transactions right then, and removes the pending aspect
Check to see if the Fragment isVisible() or isAdded() or some other fragment 'is' method that allows you to get runtime information about the current state the Fragment is in it's lifecycle, before you execute code that could potentially be run after the fragments view is destroyed (i.e. click listeners)
So lets say you have a click handler, where when the user clicks something you animate to another fragment. You could use something like the below piece of code that you run before the FragmentTransaction on your outermost view (in a Fragment, it'd be what returns from getView()), and that would either permanently disable clicks to a view if it was going to be destroyed, or temporarily disable clicks for a a period of time if you are going to re-use the view.
Hope this helps.
public class ClickUtil {
/**
* Disables any clicks inside the given given view.
*
* #param view The view to iterate over and disable all clicks.
*/
public static void disable(View view) {
disable(view, null);
}
/**
* Disables any clicks inside the given given view for a certain amount of time.
*
* #param view The view to iterate over and disable all clicks.
* #param millis The number of millis to disable clicks for.
*/
public static void disable(View view, Long millis) {
final List<View> clickableViews = (millis == null) ? null : new ArrayList<View>();
disableRecursive(view, clickableViews);
if (millis != null) {
MainThread.handler().postDelayed(new Runnable() {
#Override public void run() {
for (View v : clickableViews) {
v.setClickable(true);
}
}
}, millis);
}
}
private static void disableRecursive(View view, List<View> clickableViews) {
if (view.isClickable()) {
view.setClickable(false);
if (clickableViews != null)
clickableViews.add(view);
}
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
for (int i = 0; i < vg.getChildCount(); i++) {
disableRecursive(vg.getChildAt(i), clickableViews);
}
}
}
}
Bet my arm it's due to extra stateless fragments living somewhere inside of your app. I'd personally discourage retaining instance and let Android do what it can with it, while you use standard mechanism to keep your state (saveInstanceState, database, high-level classes/patterns, SharedPreferences, etc).
Personally I've had plenty of issues when retaining a fragment instance (normally when re-creating or re-activating fragments through config changes or leaving and re-entering the app), resulting generally on two fragments, one of them connected to views, stateless, thus useless; and the "real" one keeping the previous state without any connection to views, hence ending up with exceptions and all sort of fanciness you don't want to have.
Start by not retaining the instance and see what comes up.
You could use mHandler.removeCallbacksAndMessages(null) work in many situations for me.

Categories

Resources