The scroll state of an item on a view pager is not getting saved and restored on android KitKat and older devices (I've tested API 15,17,19,22,23 at this point)
The scroll view is in a fragment inside another fragment with a view pager.
The view pager adapter is a FragmentStatePagerAdapter
When the pager adapter saves the state, it calls android.support.v4.app.FragmentManager#saveFragmentInstanceState.
From there we end up in android.support.v4.app.FragmentManagerImpl#saveFragmentViewState but a call to ScrollView#saveHierarchyState doesn't add a saved state to the bundle like it does on newer devices. Specifically the newer devices add an instance of android.widget.HorizontalScrollView.SavedState (That's what the debugger says anyhow, the code suggests it would be a android.widget.ScrollView.SavedState whereas the older API devices add android.view.AbsSavedState#EMPTY_STATE
void saveFragmentViewState(Fragment f) { //f is my fragment
if (f.mInnerView == null) {
return;
}
if (mStateArray == null) {
mStateArray = new SparseArray<Parcelable>();
} else {
mStateArray.clear();
}
// f.mInnerView is a ScrollView
f.mInnerView.saveHierarchyState(mStateArray); //should save state here
if (mStateArray.size() > 0) {
f.mSavedViewState = mStateArray;
mStateArray = null;
}
}
Further investigation of what should be happening lead me to android.widget.ScrollView#onSaveInstanceState which actually saves the scroll position into android.widget.ScrollView.SavedState#scrollPosition
When I diffed ScrollView.java between android API 23 and 19, I found that newer versions had indeed added code which saves and restores this state.
I discovered that the support library widget, NestedScrollView, was the easiest solution as it saves scroll state properly. As the linked documentation states, it doesn't actually need to be nested as the same class can be the parent or nested child. One important change I needed to make was to include the scrollbars which aren't there by default for some reason with this class.
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/my_scroller"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="#style/AppTheme"
android:scrollbars="vertical"
tools:context="com.example.myapplication.FragmentOne">
Related
I am working on an Android app that runs on only one devicerunning KitKat.
The smooth scrolling feature for a RecylerView I used that was working on other physical tablets and genymotion has unfortunately stopped working on the one device it needs to work on.
Instead of scrolling to a certain position it passes over the target position and scrolls all the way to the bottom and looks really bad.
I am able to track down the error to the abstract SmoothScroller in the RecyclerView class.
if (getChildPosition(mTargetView) == mTargetPosition) {
onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
mRecyclingAction.runIfNecessary(recyclerView);
stop();
} else {
Log.e(TAG, "Passed over target position while smooth scrolling.");
mTargetView = null;
}
I was using a SnappingLinearLayoutManager that I found online, but swapped it out with the normal LinearLayoutManager from Android, and still am having the same problem.
The list is 7 items long (user can see 4 at a time) and I scroll to the 5th item (position 4) item.
When I scroll to the 3rd I don't receive this error.
Also after I scroll the list up and down once, the error stops happening.
EDIT:
I am able to use layoutManager.scrollToPositionWithOffset(); But I am trying to do this with the smooth scroll animation.
Here is some of my code and details:
private void setupMainRecyclerViewWithAdapter() {
mainLayoutManager = new SnappingLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mainListRecyclerView.setLayoutManager(mainLayoutManager);
settingsMainListAdapter = new SettingsListAdapter(SettingsActivity.this,
settingsPresenter.getSettingsItems(),
settingsPresenter);
mainListRecyclerView.setAdapter(settingsMainListAdapter);
mainListRecyclerView.addItemDecoration(new BottomOffsetDecoration(EXTRA_VERTICAL_SCROLLING_SPACE));
}
#Override
public void scrollMainList(boolean listAtTop) {
if(listAtTop) {
mainListRecyclerView.smoothScrollToPosition(4);
moveMainMoreButtonAboveList();
} else {
mainListRecyclerView.smoothScrollToPosition(0);
moveMainMoreButtonBelowList();
}
}
If you call recyclerView.smoothScrollToPosition(pos) will be called immediately on the UI thread and if recyclerView's Adapter is too much busy to generating view items then the calling of smoothScrollToPosition will be missed then because recyclerView has no data to smooth scroll. So it's better to do that in a background thread by recyclerView.post(). By calling this it goes into the Main thread queue and gets executed after the other pending tasks are finished.
Therefore you should do something like this which worked for my case:
recyclerView.post(new Runnable() {
#Override
public void run() {
recyclerView.smoothScrollToPosition(pos);
}
});
Well, I realize it's too late, however I tried some different solutions and found one...
in custom LinearSmoothScroller I override updateActionForInterimTarget
#Override
protected void updateActionForInterimTarget(Action action) {
action.jumpTo(position);
}
It's appears not very smooth, but not instant in contrast with scrollToPositionWithOffset.
Just add one line for smooth scroll
recyclerView.setNestedScrollingEnabled(false);
it will work fine
Take a look at hasPendingAdapterUpdates(). You can use this along with a delay() for coroutines or Thread.sleep() to enable the backing data to be available before doing the scroll.
I'm using AppGyver Steroids and Supersonic to build an app and I'm having some issues navigating between views programmatically.
Based on the docs, you navigate between views like this:
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.push(view_obj);
However, when I inspect things via the Chrome DevTools, it appears that a second duplicate view is created i.e. If I navigate away from the index page and then navigate back, I now have two index pages, instead of what [I think] should be one. It also doesn't close the previous view I was on.
How can I prevent this from happening and simply move to the existing view, instead of duplicating views? How do I close a view after I have navigated away from it?
Thanks.
The problem you're encountering is that you're creating a new supersonic.ui.View("main#index") every time you navigate. On top of this, I think you want to return to the same view when you navigate back to a view for the second time, i.e. you want the view to remain in memory even if it has been removed from the navigation stack with pop() (rather than pushing a new instance of that view). For this, you need to preload or "start()" the view, as described in the docs here.
I implemented my own helper function to make this easier; here is my code:
start = function(dest, isModal) {
var viewId=dest,
view=new supersonic.ui.View({
location: dest,
id: viewId
});
view.isStarted().then(function(started) {
if (started) {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
} else {
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
Use it like start('module#view');. As a bonus, you can pass true as the second argument and it gets pushed as a modal instead.
It checks if you've already started a view - if so, it just pushes that view back onto the stack. If not, it start()s (i.e. preloads) it, then pushes it. This ensures that the view stays in memory (with any user input that has been modified) even when you pop() it from the stack.
You have to imagine that the layer stack is actually a stack in the Computer Science sense. You can only add and remove views at the top of the stack. The consequence of this is that complex navigations such as A > B > C > D > B are difficult/hacky to do (in this case, you'd have to pop() D and C in succession to get back to B).
Views will close if you pop() them, as long as you didn't start() them. If you did, and you pop() them, they remain in memory. To kill that view, you have to call stop() on it, as described in the docs I linked above.
try
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.replace(view_obj);
And take a look at supersonic.ui.layers.pop();
Thanks to LeedsEbooks for helping me get my head around this challenge. I was able to find a solution. Here is the code:
var start = function(route_str, isModal) {
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view_id_str = match_obj[2],
view_location_str = route_str,
view = new supersonic.ui.View({
location: view_location_str,
id: view_id_str
});
view.isStarted().then(function(started) {
if (started)
{
if (isModal)
{
supersonic.ui.modal.show(view);
}
else {
supersonic.ui.layers.push(view);
}
}
else
{
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal)
{
supersonic.ui.modal.show(view);
}
else
{
supersonic.ui.layers.push(view);
}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
You must ensure that your route has the format module#view as defined in the documentation.
PLEASE NOTE
There seems to some problem with the supersonic ui method for starting views. If you run the following code:
supersonic.ui.views.start("myapp#first-view");
supersonic.ui.views.find("first-view").then( function(startedView) {
console.log(startedView);
});
You'll notice that your view id and location are identical. This seems to be wrong as the id should be first-view and location should be myapp#first-view.
So I decided to not use the AppGyver methods and create my own preload method instead, which I run from the controller attached to my home view (this ensures that all the views I want to preload are handled when the app loads). Here is the function to do this:
var preload = function(route_str)
{
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view = new supersonic.ui.View({
location: route_str,
id: match_obj[2]
});
view.start();
};
By doing this, I'm sure that the view will get loaded with the right location and id, and that when I use my start() function later, I won't have any problems.
You'll want to make sure that your structure.coffee file doesn't have any preload instructions so as not to create duplicate views that you'll have problems with later.
Finally, I have a view that is 2 levels in that is a form that posts data via AJAX operation. I wanted the view to go back to the previous view when the AJAX operation was complete. Using my earlier function resulted in the push() being rejected. It would be nice if AppGyver Supersonic could intelligently detect that pushing to a previous view should default to a layers.pop operation, but you don't always get what you want. Anyway, I managed to solve this using supersonic.ui.layers.pop(), which simply does what the Back button would have done.
Everything working as intended now.
I build a recyclerview like so:
recyclerView = (RecyclerView) view.findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
currentResults = new ArrayList<Stuff>();
stuffAdapter = new StuffAdapter(currentResults, getActivity());
recyclerView.setAdapter(stuffAdapter);
then when the user searches for things and results are returned, they're added to the adapter in the normal way
currentResults.clear();
currentResults.addAll(searchResponse.results);
restaurantAdapter.notifyDataSetChanged();
this is all very standard. the results are obtained via a searchview widget in an appcompat toolbar and a retrofit callback.
the problem is that there is one particular point where the user can close the keyboard, and the UI adjusts to fit, and then the whole thing crashes with
java.lang.UnsupportedOperationException: RecyclerView does not support scrolling to an absolute position. at
android.support.v7.widget.RecyclerView.scrollTo(RecyclerView.java:941) at
android.view.View.setScrollX(View.java) at
android.animation.PropertyValuesHolder.nCallIntMethod(Native Method) at
android.animation.PropertyValuesHolder.access$200(PropertyValuesHolder.java)
at android.animation.PropertyValuesHolder$IntPropertyValuesHolder.setAnimatedValue(PropertyValuesH
Nobody is calling scrollto. I don't get why it's throwing this. Any insight would be appreciated.
Reading from another SO Question :
java.lang.UnsupportedOperationException: RecyclerView does not support scrolling to an absolute position
The problem is with some specific devices only, because of there Implementation of ICS. As the suggested work around, you can make a CustomRecyclerView that extend RecyclerView and override scrollTo that catch the error and stop the application to crash.
#Override
public void scrollTo(int x, int y) {
Log.e(TAG, "CustomRecyclerView does not support scrolling to an absolute position.");
// Either don't call super here or call just for some phones, or try catch it. From default implementation we have removed the Runtime Exception trown
}
EDIT: tl;dr: WebView appears as white box, even though I appear to be setting it up correctly, and indeed it does work the first two times, but fails subsequently)
EDIT: Video showing the problem in action...
I have the following bit of code which inflates a view (Which contains a WebView) from the xml which defines it:
private void createCard(ViewGroup cvFrame, Card card) {
//... setup vairables...
cvFrame.clearDisappearingChildren();
cvFrame.clearAnimation();
try {
View cv = LayoutInflater.from(getBaseContext()).inflate(R.layout.card_back_view,
cvFrame, true);
cv.setBackgroundDrawable(Drawable.createFromStream(mngr.open(deckName + "_Card_back.png"), deckName));
TextView suit = (TextView)cv.findViewWithTag("card_back_suit");
//...setup text view for suit, this code works fine every time...
WebView title = (WebView)cv.findViewWithTag("card_back_title");
//This WebView doesn't appear to be the one which actually appears on screen (I can change settings till I'm blue in the face, with no effect)
if (title != null) {
title.setBackgroundColor(0x00000000);
title.loadData(titleText, "text/html", "UTF-8");
} else {
Log.e("CardView", "Error can't find title WebView");
}
} catch (IOException e) {
Log.e("CardView", "Error making cards: ", e);
}
}
When this method is called as part of the onCreate method in my Activity, the WebView contains the correct code, and is suitably transparent.
I have a gesture listener which replaces the contents of the ViewGroup with different content (It animates the top card off to the left, replaces the contents of the top card with card 2, puts the top card back, then replaces card 2 with card 3)
//Gesture listener event
ViewGroup cvFrame = (ViewGroup)findViewById(R.id.firstCard);
cardLoc++
cvFrame.startAnimation(slideLeft);
(onAnimationEnd code)
public void onAnimationEnd(Animation animation) {
if (animation == slideLeft) {
ViewGroup cvFrameOldFront = (ViewGroup)findViewById(R.id.firstCard);
ViewGroup cvFrameNewFront = (ViewGroup)findViewById(R.id.secondCard);
createCard(cvFrameOldFront, cards.get((cardLoc)%cards.size()));
createCard(cvFrameNewFront, cards.get((cardLoc+1)%cards.size()));
TranslateAnimation slideBack = new TranslateAnimation(0,0,0,0);
slideBack.setDuration(1);
slideBack.setFillAfter(true);
cvFrameOldFront.startAnimation(slideBack);
}
}
When the animation has happened and I replace the contents of the cards, the TextView suit is replaced fine and the code definitely passes through the code to replace the WebView contents, but for some reason I end up with a white rectangle the size and shape of the WebView, no content, no transparency.
If I change the WebView to a TextView, it's contents is replaced fine, so it's an issue that occurs only with the WebView control :S
Can anyone tell me why / suggest a fix?
It turns out the WebView doesn't get cleared down when using the LayoutInflater to replace the contents of a ViewGroup. The other controls all seem to get removed (or at least the findViewWithTag() returns the right reference for every other control). I've just added in the line cvFrame.removeAllViews() immediately before the LayoutInflater does it's stuff and that fixed the issue.
If anyone has any better explanation for this I'll throw the points their way otherwise they will just go into the ether...
By calling findViewById, you are getting a reference on the previously loaded webview do you ?
so the loadData call that fails is the second one you make on a single webview instance.
you may want to check this :
Android WebView - 1st LoadData() works fine, subsequent calls do not update display
It appears that loadData() won't load data twice... you may want to try WebView.loadDataWithBaseUri()
Hope that helps.
I had a similar problem loading several WebViews content.
It was because of a misusing of the pauseTimers function
The situation was : the first webView weren't needed anymore, conscientiously I wanted to pause it before to release it. Calling onPause() and pauseTimers()
pauseTimers being common to any web views, it broke every use of webviews occuring after that, there were displaying only white rectangles.
Maybe its not your problem here, but it's worth checking your not calling WebView.pauseTimers() somewhere.
To confirm your answer, the source code for LayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot) does in fact internally calls root.addView() which attaches the newly inflated view at the end of the root's children instead of replacing them.
So the mystery now is why did your call to findViewWithTag() is returning the expected objects for your other widgets (which would be the top, most recently created instances), but for your WebView it was returning something else.
Is it possible that there is another object in your layout XML which shares the same "card_back_title" tag?
I was also wondering why you didn't use the more common findViewById() instead, but I am not sure whether it would make a difference.
Good day everyone.
I am creating a calendar component, and I'm working in the month view. I have created a view named MonthView, and I am adding a couple instances of this to a ViewFlipper:
viewFlipper = new ViewFlipper(getContext());
viewFlipper.addView(new MonthView(viewFlipper.getContext()));
viewFlipper.addView(new MonthView(viewFlipper.getContext()));
I have implemented the fling gesture so that I change views when sliding my finger left or right. This will cyclically update and display the months.
Now, I need to give the fling gesture a smoothly effect when touching and slowly sliding my finger. The same we get when we use a Slider instead a ViewFlipper.
The problem with Scroller is that the effect is not cyclic. Once I get to the last view, I have to slide in the other direction.
I need someone help me find how to give a scroll-like effect to the ViewFlipper, or how to make a Scroller cyclic.
Thanks in advance.
Extra comment:
I have already implemented a ViewFlipper with 2 views. I update the views by using the SimpleOnGestureListener.onFling(...) method, and the behavior I got is something like this:
Imagine I always slide from rigth to left, like flipping a book's page to read the next one, and also imagine there is a caption in the header of the view that is displayed after flipping.
View # 0 --> Caption: January 2011
View # 1 --> Caption: Febrary 2011
View # 0 --> Caption: March 2011
View # 1 --> Caption: April 2011
View # 0 --> Caption: May 2011
If at this point I slide from left to right, the result will be something like:
View # 1 --> Caption: April 2011
View # 0 --> Caption: March 2011
The ability to cyclically move forward or backward, giving the user the idea of having infinite views, but using only a couple is characteristic of ViewFlipper, and that's what I can't loose.
That's why I need a way to add the cool scroll effect without loosing what I've got.
Thanks.
Then you can use ViewFlinger!
viewflinger this an android widget (extends ViewGroup) that allows to group a set of views that can be swiped horizontally. It offers a smoother transition that cannot be accomplished using ViewFlipper. This is based on the Workspace class on the iosched project, which is based in the class with the same name of the Launcher app.
Download: 1.0.2 | Sources | JavaDoc
If you use Maven, you can use it as an artifact from this repository: http://mvn.egoclean.com/. Also, you would want to look this video where I show how it looks like: http://www.youtube.com/watch?v=gqIXq5x7iLs (sorry for my accent which sucks)
I think what you wanted to do is create an apparently infinite list of layouts being flinged by either the ViewFlipper or Christian's ViewFlinger. And also you want to keep reusing views / layouts inside the Flinger / Flipper. Right ?
If yes, probably the following is what you wanted to do. I've done this based on Christian's ViewFlinger,
Here you go,
First add three layouts to the ViewFlinger:
<com.egoclean.android.widget.flinger.ViewFlinger
android:id="#+id/calendarViewFlipper"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ScrollView
android:id="#+id/calendarViewLayout0"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</ScrollView>
<ScrollView
android:id="#+id/calendarViewLayout1"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</ScrollView>
<ScrollView
android:id="#+id/calendarViewLayout2"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
</ScrollView>
</com.egoclean.android.widget.flinger.ViewFlinger>
Then inside your activity, you take an array of three views so that you can access them directly through the array instead of searching every time inside the flinger,
private ViewFlinger viewFlinger;
private ViewGroup layouts[] = new ViewGroup[3];
private boolean userEvent = false;
#Override
public final void onCreateSub(Bundle savedInstanceState)
{
setContentView(R.layout.your_layout);
viewFlinger = (ViewFlinger) findViewById(R.id.calendarViewFlipper);
layouts[0] = (ViewGroup) findViewById(R.id.calendarViewLayout0);
layouts[1] = (ViewGroup) findViewById(R.id.calendarViewLayout1);
layouts[2] = (ViewGroup) findViewById(R.id.calendarViewLayout2);
viewFlinger.setOnScreenChangeListener(new ViewFlinger.OnScreenChangeListener()
{
#Override
public void onScreenChanging(View newScreen, int newScreenIndex)
{
}
#Override
public void onScreenChanged(View newScreen, int newScreenIndex)
{
if (userEvent)
{
ViewGroup tempLayout = null;
if (newScreenIndex != 1)
{
// We don't want our actions to raise events and create a cyclic event chain
userEvent = false;
if (newScreenIndex == 2) // Scrolling towards right
{
tempLayout = layouts[0];
viewFlinger.removeViewFromFront();
viewFlinger.addViewToBack(tempLayout);
layouts[0] = layouts[1];
layouts[1] = layouts[2];
layouts[2] = tempLayout;
// Any other logic comes here...
}
else if (newScreenIndex == 0) // Scrolling towards left
{
tempLayout = layouts[2];
viewFlinger.removeViewFromBack();
viewFlinger.addViewToFront(tempLayout);
layouts[2] = layouts[1];
layouts[1] = layouts[0];
layouts[0] = tempLayout;
// Any other logic comes here...
}
// We switch the screen index back to 1 since the current screen index would change back to 1
viewFlinger.setCurrentScreenNow(1, false);
userEvent = true;
// And any other logic that you'd like to put when the swapping is complete May be fill the swapped view with the correct values based on its new location etc...
View result = refreshView(tempLayout.getChildAt(0));
if (result.getParent() != tempLayout)
{
((ViewGroup) result.getParent()).removeView(result);
tempLayout.removeAllViews();
tempLayout.addView(result);
}
}
}
}
});
}
I hope this is clear to you and helps you with your problem. It is working very fine for me! Should work fine for you too.
P.S. Thanks # Christian for the ViewFlinger, it is awesome. However it lacks some good onConfigurationChanged logic, if you get time do put something in :). The rest is the best !