We've all been advised against nesting views that contain a scrolling mechanism. However, in the latest Android release (5.0), the Phone app caught my attention with what seems to be a ListView inside of a ScrollView.
What really intrigued me was that the scrolling mechanism switched from the ScrollView to the ListView seamlessly.
Notice the content above the tabs is pushed out of view before the actual ListView begins scrolling.
I've tried duplicating this myself, but ended up unsuccessful. Here is the basic approach I was taking...
With a single, continuous touch event (no lifting of the finger) ...
As user scrolls, the ListView slowly covers up the ImageView. Once the ImageView is 100% covered and the ListView takes up the entire screen, the ListView begins to scroll.
I'm currently listening to touch events on the ListView and if the top has been reached, call requestDisallowInterceptTouchEvent on the ListView, i.e.
#Override
public boolean onTouch(View v, MotionEvent event) {
if (listViewAtTop) {
v.requestDisallowInterceptTouchEvent(true);
} else {
v.requestDisallowInterceptTouchEvent(false);
}
return false;
}
The switching scrolling context works, only if you lift your finger and continue scrolling.
Is there a different approach that will achieve the desired effect?
Android 5.0 Lollipop (API 21) added nested scrolling support.
From what I can tell, both ListView (AbsListView) and ScrollView support this now (if running on API 21), but it must be enabled on the scrolling views.
There are two ways, by calling
setNestedScrollingEnabled(true) or with the layout attribute android:nestedScrollingEnabled="true" (which is undocumented)
To learn about how it works, or to support this for a custom widget, the key methods are these:
onStartNestedScroll
onNestedScrollAccepted
onNestedPreScroll
onNestedScroll
onStopNestedScroll
Unfortunately, there is no guide or training which explains how this works other than the JavaDoc itself which is rather light and there are no examples other than ScrollView.
Add latest support package 'com.android.support:support-v4:22.1.1' to your project. And try this:
<android.support.v4.widget.NestedScrollView
android:id="#+id/nScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<FrameLayout ...>
<ListView ... />
</FrameLayout >
</android.support.v4.widget.NestedScrollView>
By default nested scrolling is Enabled.
While trying to figure out how to solve this issue myself, I found this question first; however, the answer didn't really go into too much detail. I did find a lot of good resources, so if anyone else finds themselves looking for this, I'll link them below. A term for this effect is "Sticky Scrolling".
An article talking about "Synchronized Scrolling".
http://www.pushing-pixels.org/2011/07/18/android-tips-and-tricks-synchronized-scrolling.html
A good video showcasing some Android scrolling tricks, "Quick Return" and "Sticky Scrolling".
https://www.youtube.com/watch?v=PL9s0IJ9oiI
Code:
https://code.google.com/p/romannurik-code/source/browse/misc/scrolltricks
And lastly, here is another one showcasing the same effect using a listView instead of a ScrollView.
https://www.youtube.com/watch?v=rk-tLisxSgM
Code:
https://github.com/jatago/list_sticky_scroll_trick
I found an alternative 'trick' which is quite simple... Use only a ListView with an added transparent header.
I have been wanting to achieve the same effect as well. I came up finding a relevant library called ObservableScrollView in GitHub and it requires more work on the back-end via a TouchInterceptFramework but at least it did the job even for pre-lollipop devices. It also supports not only child scrollviews and listviews but also recyclerviews. Here's the link:
https://github.com/ksoichiro/Android-ObservableScrollView
I hope they consider nested scrolling for both lollipop and pre-lollipop devices as a part of their design standard soon. This is a good sign.
This is classic example of dummy layouts. Something not entirely obvious at first look. Basically the scenario is something like this.
Grey Area->FrameLayout
Followed by a listview that fills up the entire framelayout and followed by a imageview that overlaps the top half of a listview. The listview's first item is a dummy item and has a height identical to that of the imageview.
(Note: The actual data starts from the second element)
Next Step is easy
Translate the Imageview as per the scroll of the listview.
I suppose this is the best way to do that whilst avoiding nested scrolling
You can use the following combination of attributes on your ListView to achieve this:
<ImageView ... /> <!-- must be before ListView -->
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="..." <!-- height of imageView -->
android:clipToPadding="false"
...
/>
You don't have to manage any scrolling in your code at all, and it requires no header/dummy views in your list adapter.
I am using something like this and it works ok I think
scrollView.onScroll { x, y ->
Timber.d("ScrollView offset: ($x, $y)")
val height = dashboardChart.measuredHeight
val recyclerView = viewPager.findViewById<RecyclerView>(R.id.recyclerView)
if(y >= height) {
Timber.d("ScrollView enable nested scrolling!")
recyclerView.isNestedScrollingEnabled = true
} else {
Timber.d("ScrollView disable nested scrolling!")
recyclerView.isNestedScrollingEnabled = false
}
}
Where scrollView is parent I am listening onScroll event (it is extension underneath it is viewTreeObserver.addOnScrollListener). Then depending whether I've scrolled initial offset or not I am enabling/disabling child recyclerView (similary ListView or other scrollView) scrolling.
Related
I started using ConstraintLayout a few weeks ago and I am finding (I believe) that it has many bugs. One huge issue is that I cannot set view visibilities. The following code does not work:
<TextView
android:visibility="gone"
android:id="#+id/sampleView"
style="#style/RSTextView"
android:layout_width="0dp"
android:text="sample text"
android:textSize="20dp"
android:shadowColor="#00000000"
app:layout_constraintStart_toEndOf="#+id/privacyPolicyText"
app:layout_constraintTop_toBottomOf="#+id/enterAgreementText1" />
You can see I've marked it as "gone", but when I run and inspect the view using the layout inspector, visibility is STILL set to visible. I've had to come up with hacky solutions to this involving setting the alpha to 0 and isEnabled to false when I want a view to be gone and vice versa. When I try to set the visibility programmatically, I have the same issue:
sampleView.visibility = ConstraintLayout.GONE
This still results in a visible view. This is driving me nuts because I'm forced to make multi-line hacky solutions. Any help would be appreciated.
Be careful using Groups in Constraint Layout. Main things to note while using groups are
Group's visibility overrides normal view's visibility
If the group's visibility is unset, it defaults to visible and overrides your view's visibility.
While using Constraint Layout be careful in using Groups. I think almost all visibility issues in constraint layout are caused by groups.
I don't know if the below links I found are related to your problem:
https://issuetracker.google.com/issues/37151322
https://issuetracker.google.com/issues/37139335
https://issuetracker.google.com/issues/37138937
but they are all filed reports about problems/bugs concerning the visibility of views inside a ConstraintLayout.
Check them.
#Pavan Varma pointed out that Group visibility overrides the view's visibility. I thought that if the groups visibility was unset, it wouldn't override anything, but it still does. If you're having issues with visibility in ContraintLayouts, pay close attention to its group's visibility that overrides it. If the group's visibility is unset, it defaults to visible and overrides your view's visibility.
EDIT:
ADDITIONAL DETAILS: Strangely enough, it only gets buggy if there are only <=3 elements. More than that, because every element can be scrolled away, then the layout will "fix" itself upon reshowing.
ADDITIONAL DETAILS: Link to the demo video that showing the problem.
Youtube Video - https://youtu.be/zlwi_Bz-HQo
So below screenshot, was taken from the same run from an android studio.
In the screenshot, I have 2 exact same card view (with dummy data straight from the .xml, so, I assign no data from java file).
The problem
If you guys look at the bottom right of each CardView, there's a
text view called "read more".
Weirdly enough, even if they're identical, it's placed differently.
Btw, it's actually 3 identical cards. On the first run, the top "read
more" also incorrectly placed, but it auto-corrected itself when I
completely scroll it down and back to the top.
The second problem is the 5x4 dots on the top of the card. It's differently placed from what it's seen from the editor.
(The placement is accurate on editor)
Any idea how to handle this irregularity? Thanks.
Btw, I'm not sure if you guys need the code, but just in case, here it is on Pastebin (to shorten the post length).
visit_note_timeline.xml (the cardview)
VisitNoteAdapter.java (In case you're wondering, "visit note" is just a dummy empty class)
MainActivity.java
ratings_previews.xml (the 5x4 white dots on top right of the card, under more button)
activity_main.xml
content_main.xml
Use match_parent for RecyclerView.
Fix like this:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/recycler_view_timeline"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
If parent view's width is wrap_content, parent view's width can not be determined until child view's width is determed.
I don't know the result of setting layout_width="wrap_content" for parent view and layout_width="match_parent" for child view.
I guess the result may be wrong layout.
In your layout, the same situation is happend.
RecyclerView has layout_width="wrap_content" and CardView has layout_width="match_parent".
I have a nested layout like the following:
<LinearLayout> <!----Parent layout--->
<LinearLayout> <!-----child 1--->
...
</LinearLayout> <!----child 1 ended--->
<LinearLayout> <!-----child 2--->
...
</LinearLayout> <!----child 2 ended--->
</LinearLayout> <!----Parent endded--->
The problem I am having now is that since all my data items are within child 1 or child 2 Linearlayout, if I add or delete a item the child linearlayout will animated with the effect of animateLayoutChanges but the parent layout will not do any animation. (I have android:animateLayoutChanges set to true for all linear layouts). Especially when I delete an item within child 1 the animation effect becomes weird (basically child 2 will jump up while child 1 is still doing its animation).
Does anybody have any idea how to solve this?
Thanks
UPDATE
Shortly after I posted this question, I found this on android developer's site in the LayoutTransition API.
Using LayoutTransition at multiple levels of a nested view hierarchy may not work due to the interrelationship of the various levels of layout.
So does anyone have any work around suggestions for this issue?
The animateLayoutChanges property makes use of LayoutTransitions, which animate both the layout's children and, from Android 4.0 onward, ancestors in the layout hierarchy all the way to the top of the tree. In Honeycomb, only the layout's children will be animated. See this Android Developers Blog post for details.
Unfortunately, it seems that there's currently no simple way to have the siblings of a layout react to its LayoutTransitions. You could try using a TransitionListener to get notified when the layout's bounds are being changed, and move the sibling views accordingly using Animators. See Chet Haase's second answer in this Google+ post.
EDIT - Turns out there is a way. In Android 4.1+ (API level 16+) you can use a layout transition type CHANGING, which is disabled by default. To enable it in code:
ViewGroup layout = (ViewGroup) findViewById(R.id.yourLayout);
LayoutTransition layoutTransition = layout.getLayoutTransition();
layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
So, in your example, to have child 2 layout animated, you'd need to enable the CHANGING layout transformation for it. The transformation would then be applied when there is a change in the bounds of its parent.
See this DevBytes video for more details.
Ok, after digesting the first answer, I make it simple here, for those who don't get proper animation result when using android:animateLayoutChanges="true" in NESTED layout:
Make sure you add android:animateLayoutChanges="true" to the will-be-resized ViewGroup (LinearLayout/RelativeLayout/FrameLayout/CoordinatorLayout).
Use setVisibility() to control the visibility of your target View.
Listen carefully from here, add android:animateLayoutChanges="true" to the outer ViewGroup of your will-be-resized ViewGroup, this outer ViewGroup must be the one who wraps all the position-changing View affected by the animation.
Add following code in your Activity before the setVisibility(), here the rootLinearLayout is the outer ViewGroup I mentioned above:
LayoutTransition layoutTransition = rootLinearLayout.getLayoutTransition();
layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
Before:
After:
Reminder: If you miss the 3rd step, you will get null pointer exception.
Good luck!
As a Kotlin Extension
fun ViewGroup.forceLayoutChanges() {
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
}
Usage
someContainer.forceLayoutChanges()
Notes:
In my experience, this happens when the container is a deep nested layout. For some reason android:animateLayoutChanges="true" just doesn't work, but using this function will force it to work.
We had added the android:animateLayoutChanges attribute to our LinearLayout but the change didn’t trigger an animation. To fix that, use this code:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
((ViewGroup) findViewById(R.id.llRoot)).getLayoutTransition()
.enableTransitionType(LayoutTransition.CHANGING);
}
More details.
It seems that a delayed transition on the parent also works for animating. At least for me the following code gives a proper expand/collapse animation.
expandTrigger.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
TransitionManager.beginDelayedTransition(parentLayout);
expanded = !expanded;
child1.setVisibility(expanded ? View.VISIBLE : View.GONE);
}
});
For deeply nested layouts you sometimes might need to use a parent higher up in the hierarchy in the call to the TransitionManager.
I had a similar issue:
I was using a animatelayoutchanges in one of my activity with a recycler view, I also added some custom layout transition because I wanted to increase speed of the animation while an item disappears in the list. It was working fine when it was not in a nested layout.
I had used the same adapter for another recyclerview which was in a nested layout. It was not working and I tried all the above solutions, None worked for me.
The real reason was, I forgot to set
mTicketsListAdapter.setHasStableIds(true);
in the nested layout activity. And after setting setHasStableIds to true, the animations was working perfectly in the nested layout.
I have used HorizontalListView which does the functioning of ListView but in horizontal manner. This HorizontalListView doesn't show fadingEdge on scroll to first and last items of the list.So I need to add fadingEdge on this view.I may find the scroll of first and last child,but I don't have any idea on how to show fadingEdge when scroll reaches to first and last child as done in ListView.Further I have already added
<com.ui.widgets.HorizontalListView
android:id="#+id/horizontalListView"
android:layout_width="fill_parent"
android:layout_height="120dip"
android:gravity="left"
android:fadingEdge="horizontal"/>
And it doesn't have any effect.So could you point me in the right direction on how to programmatically add fadingEdge to the HorizontalListView
From the documentation for fadingEdge:
This attribute is deprecated and will be ignored as of API level 14 (ICE_CREAM_SANDWICH). Using fading edges may introduce noticeable performance degradations and should be used only when required by the application's visual design. To request fading edges with API level 14 and above, use the android:requiresFadingEdge attribute instead.
Try using the android:overScrollMode attribute.
Just add
android:overScrollMode="always"
to your View.
android:overScrollMode can have three values: "always", "never", "ifContentScrolls".
I have a quite complex View build-up, and as a part of that, I have a ListView inside a LinearLayout within a ScrollView (and quite a lot more components, but they do not matter in this issue).
Now the whole activity scrolls nicely as it should, but the ListView has a limited height, and when the Items inside it surpass the height, the disappear of my screen. I've tried to place the ListView inside it's own ScrollView, but this doesn't work. When I try to scroll on the ListView, the main ScrollView is selected and my screen scrolls instead of the ListView.
My question may sound easy, but I haven't been able to fix this... Is it possible to make the ListView scrollable aswell?
The relevant XML:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:id="#+id/GlobalLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView android:id="#+id/EndpointList"
android:choiceMode="multipleChoice"
android:layout_height="175dip"
android:layout_width="fill_parent" />
</LinearLayout>
</ScrollView>
Instead of ListView with other layouts inside ScrollView create one ListView with header and footer.
Add views that should be above ListView as a header:
addHeaderView(View v)
and that below as a footer:
addFooterView(View v)
Put everything what should be above ListView to header of ListView and the same with footer.
LayoutInflater inflater = LayoutInflater.from(this);
mTop = inflater.inflate(R.layout.view_top, null);
mBottom = inflater.inflate(R.layout.view_bottom, null);
list.addHeaderView(mTop);
list.addFooterView(mBottom);
// add header and footer before setting adapter
list.setAdapter(mAdapter);
In result you'll get one scrollable view.
Actually, the way that I have it set up is really working... Placing a ListView in a LinearLayout within a ScrollView. Just avoid that the ListView is the direct child of the ScrollView, and it will work out just fine...
Just be aware that if there aren't enough items in the ListView to fill it so it goes 'off screen', that it won't scroll (kind of logically though). Also note that when you have enough items to scroll through, you need to keep pressing on an item in the ListView to make it scroll, and half of the time, focus is given to the global scrollview in stead of the ListView... To avoid this (most of the time), keep pressing on the most top or most down item, depending on which way you want to scroll.This will optimize your chance to get focus on your ListView.
I've made a video that it is possible, am uploading it now to YouTube...
Video is http://www.youtube.com/watch?v=c53oIg_3lKY. The quality is kinda bad, but it proves my point.
Just for a global overview, I used the ScrollView to be able to scroll my entire activity, the LinearLayout for the Activity's layout, and the ListView to, well, make the list...
I would like to just note something for the video
The listview is working once every x touches not because you placed it inside a linearlayout but because you are touching the the divider...
the scrollview will then consider that the place you touched does not have children to dispatch the motionevent to... so it calls the super.dispatchTouchEvent which is in this case the View.dispatchTouchView hence the listview.onTouchEvent.
When you touch inside a row the scrollview which is really a viewgroup will send the dispatch to the children in your case the textview and never calls the one of the view so the listview do not scroll.
Hope my explanation was clear enough to point out why is it not working.