Sorry for the confusing title, I cannot express the problem very concisely...
I have an Android app with a ListView that uses a circular / "infinite" adapter, which basically means I can scroll it up or down as much as I want and the items will wrap around when it reaches the top or bottom, making it seem to the user as if he is spinning an infinitely long list of (~100) repeating items.
The point of this setup is to let the user select a random item, simply by spinning / flinging the listview and waiting to see where it stops. I decreased the friction of the Listview so it flings a bit faster and longer and this seems to work really nice. Finally I placed a partially transparent image on top of the ListView to block out the top and bottom items (with a transition from transparent to black), making it seem as if the user is "selecting" the item in the middle, as if they were on a rotating "wheel" that they control by flinging.
There is one obvious problem: after flinging the ListView does not stop at a particular item, but it can stop hovering between two items (where the first visible item is then only partially shown). I want to avoid this because in that case it is not obvious which item has been "randomly selected".
Long story short: after the ListView has finished scrolling after flinging, I want it to stop on a "whole" row, instead of on a partially visible row.
Right now I implemented this behavior by checking when the scrolling has stopped, and then selecting the first visible item, as such:
lv = this.getListView();
lv.setFriction(0.005f);
lv.setOnScrollListener(new OnScrollListener() {
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE)
{
if (isAutoScrolling) return;
isAutoScrolling = true;
int pos = lv.getFirstVisiblePosition();
lv.setSelection(pos);
isAutoScrolling = false;
}
}
});
This works reasonably well, apart from one glaringly obvious problem... The first visible item might only be visible for a pixel or two. In that case, I want the ListView to jump "up" for those two pixels so that the second visible item is selected. Instead, of course, the first visible item is selected which means the ListView jumps "down" almost an entire row (minus those two pixels).
In short, instead of jumping to the first visible item, I want it to jump to the item that is visible the most. If the first visible item is less than half visible, I want it to jump to the second visible item.
Here's an illustration that hopefully conveys my point. The left most ListView (of each pair) shows the state after flinging has stopped (where it comes to a halt), and the right ListView shows how it looks after it made the "jump" by selecting the first visible item. On the left I show the current (wrong) situation: Item B is only barely visible, but it is still the first visible item so the listView jumps to select that item - which is not logical because it has to scroll almost an entire item height to get there. It would be much more logical to scroll to Item C (which is depicted on the right) because that is "closer".
(source: nickthissen.nl)
How can I achieve this behavior? The only way I can think of is to somehow measure how much of the first visible item is visible. If that is more than 50%, then I jump to that position. If it is less than 50%, I jump to that position + 1. However I have no clue how to measure that...
Any idea's?
You can get the visible dimensions of a child using the getChildVisibleRect method. When you have that, and you get the total height of the child, you can scroll to the appropriate child.
In the example below I check whether at least half of the child is visible:
View child = lv.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
lv.getChildVisibleRect (child, r, null);
if (Math.abs (r.height ()) < height / 2.0) {
// show next child
}
else {
// show this child
}
Here's my final code inspired by Shade's answer.
I forgot to add "if(Math.abs(r.height())!=height)" at first. Then it just scrolls twice after it scroll to correct position because it's always greater than height/2 of childView.
Hope it helps.
listView.setOnScrollListener(new AbsListView.OnScrollListener(){
#Override
public void onScrollStateChanged(AbsListView view,int scrollState) {
if (scrollState == SCROLL_STATE_IDLE){
View child = listView.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
listView.getChildVisibleRect (child, r, null);
if(Math.abs(r.height())!=height){//only smooth scroll when not scroll to correct position
if (Math.abs (r.height ()) < height / 2.0) {
listView.smoothScrollToPosition(listView.getLastVisiblePosition());
}
else if(Math.abs (r.height ()) > height / 2.0){
listView.smoothScrollToPosition(listView.getFirstVisiblePosition());
}
else{
listView.smoothScrollToPosition(listView.getFirstVisiblePosition());
}
}
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {
}});
Follow these 3 steps, then you can get exactly what you want!!!!
1.Initialize the two variable for scrolling up and down:
int scrollingUp=0,scrollingDown=0;
2.Then increment the value of the variable based on scrolling:
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if(mLastFirstVisibleItem<firstVisibleItem)
{
scrollingDown=1;
}
if(mLastFirstVisibleItem>firstVisibleItem)
{
scrollingUp=1;
}
mLastFirstVisibleItem=firstVisibleItem;
}
3.Then do the changes in the onScrollStateChanged():
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case SCROLL_STATE_IDLE:
if(scrollingUp==1)
{
mainListView.post(new Runnable() {
public void run() {
View child = mainListView.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
mainListView.getChildVisibleRect (child, r, null);
int dpDistance=Math.abs (r.height());
double minusDistance=dpDistance-height;
if (Math.abs (r.height()) < height/2)
{
mainListView.smoothScrollBy(dpDistance, 1500);
}
else
{
mainListView.smoothScrollBy((int)minusDistance, 1500);
}
scrollingUp=0;
}
});
}
if(scrollingDown==1)
{
mainListView.post(new Runnable() {
public void run() {
View child = mainListView.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
mainListView.getChildVisibleRect (child, r, null);
int dpDistance=Math.abs (r.height());
double minusDistance=dpDistance-height;
if (Math.abs (r.height()) < height/2)
{
mainListView.smoothScrollBy(dpDistance, 1500);
}
else
{
mainListView.smoothScrollBy((int)minusDistance, 1500);
}
scrollingDown=0;
}
});
}
break;
case SCROLL_STATE_TOUCH_SCROLL:
break;
}
}
You probably solved this problem but I think that this solution should work
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
View firstChild = lv.getChildAt(0);
int pos = lv.getFirstVisiblePosition();
//if first visible item is higher than the half of its height
if (-firstChild.getTop() > firstChild.getHeight()/2) {
pos++;
}
lv.setSelection(pos);
}
getTop() for first item view always return nonpositive value so I don't use Math.abs(firstChild.getTop()) but just -firstChild.getTop(). Even if this value will be >0 then this condition is still working.
If you want to make this smoother then you can try to use lv.smoothScrollToPosition(pos) and enclose all above piece of code in
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
post(new Runnable() {
#Override
public void run() {
//put above code here
//and change lv.setSelection(pos) to lv.smoothScrollToPosition(pos)
}
});
}
Once you know the first visible position, you should be able to use View.getLocationinWindow() or View.getLocationOnScreen() on the next position's view to get the visible height of the first. Compare that to the View's height, and scroll to the next position if appropriate.
You may need to tweak it to account for padding, depending on what your rows look like.
I haven't tried the above, but it seems like it should work. If it doesn't, here's another, probably less robust idea:
getLastVisiblePosition(). If you take the difference between last and first, you can see how many positions are visible on the screen. Compare that to how many positions were visible when the list was first populated(scroll position 0).
If the same number of positions are visible, simply scroll to the first visible position as you are doing. If there is one more visible, scroll to "first + 1" position.
If you can get the position of the row that needs to be scrolled to, you can use the method:
smoothScrollToPosition
So something like:
int pos = lv.getFirstVisiblePosition();
lv.smoothScrollToPosition(pos);
Edit
Try this, sorry I don't have time to test, I'm out and about.
ImageView iv = //Code to find the image view
Rect rect = new Rect(iv.getLeft(), iv.getTop(), iv.getRight(), iv.getBottom());
lv.requestChildRectangleOnScreen(lv, rect, false);
My Solution:
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
if (swipeLayout.isRefreshing()) {
swipeLayout.setRefreshing(false);
} else {
int pos = firstVisibleItem;
if (pos == 0 && lv_post_list.getAdapter().getCount()>0) {
int topOfNext = lv_post_list.getChildAt(pos + 1).getTop();
int heightOfFirst = lv_post_list.getChildAt(pos).getHeight();
if (topOfNext > heightOfFirst) {
swipeLayout.setEnabled(true);
} else {
swipeLayout.setEnabled(false);
}
}
else
swipeLayout.setEnabled(false);
}
}
Related
Actually I don't know how it properly called - overlay, parallax or slideUP, whatever, I have an Activity called "cafe details" which presents a Container(LinearLayout) with header information (name, min price, delivery time min e.t.c) and other container (ViewPager) which contains a ExpandableListView with something information (menus&dishes) and all I want to do is slide up my Viewpager when scrolls listview to scpecific Y position to cover(or overlay) header information.
A similar effect (but with parallax that I don't need to use) looks like this
I can detect when user scrolling listview down or up but how I can move container with ViewPager to overlay other container? Please give me ideas, regards.
UPD
I have tried a huge number of ways how to implement it and all of them unfortunately are not suitable. So now I have come to next variant - add scroll listener to ListView, calculate scrollY position of view and then based on that move the viewpager on y axis by calling setTranslationY();
Here is some code
1) ViewPager's fragment
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
#Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
if (getActivity() != null) {
((MainActivity) getActivity()).resizePagerContainer(absListView);
}
}
});
2) MainActivity
//object fields
int previousPos;
private float minTranslation;
private float minHeight;
<--------somewhere in onCreate
minTranslation = - (llVendorDescHeaders.getMeasuredHeight()+llVendorDescNote.getMeasuredHeight());
//llVendorDescHeaders is linearLayout with headers that should be hidden
//lVendorDescNote is a textView on green background;
minHeight = llVendorDescriptionPagerContainer.getMeasuredHeight();
//llVendorDescriptionPagerContainer is a container which contains ViewPager
--------->
public void resizePagerContainer(AbsListView absListView){
final int scrollY = getScrollY(absListView);
if (scrollY != previousPos) {
final float translationY = Math.max(-scrollY, minTranslation);
llVendorDescriptionPagerContainer.setTranslationY(translationY);
previousPos = scrollY;
}
}
private int getScrollY(AbsListView view) {
View child = view.getChildAt(0);
if (child == null) {
return 0;
}
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = child.getTop();
return -top + firstVisiblePosition * child.getHeight() ;
}
This simple solution unfortunately has a problem - it is blinking and twitching (I don't know how to call it right) when scrolls slowly. So instead setTranslationY() I've used an objectAnimator:
public void resizePagerContainer(AbsListView absListView){
.............
ObjectAnimator moveAnim = ObjectAnimator.ofFloat(llVendorDescriptionPagerContainer, "translationY", translationY);
moveAnim.start();
..............
}
I don't like this solution because 1) anyway it does resize viewpager with delay, not instantly 2) I don't think that is good idea to create many ObjectAnimator's objects every time when I scroll my listView.
Need your help and fresh ideas. Regards.
I'm assuming that you are scrolling the top header (the ImageView is a child of the header) based on the scrollY of the ListView/ScrollView, as shown below:
float translationY = Math.max(-scrollY, mMinHeaderTranslation);
mHeader.setTranslationY(translationY);
mTopImage.setTranslationY(-translationY / 3); // For parallax effect
If you want to stick the header/image to a certain dimension and continue the scrolling without moving it anymore, then you can change the value of mMinHeaderTranslation to achieve that effect.
//change this value to increase the dimension of header stuck on the top
int tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height);
mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
mMinHeaderTranslation = -mHeaderHeight + tabHeight;
The code snippets above are in reference to my demo but I think it's still general enough for you.
If you're interested you can check out my demo
https://github.com/boxme/ParallaxHeaderViewPager
Have you tried CoordinatorLayout from this new android's design support library? It looks like it's what you need. Check this video from 3:40 or this blog post.
I want to find out the position or ids related to a ListView's items: only those ones which are completely visible on the screen.
Using listview.getFirstVisibleposition and listview.getLastVisibleposition takes partial list items into account.
I followed a little bit similar approach as suggested by Rich, to suit my requirement which was to fetch completely visible items on screen when List View is scrolled every time.
This is what i did
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//Loop to get tids of all completely visible List View's item scrolled on screen
for (int listItemIndex = 0; listItemIndex <= getListView().getLastVisiblePosition() - getListView().getFirstVisiblePosition(); listItemIndex++) {
View listItem = getListView().getChildAt(listItemIndex);
TextView tvNewPostLabel = (TextView) listItem.findViewById(R.id.tvNewPostLabel);
if (tvNewPostLabel != null && tvNewPostLabel.getVisibility() == View.VISIBLE) {
int listTid = (int) tvNewPostLabel.getTag();
if (listItem.getBottom() < getListView().getHeight()) {//If List View's item is not partially visible
listItemTids.add(listTid);
}
}
}
}
I have not tried this, but here are the pieces of the framework that I believe will get you to what you're looking for (at least this is what I'd try first)
As you've stated, you should get the last visible position from the list view using ListView.getLastVisiblePosition()
You can then access the View representing this position using ListView.getChildAt(position)
You now have a reference to the view, which you can call a combination of View.getLocationOnScreen(location) and View.getHeight()
Also call View.getLocationOnScreen(location) and View.getHeight() on the ListView. y + height of the View should be less than or equal to y + height of the ListView if it is fully visible.
This is my first ever post here and I'm a dumb novice, so I hope someone out there can both help me and excuse my ignorance.
I have a ListView which is populated with an ArrayAdapter. When I either scroll or click, I want the selected item, or the item nearest the vertical center, to be forced to the exact vertical center of the screen. If I call listView.setSelection(int position) it aligns the selected position at the top of the screen, so I need to use listView.setSelectionFromTop(position, offset) instead. To find my offset, I take half of the View's height from the half of the ListView's height.
So, I can vertically center my item easy enough, within OnItemClick or OnScrollStateChanged, with the following:
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
All this works fine. My problem is with the initial ListView setup. I want an item to be centered when the activity starts, but I can't because I get a NullPointerException from:
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from OnCreate() or OnResume().
So my question is simply: how can I force my ListView to render at startup so I can get the height value I need? Or, alternatively, is there any other way to center items vertically within a ListView?
Thanks in advance for any help!
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from onCreate() or onResume().
You should call it in onScroll.
listView.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
//Write your logic here
int y = listView.getChildAt(0).getHeight();
}
});
I'm answering my own question here, but it's very much a hack. I think it's interesting because it sheds some light on the behavior of listviews.
The problem was in trying to act on data (a listview row) that did not yet exist (it had not been rendered). listview.getChildAt(int) was null because the listview had no children yet. I found out onScroll() is called immediately when the activity is created, so I simply put everything in a thread and delayed the getChildAt() call. I then enclosed the whole thing in a boolean wrapper to make sure it is only ever called once (on startup).
The interesting thing was that I only had to delay the call by 1ms for everything to be OK. And that's too fast for the eye to see.
Like I said, this is all a hack so I'm sure all this is a bad idea. Thanks for any help!
private boolean listViewReady = false;
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!listViewReady){
Thread timer = new Thread() {
public void run() {
try{
sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}finally{
runOnUiThread(new Runnable() {
public void run() {
myPosition = 2;
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
listViewReady = true;
}
});
}
}
};
timer.start();
}//if !ListViewReady
I have achieved the same using a in my opinion slighlty simpler solution
mListView.post(new Runnable() {
#Override
public void run() {
int height = mListView.getHeight();
int itemHeight = mListView.getChildAt(0).getHeight();
if (positionOfMyItem == myCollection.size() - 1) {
// last element - > don't subtract item height
itemHeight = 0;
}
mListView.setSelectionFromTop(position, height / 2 - itemHeight / 2);
}
});
Scope
I need to scroll to certain position smoothly and then "jump" to another position with setSelection(anotherPosition). This is done to create an illusion of smooth scrolling of (e.g.) 100 items in ListView. smoothScrollToPosition(100) lasts too much, you know.
Problem
setSelection() doesn't wait till smoothScrollToPosition finishes its work, so setSelection() is being called immediately and user sees quick jumping only;
Code
private final int scrollableItems = 20;
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.smoothScrollToPosition(firstVisiblePosition - scrollableItems);
mListView.setSelection(0);
}
mListView.clearFocus();
Idea
OK, we could change logic of smoothness illusion: first setSelection(), then scroll smoothly (we're scrolling to the very first item on top of the list):
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.setSelection(scrollableItems);
mListView.smoothScrollToPosition(0);
}
mListView.clearFocus();
final ListView listView = ...;
View listItemView = ...;
listView.smoothScrollBy(listItemView.getHeight() * NUMBER_OF_VIEWS,
DURATION * 2);
listView.postDelayed(new Runnable() {
public void run() {
listView.smoothScrollBy(0, 0); // Stops the listview from overshooting.
listView.setSelection(0);
}
}, DURATION);
Of course, direction of the scroll etc. would need to be adjusted for your use case (go to the top of the list)
EDIT: Old solution could overshoot if the velocity of the scroll was too high, smoothScrollBy(0,0) will stop the smooth scrolling before setting the selection properly and immediately.
Another way is to add an OnScrollListener.
private final int scrollableItems = 20;
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
if (i == SCROLL_STATE_IDLE) {
mListView.setSelection(0);
}
}
})
mListView.smoothScrollToPosition(firstVisiblePosition - scrollableItems);
}
mListView.clearFocus();
I'm creating a list of pictures using a ListView and the photos are of a size that would fit 2 to 3 photos on the screen.
The problem that I'm having is that I would like to when the user stops scrolling that the first item of the visible list would snap to the top of screen, for example, if the scroll ends and small part of the first picture displayed, we scroll the list down so the picture is always fully displayed, if mostly of the picture is displayed, we scroll the list up so the next picture is fully visible.
Is there a way to achieve this in android with the listview?
I've found a way to do this just listening to scroll and change the position when the scroll ended by implementing ListView.OnScrollListener
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
if (scrolling){
// get first visible item
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop()); // top is a negative value
int bottom = Math.abs(itemView.getBottom());
if (top >= bottom){
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition()+1, 0);
} else {
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition(), 0);
}
}
scrolling = false;
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
case OnScrollListener.SCROLL_STATE_FLING:
Log.i("TEST", "SCROLLING");
scrolling = true;
break;
}
}
The change is not so smooth but it works.
Utilizing a couple ideas from #nininho's solution, I got my listview to snap to the item with a smooth scroll instead of abruptly going to it. One caveat is that I've only tested this solution on a Moto X in a basic ListView with text, but it works very well on the device. Nevertheless, I'm confident about this solution, and encourage you to provide feedback.
listview.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
if (scrollState == SCROLL_STATE_IDLE) {
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop());
int bottom = Math.abs(itemView.getBottom());
int scrollBy = top >= bottom ? bottom : -top;
if (scrollBy == 0) {
return;
}
smoothScrollDeferred(scrollBy, (ListView)view);
}
}
private void smoothScrollDeferred(final int scrollByF,
final ListView viewF) {
final Handler h = new Handler();
h.post(new Runnable() {
#Override
public void run() {
// TODO Auto-generated method stub
viewF.smoothScrollBy(scrollByF, 200);
}
});
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
});
The reason I defer the smooth scrolling is because in my testing, directly calling the smoothScrollBy method in the state changed callback had problems actually scrolling. Also, I don't foresee a fully-tested, robust solution holding very much state, and in my solution below, I hold no state at all. This solution is not yet in the Google Play Store, but should serve as a good starting point.
Using #nininho 's solution,
In the onScrollStateChanged when the state changes to SCROLL_STATE_IDLE, remember the position to snap and raise a flag:
snapTo = view.getFirstVisiblePosition();
shouldSnap = true;
Then, override the computeScroll() method:
#Override
public void computeScroll() {
super.computeScroll();
if(shouldSnap){
this.smoothScrollToPositionFromTop(snapTo, 0);
shouldSnap = false;
}
}
You can do a much more smooth scrolling if you use RecyclerView. The OnScrollListener is way better.
I have made an example here: https://github.com/plattysoft/SnappingList
Well.. I know 10 years have past since this question was asked, but now we can use LinearSnapHelper:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
Source:
https://proandroiddev.com/android-recyclerview-snaphelper-19eaa9598da6
Apart from trying the code above one thing you should make sure of is that your listView have a height that can fit exact number of items you want to be displayed.
e.g
If you want 4 items to be displayed after snap effect and your row height (defined in its layout) should be 1/4 of the total height of the list.
Note that after the smoothScrollBy() call, getFirstVisiblePosition() may point to the list item ABOVE the topmost one in the listview. This is especially true when view.getChildAt(0).getBottom() == 0. I had to call view.setSelection(view.getFirstVisiblePosition() + 1) to remedy this odd behavior.