I have some code here (which I did not write) which I need to fix. Here's the required flow:
User clicks on an item in a ListView
The item expands to show a footer which is otherwise hidden
If another list item is expanded, it is shrunk back to normal size (so that only 1 item is expanded at a time).
My problem: When tapping an item which is not expanded, nothing happens. The 2nd time, the item expands, tapping again shrinks it, then once again the 1st tap does nothing and so on.
Of course, I'm trying to eliminate the 1st redundant tap which does nothing.
Another interesting side-effect: When I tap an item the 1st time, nothing happens, then I will tap a DIFFERENT item once, and both the items will expand together.
I've been over the code for quite a while now and I can't see what's causing this.
Here's the code:
Setting the listener on the ListView:
productsListView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> list, View view,int position, long id)
{
if (lastSelectedPosition == -1) {
lastSelectedPosition = position;
} else if (lastSelectedPosition == position) {
lastSelectedPosition = -1;
} else {
lastSelectedPosition = position;
}
View child;
ProductItemView tag;
for (int i = 0; i < productsListView.getChildCount(); i++) {
child = productsListView.getChildAt(i);
tag = (ProductItemView) child.getTag();
tag.onSomeListItemClicked(position);
productsListView.smoothScrollToPosition(position);
}
}
});
The list view's adapter:
public class ProductsCursorAdapter extends CursorAdapter {
public ProductsCursorAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
}
#Override
public void bindView(View view, Context context, Cursor cursor) {
ProductItemView item = null;
int pos = cursor.getPosition();
Log.d("BookListFragment", "BookListFragment: Position is: " + pos);
item = new ProductItemView(getActivity(), cursor.getPosition(), view, new ProductDAO(cursor));
view.setTag(item);
item.setContainer(BookListFragment.this, BookListFragment.this);
if (lastSelectedPosition == cursor.getPosition()) {
item.openedFooter();
}
}
#Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
View view = (View) getActivity().getLayoutInflater().inflate(R.layout.course_list_item, null);
return view;
}
}
Relevant code inside ProductItemView:
public void onSomeListItemClicked(int position)
{
if (m_position == position)
{
Log.i("ProductItemView", "Animate footer for position: " + m_position);
animateFooter(position);
}
else
{
Log.i("ProductItemView", "Hide footer for position: " + m_position);
hideFooter(position);
}
}
public void showFooter(int position) {
if (!isFooterVisible())
{
animateFooter(position);
}
}
public void hideFooter(int position)
{
Log.i("ProductItemView", "Hide called for position: " + m_position);
if (isFooterVisible() && position != m_position)
{
animateFooter(position);
}
}
public void animateFooter(final int position)
{
if (footer != null && (m_footerExpandAnim == null || m_footerExpandAnim.hasEnded()))
{
Log.i("ProductItemView", "Animating footer for position: " + m_position);
isFooterVisible=!isFooterVisible;
m_footerExpandAnim = new ExpandAnimation(footer, 200, animationDelegate, position);
footer.startAnimation(m_footerExpandAnim);
}
}
ExpandAnimation:
public ExpandAnimation(View view, int duration, AnimationDelegate delegate, int position) {
this.position = position;
this.delegate = delegate;
setDuration(duration);
mAnimatedView = view;
mViewLayoutParams = (LayoutParams) view.getLayoutParams();
// decide to show or hide the view
mIsVisibleAfter = (view.getVisibility() == View.VISIBLE);
mMarginStart = mViewLayoutParams.bottomMargin;
mMarginEnd = (mMarginStart == 0 ? (-view.getHeight()) : 0);
mAnimatedView.clearAnimation();
Log.i("ExpandAnimation", "Margin Start = " + mMarginStart + ", Margin End = " + mMarginEnd);
//view.setVisibility(View.VISIBLE);
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
Log.i("ExpandAnimation", "InterpolatedTime: " + interpolatedTime);
if (interpolatedTime < 1.0f) {
// Calculating the new bottom margin, and setting it
mViewLayoutParams.bottomMargin = mMarginStart
+ (int) ((float)(mMarginEnd - mMarginStart) * interpolatedTime);
mAnimatedView.setLayoutParams(mViewLayoutParams);
// Invalidating the layout, making us seeing the changes we made
mAnimatedView.requestLayout();
mAnimatedView.postInvalidate();
// Making sure we didn't run the ending before (it happens!)
} else if (!mWasEndedAlready) {
mViewLayoutParams.bottomMargin = mMarginEnd;
mAnimatedView.setLayoutParams(mViewLayoutParams);
mAnimatedView.requestLayout();
mAnimatedView.postInvalidate();
if (mIsVisibleAfter) {
//mAnimatedView.setVisibility(View.GONE);
}
mWasEndedAlready = true;
}
if(delegate!=null){
delegate.animationDidEnd(position);
}
}
Some things I've noticed:
The 1st time the item is clicked, the ExpandAnimation's constructor is indeed called, but the logs from the applyTransformation method aren't printed.
The 2nd time the item is clicked, the ExpandAnimation's constructor is called, but the mMarginStart value is not what it should be (randomly between -60 to -80 instead of -100), but then the logs in the applyTransformation are printed properly.
If you need any more code, let me know. Any ideas would help.
As I mentioned, this is not my code - I'm trying to edit code which a developer who has since left wrote. If it were up to me, this entire thing would'v been written very differently. I require a solution which involves minimal changes to the code structure.
Okay, I found the problem.
The clue was that I noticed that after the 1st click which "did nothing", if I scrolled the list slightly, the item I clicked would suddenly expand. This told me that the ListView was, for some reason, preventing its child views from performing UI operations.
I added a postInvalidate call on the list on the OnItemClick listener, and everything works as expected.
Interesting.
Related
I am a New Android Application Developer. I would like to know how to create endless horizontal scroll view. For example, there are three buttons (Button1, Button2 and Button3). When user scroll the view, I still want to display Button1 again after Button3. Could you please provide any sample code or any idea?
Thanks.
You could check if your button view is still visible. First check if button one is visible:
private boolean isViewVisible(View view) {
Rect scrollBounds = new Rect();
mScrollView.getDrawingRect(scrollBounds);
float top = view.getY();
float bottom = top + view.getHeight();
if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
return true;
} else {
return false;
}
}
If it is not visible, then add button one again. Call this method in a scroll listener every time user scrolls, to check if the button is not visible. If the button is not visible, then add it again.
If you want to make it endless itself, try this:
public class Test extends ListActivity implements OnScrollListener {
Aleph0 adapter = new Aleph0();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setListAdapter(adapter);
getListView().setOnScrollListener(this);
}
public void onScroll(AbsListView view,
int firstVisible, int visibleCount, int totalCount) {
boolean loadMore = /* maybe add a padding */
firstVisible + visibleCount >= totalCount;
if(loadMore) {
adapter.count += visibleCount; // or any other amount
adapter.notifyDataSetChanged();
}
}
public void onScrollStateChanged(AbsListView v, int s) { }
class Aleph0 extends BaseAdapter {
int count = 40; /* starting amount */
public int getCount() { return count; }
public Object getItem(int pos) { return pos; }
public long getItemId(int pos) { return pos; }
public View getView(int pos, View v, ViewGroup p) {
TextView view = new TextView(Test.this);
view.setText("entry " + pos);
return view;
}
}
}
And take a look at this:
Android Endless List
android:how to make infinite scrollview with endless scrolling
Thats it. Just see when the view is out of bounds when the user is scrolling.
And when it is out of view, just re add it.
update
Basically, bbrakenhoff has answered my question but there is just one more thing left to fix. How can I update the contents of my EndlessFeedAdapter (mEndlsFidAdptr)? I need to clear the item and then reload. I'm using the CWAC EndlessAdapater. Is there a trick to clear the contents or would it be easier to just program a method? After this is done the scroll position should be maintaind.
I am getting data from a server and updating my EndlessFeedAdapter when content changes. Each time I am updating my adapter and reloading content. The problem is that after reloading my list jumps right back to the top as my scroll position is not maintained. I have tried setSelection and setSelectionFromTop extensively, but without positive results.
How do I maintain scroll position after the adapter has been updated?
I have been going through the forums searching for an answer but nothing seems to be working.
I have tried all these: Maintain/Save/Restore scroll position when returning to a ListView
This didn't work:
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// restore index and position
mList.setSelectionFromTop(index, top);
Nor this:
// Save ListView state
Parcelable state = listView.onSaveInstanceState();
// Set new items
listView.setAdapter(adapter);
// Restore previous state (including selected item index and scroll position)
listView.onRestoreInstanceState(state);
Nor the other solutions such as setting up a runnable or setting the scrollPositionY. Setting notifyDataChanged didn't work as I am loading from different lists.
My code:
private void showFeed() {
if (mFeedActivity.mInFeed) {
mQuickReturnView.setVisibility(View.VISIBLE);
} else {
mQuickReturnView.setVisibility(View.GONE);
}
Activity actvt= getActivity();
if (actvt == null || mFeedListView == null) return;
actvt.invalidateOptionsMenu();
mFeedListView.setVisibility(View.VISIBLE);
//updated with help from response
if (mAdapter == null){
mAdapter = new FeedAdapter(actvt, 0, mFeed.getItems().getFeedItemList(), this);
} else {
mAdapter.clear();
mAdapter.addAll(mFeed.getItems().getFeedItemList());
mAdapter.notifyDataSetChanged();
}
mEndlsFidAdptr = new EndlessFeedAdapter(actvt, mAdapter, R.layout.progress_row, mFeed.isShowMoreBar(),
mEndlsFidAdptr.setRunInBackground(false);
//Parcelable state = mFeedListView.onSaveInstanceState();
mFeedListView.setAdapter(mEndlsFidAdptr);
//mFeedListView.onRestoreInstanceState(state);
mFeedListView.setSelectionFromTop(mFirstVisibleItem, mVisibleItemOffset);
if(!(mFeedScope.equalsIgnoreCase(FeedScope.BOOKMARKS.xmlValue()) ||
mFeedScope.equalsIgnoreCase(FeedScope.DOCUMENT.xmlValue()) ||
mFeedScope.equalsIgnoreCase(FeedScope.NOTIFICATIONS.xmlValue()) ||
mFeedScope.equalsIgnoreCase(FeedScope.RECEIVED_TASKS.xmlValue()) ||
mFeedScope.equalsIgnoreCase(FeedScope.SEND_TASKS.xmlValue()))) {
mFeedListView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
mFeedListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCanShowHide = scrollState == SCROLL_STATE_FLING;
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
View v = mFeedListView.getChildAt(0);
//View v = null;
if(!mFeedActivity.mInFeed || v == null)
return;
int top = v.getTop();
if(mIsAnimating) {
mVisibleItemOffset = top;
mFirstVisibleItem = firstVisibleItem;
return;
}
boolean hide = false;
boolean show = false;
float stickyHeight = getResources().getDimension(R.dimen.sticky_height);
if(firstVisibleItem == mFirstVisibleItem) {
if((top + stickyHeight) < mVisibleItemOffset) {
// Content scrolled down
// if shown then hide quickactionview
if(mQuickReturnShown) {
hide = true;
}
} else if (top > mVisibleItemOffset) {
// Content scrolled up
// if hidden then show quickactionview
if(!mQuickReturnShown) {
show = true;
}
}
} else if(firstVisibleItem > mFirstVisibleItem) {
// Content scrolled down
// if shown then hide quickactionview
if(mQuickReturnShown) {
hide = true;
}
} else if (firstVisibleItem < mFirstVisibleItem) {
// Content scrolled up
// if hidden then show quickactionview
if(!mQuickReturnShown) {
show = true;
}
}
if((show && mCanShowHide) || (top == 0 && !mQuickReturnShown)) {
mTranslateAnimation = new TranslateAnimation(0, 0, -mQuickReturnHeight, 0);
mTranslateAnimation.setDuration(DURATION_MILLIS);
mTranslateAnimation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
mIsAnimating = true;
}
#Override
public void onAnimationEnd(Animation animation) {
mIsAnimating = false;
mQuickReturnShown = true;
mQuickReturnView.setVisibility(View.VISIBLE);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
mQuickReturnView.startAnimation(mTranslateAnimation);
}
if(hide) {
mTranslateAnimation = new TranslateAnimation(0, 0, 0, -mQuickReturnHeight);
mTranslateAnimation.setDuration(DURATION_MILLIS);
mTranslateAnimation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
mIsAnimating = true;
}
#Override
public void onAnimationEnd(Animation animation) {
mIsAnimating = false;
mQuickReturnShown = false;
mQuickReturnView.setVisibility(View.GONE);
}
#Override
public void onAnimationRepeat(Animation animation) { }
});
mQuickReturnView.startAnimation(mTranslateAnimation);
}
mVisibleItemOffset = top;
mFirstVisibleItem = firstVisibleItem;
}
});
} else {
mFeedListView.setOnScrollListener(null);
}
mFeedListView.setSelectionFromTop(mFirstVisibleItem, mVisibleItemOffset);
//mFeedListView.scrollTo(mCurrentX,mCurrentY);
if(mFeed.getItems().getFeedItemList().size() == 0) {
mEmptyFeedView.setVisibility(View.VISIBLE);
}
}
Are you calling the method showFeed() everytime you received new data? If yes, then maybe you could try to refill the adapter instead of assigning a new one every time.
I don't know exactly what you are doing in your adapter, so I'll show you how I did it in one of my apps.
In my Activity/Fragment I do this when I want to update the list with new items:
private void refreshCalendar(ArrayList<CalendarDay> newCalendar) {
if (mAdapter == null) {
mAdapter = new CalendarAdapter(getActivity(), newCalendar);
mExpandableListView.setAdapter(mAdapter);
}
else {
mAdapter.refill(newCalendar);
}
restoreInstanceState();
}
And in the adapter:
public void refill(ArrayList<CalendarDay> newCalendar) {
mCalendar.clear();
mCalendar.addAll(newCalendar);
notifyDataSetChanged();
}
Maybe you could try and remove this line?
mFeedListView.setSelectionFromTop(mFirstVisibleItem, mVisibleItemOffset);
Edit: You are using this line twice in your code.
Edit: In the code in your question you are only refreshing mAdapter and not mEndlsFidAdptr . That one is still assigned a new one. Everytime you assign a new adapter to you ListView it scrolls back to the top.
So the solution wasn't the logical place where I was looking. Thanks a lot to bbrakenhoff for pointing me in the right direction!
My class showFeed() was called from another class called downloadFeed() my scroll position wasn't maintained because downloadFeed() was only loading 5 items when it refreshed, hence even if I maintained the correct scroll position it was not visible.
Although it may be a long shot if someone else has this problem - to fix simply create a variable to hold the total size of your scrollable list when the user performs an onClick event. Then when downloadFeed() is called again, there are more items to download instead the default 5. Then the scroll position is able to be maintained as the visible items are now present.
I ended up using mFeedListView.setSelectionFromTop(firstVisibleItem, positionOffset)
This question refers to the SwipeListView component found here: https://github.com/47deg/android-swipelistview
After trying out several implementations and fixes I found on the web I decided to modify the sources a little.
I will post this here since i know it's a known issue and all the versions I found proved to have some issues eventually.
SwipeListViewTouchListener.java has suffered the following changes:
...
/**
* Create reveal animation
*
* #param view affected view
* #param swap If will change state. If "false" returns to the original
* position
* #param swapRight If swap is true, this parameter tells if movement is toward
* right or left
* #param position list position
*/
private void generateRevealAnimate(final View view, final boolean swap, final boolean swapRight, final int position) {
int moveTo = 0;
if (opened.get(position)) {
if (!swap) {
moveTo = openedRight.get(position) ? (int) (viewWidth - rightOffset) : (int) (-viewWidth + leftOffset);
}
} else {
if (swap) {
moveTo = swapRight ? (int) (viewWidth - rightOffset) : (int) (-viewWidth + leftOffset);
}
}
final boolean aux = !opened.get(position);
if(swap) {
opened.set(position, aux);
openedRight.set(position, swapRight);
}
animate(view).translationX(moveTo).setDuration(animationTime).setListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
swipeListView.resetScrolling();
if (swap) {
if (aux) {
swipeListView.onOpened(position, swapRight);
} else {
swipeListView.onClosed(position, openedRight.get(position));
}
}
// if (aux || !swap) {
// resetCell();
// }
}
});
}
...
/**
* Close all opened items
*/
void closeOtherOpenedItems() {
if (opened != null && downPosition != SwipeListView.INVALID_POSITION) {
int start = swipeListView.getFirstVisiblePosition();
int end = swipeListView.getLastVisiblePosition();
for (int i = start; i <= end; i++) {
if (opened.get(i) && i != downPosition) {
closeAnimate(swipeListView.getChildAt(i - start).findViewById(swipeFrontView), i);
}
}
}
}
...
/**
* #see View.OnTouchListener#onTouch(android.view.View,
* android.view.MotionEvent)
*/
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
...
closeOtherOpenedItems();
view.onTouchEvent(motionEvent);
return true;
}
The rest of the code not mentioned is the same.
Any comments highly appreciated, this changes prevent you from having to implement the SwipeListViewOnTouchListener in the activity which inflates the list.
Cons: doesn't close the row opened by openAnimate()
BaseSwipeListViewListener swipeListViewListener = new BaseSwipeListViewListener() {
int openItem = -1;
#Override
public void onStartOpen(int position, int action, boolean right) {
super.onStartOpen(position, action, right);
if (openItem > -1)
swipeListView.closeAnimate(openItem);
openItem = position;
}
}
Or better way:
#Override
public void onStartOpen(int position, int action, boolean right) {
super.onStartOpen(position, action, right);
swipeListView.closeOpenedItems();
}
And set the listener to the listView:
swipeListView.setSwipeListViewListener(swipeListViewListener);
Your fix worked, but there is a way to do it without affecting the original code:
swipeListView.setSwipeListViewListener(new BaseSwipeListViewListener() {
int openItem = -1;
int lastOpenedItem = -1;
int lastClosedItem = -1;
#Override
public void onOpened(int position, boolean toRight) {
lastOpenedItem = position;
if (openItem > -1 && lastOpenedItem != lastClosedItem) {
swipeListView.closeAnimate(openItem);
}
openItem = position;
}
#Override
public void onStartClose(int position, boolean right) {
Log.d("swipe", String.format("onStartClose %d", position));
lastClosedItem = position;
}
}
You should however, send a pull request to apply your code as that would fix the bug.
Source: https://github.com/47deg/android-swipelistview/issues/46
If you're going to modify the swipelistview library itself I have a simpler solution.
Add the following if block to SwipeListViewTouchListener.java in the onTouch method right at the beginning of case MotionEvent.ACTION_DOWN:
if(lastOpenedPosition != downPosition && opened.get(lastOpenedPosition)) {
closeAnimate(lastOpenedPosition);
return false;
}
Create an int lastOpenedPosition field and initialize it to 0, and in the generateRevealAnimate method inside the if (aux) block add:
lastOpenedPosition = position;
I would also add config variable (in res/values/swipelistview_attrs.xml) to SwipeListView and add it to the onTouch if block, to add the ability to turn this feature off and on.
This basically results in if the list is touched while a row is open, than the row will close. Which, imho, is better functionality than the row closing only after you finished opening another row.
swipeListView.setSwipeListViewListener(new BaseSwipeListViewListener() {
//...
#Override
public void onClickBackView(int position) {
//DELETE ITEM
adapter.notifyDataSetChanged();
swipeListView.closeOpenedItems();
}
//...
});
Yeah, the SwipeListView of the original codes can open many items at the same time. Your code segment here can open one item at one time? Or when open another item, the opened items will be closed?
I am attempting to animate the ListView items when a scroll takes place. More specifically, I am trying to emulate the scroll animations from the iMessage app on iOS 7. I found a similar example online:
To clarify, I'm trying to achieve the "fluid" movement effect on the items when the user scrolls, not the animation when a new item is added. I've attempted to modify the Views in my BaseAdapter and I've looked into the AbsListView source to see if I could somehow attach an AccelerateInterpolator somewhere that would adjust the draw coordinates sent to the children Views (if that is even how AbsListView is designed). I've been unable to make any progress so far.
Does anybody have any ideas of how to replicate this behaviour?
For the record to help with googling: this is called "UIKit Dynamics" on ios.
How to replicate Messages bouncing bubbles in iOS 7
It is built-in to recent iOS releases. However it's still somewhat hard to use. (2014) This is the post on it everyone copies:widely copied article Surprisingly, UIKit Dynamics is only available on apple's "collection view", not on apple's "table view" so all the iOS debs are having to convert stuff from table view to "collection view"
The library everyone is using as a starting point is BPXLFlowLayout, since that person pretty much cracked copying the feel of the iphone text messages app. In fact, if you were porting it to Android I guess you could use the parameters in there to get the same feel. FYI I noticed in my android fone collection, HTC phones have this effect, on their UI. Hope it helps. Android rocks!
This implementation works quite good. There is some flickering though, probably because of altered indices when the adapter add new views to top or bottom..That could be possibly solved by watching for changes in the tree and shifting the indices on the fly..
public class ElasticListView extends GridView implements AbsListView.OnScrollListener, View.OnTouchListener {
private static int SCROLLING_UP = 1;
private static int SCROLLING_DOWN = 2;
private int mScrollState;
private int mScrollDirection;
private int mTouchedIndex;
private View mTouchedView;
private int mScrollOffset;
private int mStartScrollOffset;
private boolean mAnimate;
private HashMap<View, ViewPropertyAnimator> animatedItems;
public ElasticListView(Context context) {
super(context);
init();
}
public ElasticListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ElasticListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mScrollState = SCROLL_STATE_IDLE;
mScrollDirection = 0;
mStartScrollOffset = -1;
mTouchedIndex = Integer.MAX_VALUE;
mAnimate = true;
animatedItems = new HashMap<>();
this.setOnTouchListener(this);
this.setOnScrollListener(this);
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (mScrollState != scrollState) {
mScrollState = scrollState;
mAnimate = true;
}
if (scrollState == SCROLL_STATE_IDLE) {
mStartScrollOffset = Integer.MAX_VALUE;
mAnimate = true;
startAnimations();
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mScrollState == SCROLL_STATE_TOUCH_SCROLL) {
if (mStartScrollOffset == Integer.MAX_VALUE) {
mTouchedView = getChildAt(mTouchedIndex - getPositionForView(getChildAt(0)));
if (mTouchedView == null) return;
mStartScrollOffset = mTouchedView.getTop();
} else if (mTouchedView == null) return;
mScrollOffset = mTouchedView.getTop() - mStartScrollOffset;
int tmpScrollDirection;
if (mScrollOffset > 0) {
tmpScrollDirection = SCROLLING_UP;
} else {
tmpScrollDirection = SCROLLING_DOWN;
}
if (mScrollDirection != tmpScrollDirection) {
startAnimations();
mScrollDirection = tmpScrollDirection;
}
if (Math.abs(mScrollOffset) > 200) {
mAnimate = false;
startAnimations();
}
Log.d("test", "direction:" + (mScrollDirection == SCROLLING_UP ? "up" : "down") + ", scrollOffset:" + mScrollOffset + ", toucheId:" + mTouchedIndex + ", fvisible:" + firstVisibleItem + ", " +
"visibleItemCount:" + visibleItemCount + ", " +
"totalCount:" + totalItemCount);
int indexOfLastAnimatedItem = mScrollDirection == SCROLLING_DOWN ?
getPositionForView(getChildAt(0)) + getChildCount() :
getPositionForView(getChildAt(0));
//check for bounds
if (indexOfLastAnimatedItem >= getChildCount()) {
indexOfLastAnimatedItem = getChildCount() - 1;
} else if (indexOfLastAnimatedItem < 0) {
indexOfLastAnimatedItem = 0;
}
if (mScrollDirection == SCROLLING_DOWN) {
setAnimationForScrollingDown(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
} else {
setAnimationForScrollingUp(mTouchedIndex - getPositionForView(getChildAt(0)), indexOfLastAnimatedItem, firstVisibleItem);
}
if (Math.abs(mScrollOffset) > 200) {
mAnimate = false;
startAnimations();
mTouchedView = null;
mScrollDirection = 0;
mStartScrollOffset = -1;
mTouchedIndex = Integer.MAX_VALUE;
mAnimate = true;
}
}
}
private void startAnimations() {
for (ViewPropertyAnimator animator : animatedItems.values()) {
animator.start();
}
animatedItems.clear();
}
private void setAnimationForScrollingDown(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
for (int i = indexOfTouchedChild + 1; i <= indexOflastAnimatedChild; i++) {
View v = getChildAt(i);
v.setTranslationY((-1f * mScrollOffset));
if (!animatedItems.containsKey(v)) {
animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * i));
}
}
}
private void setAnimationForScrollingUp(int indexOfTouchedChild, int indexOflastAnimatedChild, int firstVisibleIndex) {
for (int i = indexOfTouchedChild - 1; i > 0; i--) {
View v = getChildAt(i);
v.setTranslationY((-1 * mScrollOffset));
if (!animatedItems.containsKey(v)) {
animatedItems.put(v, v.animate().translationY(0).setDuration(300).setStartDelay(50 * (indexOfTouchedChild - i)));
}
}
}
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
Rect rect = new Rect();
int childCount = getChildCount();
int[] listViewCoords = new int[2];
getLocationOnScreen(listViewCoords);
int x = (int)event.getRawX() - listViewCoords[0];
int y = (int)event.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
mTouchedIndex = getPositionForView(child);
break;
}
}
return false;
}
return false;
}
}
I've taken just a few minutes to explore this and it looks like it can be done pretty easily with API 12 and above (hopefully I'm not missing something ...). To get the very basic card effect, all it takes is a couple lines of code at the end of getView() in your Adapter right before you return it to the list. Here's the entire Adapter:
public class MyAdapter extends ArrayAdapter<String>{
private int mLastPosition;
public MyAdapter(Context context, ArrayList<String> objects) {
super(context, 0, objects);
}
private class ViewHolder{
public TextView mTextView;
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(getContext()).inflate(R.layout.grid_item, parent, false);
holder.mTextView = (TextView) convertView.findViewById(R.id.checkbox);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.mTextView.setText(getItem(position));
// This tells the view where to start based on the direction of the scroll.
// If the last position to be loaded is <= the current position, we want
// the views to start below their ending point (500f further down).
// Otherwise, we start above the ending point.
float initialTranslation = (mLastPosition <= position ? 500f : -500f);
convertView.setTranslationY(initialTranslation);
convertView.animate()
.setInterpolator(new DecelerateInterpolator(1.0f))
.translationY(0f)
.setDuration(300l)
.setListener(null);
// Keep track of the last position we loaded
mLastPosition = position;
return convertView;
}
}
Note that I'm keeping track of the last position to be loaded (mLastPosition) in order to determine whether to animate the views up from the bottom (if scrolling down) or down from the top (if we're scrolling up).
The wonderful thing is, you can do so much more by just modifying the initial convertView properties (e.g. convertView.setScaleX(float scale)) and the convertView.animate() chain (e.g. .scaleX(float)).
Try this by putting this in your getView() method Just before returning your convertView:
Animation animationY = new TranslateAnimation(0, 0, holder.llParent.getHeight()/4, 0);
animationY.setDuration(1000);
Yourconvertview.startAnimation(animationY);
animationY = null;
Where llParent = RootLayout which consists your Custom Row Item.
It's honestly going to be a lot of work and quite mathematically intense, but I would have thought you could make the list item's layouts have padding top and bottom and that you could adjust that padding for each item so that the individual items become more or less spaced out. How you would track by how much and how you would know the speed at which the items are being scrolled, well that would be the hard part.
Since we do want items to pop every time they appear at the top or bottom of our list, the best place to do it is the getView() method of the adapter:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
animatePostHc(position, v);
} else {
animatePreHc(position, v);
}
From what I understand what you are looking for is a parallax effect.
This answer is really complete and I think that can help you a lot.
Use this library: http://nhaarman.github.io/ListViewAnimations
It is very awesome. Better than the iOS in atleast it is open source :)
I have a listView. When I scroll and stops in a particular place.
How can I get the amount of pixels I scrolled(from top)?
I have tried using get listView.getScrollY(), but it returns 0.
I had the same problem.
I cannot use View.getScrollY() because it always returns 0 and I cannot use OnScrollListener.onScroll(...) because it works with positions not with pixels. I cannot subclass ListView and override onScrollChanged(...) because its parameter values are always 0. Meh.
All I want to know is the amount the children (i.e. content of listview) got scrolled up or down. So I came up with a solution. I track one of the children (or you can say one of the "rows") and follow its vertical position change.
Here is the code:
public class ObservableListView extends ListView {
public static interface ListViewObserver {
public void onScroll(float deltaY);
}
private ListViewObserver mObserver;
private View mTrackedChild;
private int mTrackedChildPrevPosition;
private int mTrackedChildPrevTop;
public ObservableListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mTrackedChild == null) {
if (getChildCount() > 0) {
mTrackedChild = getChildInTheMiddle();
mTrackedChildPrevTop = mTrackedChild.getTop();
mTrackedChildPrevPosition = getPositionForView(mTrackedChild);
}
} else {
boolean childIsSafeToTrack = mTrackedChild.getParent() == this && getPositionForView(mTrackedChild) == mTrackedChildPrevPosition;
if (childIsSafeToTrack) {
int top = mTrackedChild.getTop();
if (mObserver != null) {
float deltaY = top - mTrackedChildPrevTop;
mObserver.onScroll(deltaY);
}
mTrackedChildPrevTop = top;
} else {
mTrackedChild = null;
}
}
}
private View getChildInTheMiddle() {
return getChildAt(getChildCount() / 2);
}
public void setObserver(ListViewObserver observer) {
mObserver = observer;
}
}
Couple of notes:
we override onScrollChanged(...) because it gets called when the listview is scrolled (just its parameters are useless)
then we choose a child (row) from the middle (doesn't have to be precisely the child in the middle)
every time scrolling happens we calculate vertical movement based on previous position (getTop()) of tracked child
we stop tracking a child when it is not safe to be tracked (e.g. in cases where it might got reused)
You cant get pixels from top of list (because then you need to layout all views from top of list - there can be a lot of items). But you can get pixels of first visible item: int pixels = listView.getChildAt(0).getTop(); it generally will be zero or negative number - shows difference between top of listView and top of first view in list
edit:
I've improved in this class to avoid some moments that the track was losing due to views being too big and not properly getting a getTop()
This new solution uses 4 tracking points:
first child, bottom
middle child, top
middle child, bottom
last child, top
that makes sure we always have a isSafeToTrack equals to true
import android.view.View;
import android.widget.AbsListView;
/**
* Created by budius on 16.05.14.
* This improves on Zsolt Safrany answer on stack-overflow (see link)
* by making it a detector that can be attached to any AbsListView.
* http://stackoverflow.com/questions/8471075/android-listview-find-the-amount-of-pixels-scrolled
*/
public class PixelScrollDetector implements AbsListView.OnScrollListener {
private final PixelScrollListener listener;
private TrackElement[] trackElements = {
new TrackElement(0), // top view, bottom Y
new TrackElement(1), // mid view, bottom Y
new TrackElement(2), // mid view, top Y
new TrackElement(3)};// bottom view, top Y
public PixelScrollDetector(PixelScrollListener listener) {
this.listener = listener;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// init the values every time the list is moving
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL ||
scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
for (TrackElement t : trackElements)
t.syncState(view);
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
boolean wasTracked = false;
for (TrackElement t : trackElements) {
if (!wasTracked) {
if (t.isSafeToTrack(view)) {
wasTracked = true;
if (listener != null)
listener.onScroll(view, t.getDeltaY());
t.syncState(view);
} else {
t.reset();
}
} else {
t.syncState(view);
}
}
}
public static interface PixelScrollListener {
public void onScroll(AbsListView view, float deltaY);
}
private static class TrackElement {
private final int position;
private TrackElement(int position) {
this.position = position;
}
void syncState(AbsListView view) {
if (view.getChildCount() > 0) {
trackedChild = getChild(view);
trackedChildPrevTop = getY();
trackedChildPrevPosition = view.getPositionForView(trackedChild);
}
}
void reset() {
trackedChild = null;
}
boolean isSafeToTrack(AbsListView view) {
return (trackedChild != null) &&
(trackedChild.getParent() == view) && (view.getPositionForView(trackedChild) == trackedChildPrevPosition);
}
int getDeltaY() {
return getY() - trackedChildPrevTop;
}
private View getChild(AbsListView view) {
switch (position) {
case 0:
return view.getChildAt(0);
case 1:
case 2:
return view.getChildAt(view.getChildCount() / 2);
case 3:
return view.getChildAt(view.getChildCount() - 1);
default:
return null;
}
}
private int getY() {
if (position <= 1) {
return trackedChild.getBottom();
} else {
return trackedChild.getTop();
}
}
View trackedChild;
int trackedChildPrevPosition;
int trackedChildPrevTop;
}
}
original answer:
First I want to thank #zsolt-safrany for his answer, that was great stuff, total kudos for him.
But then I want to present my improvement on his answer (still is pretty much his answer, just a few improvements)
Improvements:
It's a separate "gesture detector" type of class that can be added to any class that extends AbsListView by calling .setOnScrollListener(), so it's a more flexible approach.
It's using the change in scroll state to pre-allocate the tracked child, so it doesn't "waste" one onScroll pass to allocate its position.
It re-calculate the tracked child on every onScroll pass to avoiding missing random onScroll pass to recalculate child. (this could be make more efficient by caching some heights and only re-calculate after certain amount of scroll).
hope it helps
import android.view.View;
import android.widget.AbsListView;
/**
* Created by budius on 16.05.14.
* This improves on Zsolt Safrany answer on stack-overflow (see link)
* by making it a detector that can be attached to any AbsListView.
* http://stackoverflow.com/questions/8471075/android-listview-find-the-amount-of-pixels-scrolled
*/
public class PixelScrollDetector implements AbsListView.OnScrollListener {
private final PixelScrollListener listener;
private View mTrackedChild;
private int mTrackedChildPrevPosition;
private int mTrackedChildPrevTop;
public PixelScrollDetector(PixelScrollListener listener) {
this.listener = listener;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// init the values every time the list is moving
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL ||
scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
if (mTrackedChild == null) {
syncState(view);
}
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mTrackedChild == null) {
// case we don't have any reference yet, try again here
syncState(view);
} else {
boolean childIsSafeToTrack = (mTrackedChild.getParent() == view) && (view.getPositionForView(mTrackedChild) == mTrackedChildPrevPosition);
if (childIsSafeToTrack) {
int top = mTrackedChild.getTop();
if (listener != null) {
float deltaY = top - mTrackedChildPrevTop;
listener.onScroll(view, deltaY);
}
// re-syncing the state make the tracked child change as the list scrolls,
// and that gives a much higher true state for `childIsSafeToTrack`
syncState(view);
} else {
mTrackedChild = null;
}
}
}
private void syncState(AbsListView view) {
if (view.getChildCount() > 0) {
mTrackedChild = getChildInTheMiddle(view);
mTrackedChildPrevTop = mTrackedChild.getTop();
mTrackedChildPrevPosition = view.getPositionForView(mTrackedChild);
}
}
private View getChildInTheMiddle(AbsListView view) {
return view.getChildAt(view.getChildCount() / 2);
}
public static interface PixelScrollListener {
public void onScroll(AbsListView view, float deltaY);
}
}
Try to implement OnScrollListener:
list.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
int last = view.getLastVisiblePosition();
break;
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
});