I want to scroll the a ListView in Android by number of pixels. For example I want to scroll the list 10 pixels down (so that the first item on the list has its top 10 pixel rows hidden).
I thought the obviously visible scrollBy or scrollTo methods on ListView would do the job, but they don't, instead they scroll the whole list wrongly (In fact, the getScrollY always return zero even though I have scrolled the list using my finger.)
What I'm doing is I'm capturing Trackball events and I want to scroll the listview smoothly according to the motion of the trackball.
The supported way to scroll a ListView widget is:
mListView.smoothScrollToPosition(position);
http://developer.android.com/reference/android/widget/AbsListView.html#smoothScrollToPosition(int)
However since you mentioned specifically that you would like to offset the view vertically, you must call:
mListView.setSelectionFromTop(position, yOffset);
http://developer.android.com/reference/android/widget/ListView.html#setSelectionFromTop(int,%20int)
Note that you can also use smoothScrollByOffset(yOffset). However it is only supported on API >= 11
http://developer.android.com/reference/android/widget/ListView.html#smoothScrollByOffset(int)
If you look at the source for the scrollListBy() method added in api 19 you will see that you can use the package scoped trackMotionScroll method.
public class FutureListView {
private final ListView mView;
public FutureListView(ListView view) {
mView = view;
}
/**
* Scrolls the list items within the view by a specified number of pixels.
*
* #param y the amount of pixels to scroll by vertically
*/
public void scrollListBy(int y) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mView.scrollListBy(y);
} else {
// scrollListBy just calls trackMotionScroll
trackMotionScroll(-y, -y);
}
}
private void trackMotionScroll(int deltaY, int incrementalDeltaY) {
try {
Method method = AbsListView.class.getDeclaredMethod("trackMotionScroll", int.class, int.class);
method.setAccessible(true);
method.invoke(mView, deltaY, incrementalDeltaY);
} catch (Exception ex) {
throw new RuntimeException(ex);
};
}
}
Here is some code from my ListView subclass. It can easily be adapted so it can be used in Activity code.
getListItemsHeight() returns the total pixel height of the list, and fills an array with vertical pixel offsets of each item. While this information is valid, getListScrollY() returns the current vertical pixel scroll position, and scrollListToY() scrolls the list to pixel position.
If the size or the content of the list changes, getListItemsHeight() has to be called again.
private int m_nItemCount;
private int[] m_nItemOffY;
private int getListItemsHeight()
{
ListAdapter adapter = getAdapter();
m_nItemCount = adapter.getCount();
int height = 0;
int i;
m_nItemOffY = new int[m_nItemCount];
for(i = 0; i< m_nItemCount; ++i){
View view = adapter.getView(i, null, this);
view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
m_nItemOffY[i] = height;
height += view.getMeasuredHeight();
}
return height;
}
private int getListScrollY()
{
int pos, nScrollY, nItemY;
View view;
pos = getFirstVisiblePosition();
view = getChildAt(0);
nItemY = view.getTop();
nScrollY = m_nItemOffY[pos] - nItemY;
return nScrollY;
}
private void scrollListToY(int nScrollY)
{
int i, off;
for(i = 0; i < m_nItemCount; ++i){
off = m_nItemOffY[i] - nScrollY;
if(off >= 0){
setSelectionFromTop(i, off);
break;
}
}
}
For now, ListViewCompat is probably a better solution.
android.support.v4.widget.ListViewCompat.scrollListBy(#NonNull ListView listView, int y)
if you want to move by pixels then u can use this
public void scrollBy(ListView l, int px){
l.setSelectionFromTop(l.getFirstVisiblePosition(),l.getChildAt(0).getTop() - px);
}
this works for even ones with massive headers
Related
I haven't investigated this much yet, however I haven't seen a feature such as this done before or found much information regarding the subject, so I thought it would be best to reach out to SO to see if anyone has toyed with this idea before and could provide any advice. If not, I'll post the solution below when I find it.
Desired Outcome
Currently I have a GridView populated with content. I have a FloatingActionButton on top of the GridView and when the user taps on the FAB, I would like to display a new View. When tapped, I want each individual GridView item to rotate and move towards the edge of the screen resulting in the whole GridView parting to make room for the new View to slide in from the bottom. The RecyclerView will become unscrollable when the secondary View is present, and will stay that way until the user goes back, doing so would result in the opposite animation occurring bringing the ViewHolders back into the centre and closing the gap.
Excuse me for my appauling attempt at drawing my problem. :)
GridView pre-animation
GridView after animation
Each view item should animate independently from one another and the end animation will simulate a kind of exploding RecyclerView effect.
Possible Approach / Attempted so Far
I am currently reading up on the behaviour of the individual elements of the RecyclerView. I also got some pretty good information from this Android Summit video about RecyclerView animations.
Right now I think there are a couple of different approache:
Using two different LayoutManagers and attempting to animate between them.
Using an ItemDecorator to alter each ViewHolders margins and angle.
Any advice or recommendations will be greatly appreciated, I'll update the above list as I work through the problem.
Thanks!
Being able to animate the views within a RecyclerView turned out to be very simple! All you need is a custom RecyclerView.LayoutManager and you then have access to all the views which are visible for your animating pleasure.
The main method here is getChildCount() and getChildAt(int index), with both of these methods as seen below in getVisibleViews() you have access to all of the views which the RecyclerView is currently displaying. I then use a simple View.animate() call to animate each view to the desired X position and rotation. I'm sure this will work with all other types of animations too! I now have exactly what I wanted. :)
Here's the custom LayoutManager, remember if you don't extend a predefined LayoutManager such as GridLayoutManager or LinearLayoutManager you'll need to supply more methods such as generateDefaultLayoutParams().
public class AnimatingGridLayoutManager extends GridLayoutManager {
private static final int PARTED_ANIMATION_DURATION = 200;
private static final int PARTED_ANIMATION_VARIANCE = 20;
private static final int PARTED_ANIMATION_OFFSET = 25;
private static final int PARTED_ANIMATION_ANGLE = 15;
private boolean itemsParted;
...
public void setItemsParted(boolean itemsParted, Activity context) {
Display display = context.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int screenWidth = size.x;
if (this.itemsParted != itemsParted) {
this.itemsParted = itemsParted;
if (itemsParted) {
partItems(context, screenWidth);
} else {
closeItems(screenWidth);
}
}
}
private void partItems(Context context, int screenWidth) {
List<View> visibleViews = getVisibleViews();
for (int i = 0; i < visibleViews.size(); i++) {
int viewWidth = visibleViews.get(i).getWidth();
int offset = ViewUtils.getPixelsFromDp(getRandomNumberNearInput(PARTED_ANIMATION_OFFSET), context);
int xPos = (-viewWidth) + offset;
int rotation = getRandomNumberNearInput(-PARTED_ANIMATION_ANGLE);
if (viewPositionIsRight(i)) {
// invert values to send view to end of screen instead of start
xPos = screenWidth - offset;
rotation = -rotation;
}
visibleViews.get(i).animate()
.x(xPos)
.rotation(rotation)
.setDuration(PARTED_ANIMATION_DURATION)
.setInterpolator(new FastOutSlowInInterpolator());
}
}
private void closeItems(int screenWidth) {
List<View> visibleViews = getVisibleViews();
for (int i = 0; i < visibleViews.size(); i++) {
int xPos = 0;
if (viewPositionIsRight(i)) {
xPos = screenWidth / 2;
}
visibleViews.get(i).animate()
.x(xPos)
.rotation(0)
.setDuration(PARTED_ANIMATION_DURATION)
.setInterpolator(new FastOutSlowInInterpolator());
}
}
private boolean viewPositionIsRight(int position) {
// if isn't 2 row grid new logic is needed
return position % 2 != 0;
}
private int getRandomNumberNearInput(int input) {
Random random = new Random();
int max = input + PARTED_ANIMATION_VARIANCE;
int min = input - PARTED_ANIMATION_VARIANCE;
return random.nextInt(max - min) + min;
}
private List<View> getVisibleViews() {
List<View> visibleViews = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
visibleViews.add(getChildAt(i));
}
return visibleViews;
}
}
I have a RecyclerView which has a staggeredGridLayoutManager as layout manager. My layout stands as having 2 spans(cols), which items inside may have different heights.
Inflated items has a ImageView and some other views inside a LinearLayout container.
I want to save Inflated(or should I say binded?) View's size(height and width) after the view's image is fully loaded. Because this operation makes me know how much width and height the LinearLayout occupy at final-after the image is placed in the layout-.
After scrolling, this container may be recycled and binded again. What I want to achieve is to savebinded layout's size immediately after it is binded, according to the height and width values previously calculated because this makes recyclerView's item positions more stable. They are less likely move around.
I have mWidth and mHeight members in my ViewHolder, which basically store these values. However, I lost syncronisation between item position in adapter and corresponding ViewHolder. For example I calculate height of 8th item as 380px when it first become visible, which is correct. After recycling and binding 8th position again, my view's height retrieved as 300 px, which is incorrect.
Code:
BasicActivity is derived from Activity..
public ItemsRVAdapter(BasicActivity activity, JSONArray items){
this.items = items;
this.activity = activity;
this.itemControl = new Items(activity);
}
OnCreate:
#Override
public ItemListViewHolders onCreateViewHolder(ViewGroup viewGroup, int i) {
View layoutView =activity.getLayoutInflater().inflate(R.layout.list_element_items, viewGroup, false);
ItemListViewHolders rcv = new ItemListViewHolders(layoutView);
return rcv;
}
OnViewAttachedToWindow (I tried the same code here in different places, like onViewRecycled but I don't know this method is the most right place to calculete the size)
#Override
public void onViewAttachedToWindow(ItemListViewHolders holder)
{
holder.layoutCapsule.measure(LinearLayout.MeasureSpec.makeMeasureSpec(0, LinearLayout.MeasureSpec.UNSPECIFIED), LinearLayout.MeasureSpec.makeMeasureSpec(0, LinearLayout.MeasureSpec.UNSPECIFIED));
if(holder.image.getDrawable() != null){
holder.height = holder.layoutCapsule.getHeight();
holder.width = holder.layoutCapsule.getWidth();
}else{
holder.height = 0;
holder.width = 0;
}
}
onBindViewHolder: Only relevant part. Here I paired position value and my array's member index
#Override
public void onBindViewHolder(ItemListViewHolders holder, int position) {
try {
//JSONObject item = items.getJSONObject(holder.getAdapterPosition());
JSONObject item = items.getJSONObject(position);
holder.image.setImageDrawable(null);
ViewGroup viewGroup = holder.layoutCapsule; //Main Container
...
}
}
I recommend looking for a different approach to resolve your problem with the items moving around not depending on View sizes, but if you want to proceed this way this is my proposed solution:
Don't depend or save the size values on the holder as this gets recycled, you will need to create an object "descriptor" with the values (width and height) for each position and save them on a HashMap or something like that, save the values as you are doing it already, i understand on "onViewAttachedToWindow".
class Descriptor(){
int width;
int height;
void setWidth(int width){
this.width = width;
}
int getWidth(){
return width;
}
void setHeight(int height){
this.height = height;
}
int getHeight(){
return height;
}
Initialize array on constructor:
descriptors = new HashMap<Integer, Descriptor>();
in onBindViewHolder save the position on a view tag to use it on OnViewAttachedToWindow
public void onBindViewHolder(ItemListViewHolders holder, int position) {
....
holder.image.setTag(position);
...
}
populate values on onViewAttachedToWindow
public void onViewAttachedToWindow(ItemListViewHolders holder){
...
int position = (Integer)holder.image.getTag();
Descriptor d = descriptors.get(position);
if(d == null){
d = new Descriptor();
descriptors.put(position, d);
}
d.setWidth(holder.layoutCapsule.getWidth());
d.setHeight(holder.layoutCapsule.getHeight());
...
}
Then use the size data on the descriptor on the method you need getting it by position, you will be creating descriptors as the user is scrolling down, also this works on the asumption that the data maintains the same position during the life of the adapter.
Inside a RelativeLayout; I have a TableLayout in a ScrollView, and a horizontal LinearLayout. My view is working fine: as the table scrolls in the background, the LinearLayout is static in the foreground. What I now need, is to know exactly where the table and the LinearLayout are intersecting during scrolling.
Since the LinearLayout is static, I know where it is at all time, say y=50pixel. Supposing I have TextViews inside the TableLayout. So as the table scrolls, I want to know which TextView (i.e. tableRow) is intersecting/hidden by the LinearLayout.
Have you tried using View.getLocationOnScreen() on the table rows?
I have a horizontal line at y-offset = 310 pixel. As I scroll my
tableLayout, which is inside a ScrollView, I want to know which row of
the table is intersecting my horizontal line.
You could use the View.getLocationInWindow() method. Initially you'd need to see which row intersects the line and then using a custom ScrollView, follow the scrolling:
// in the onCreate:
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int[] loc = new int[2];
ll.getLocationInWindow(loc);
scrollView.setYIntersection(loc[1]);
ViewGroup table = (ViewGroup) scrollView.getChildAt(0);
// find out which row initially intersects our line so
// we can initialize mCrossedRow and position
for (int i = 0; i < table.getChildCount(); i++) {
final View row = table.getChildAt(i);
row.getLocationInWindow(loc);
if (loc[1] <= scrollView.getYIntersection()
&& loc[1] + row.getHeight() >= scrollView
.getYIntersection()) {
scrollView.setCurrIntersection(row, i);
row.setBackgroundColor(Color.RED);
break;
}
}
scrollView.getViewTreeObserver()
.removeOnGlobalLayoutListener(this);
}
});
With the custom ScrollView being:
public class CustomScrollView extends ScrollView {
/**
* This will be the current intersected row.
*/
private View mCrossedRow;
/**
* The row position of the intersected row, mCrossedRow.
*/
private int mRowOrder;
/**
* The y value of the intersecting line.
*/
private int mLine;
/**
* Used as a temporary holder for the location retrieval.
*/
private int[] mLocHelper = new int[2];
public CustomScrollView(Context context) {
super(context);
}
public void setYIntersection(int y) {
mLine = y;
}
public int getYIntersection() {
return mLine;
}
public void setCurrIntersection(View row, int childPosition) {
mCrossedRow = row;
mRowOrder = childPosition;
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
// this will be called every time the user scrolls so we need to
// keep updating the position and see if the crossed row still
// intersects the line otherwise move it to the next row.
mCrossedRow.getLocationInWindow(mLocHelper);
if (mLocHelper[1] <= mLine
&& mLocHelper[1] + mCrossedRow.getHeight() >= mLine) {
// do nothing, we're still in the same row
} else {
if (t - oldt > 0) {
// going down so increase the row position
mRowOrder++;
} else {
// going up so decrease the row position
mRowOrder--;
}
// visual effect, the intersecting row will have a red
// background and all other rows will have a white background.
// You could setup a listener here to get notified that a new
// row intersects the line.
mCrossedRow.setBackgroundColor(Color.WHITE);
mCrossedRow = ((ViewGroup) getChildAt(0)).getChildAt(mRowOrder);
mCrossedRow.setBackgroundColor(Color.RED);
}
}
}
I have a custom ListView class (subclassed from ListView) and I need it to add a small padding to the bottom of the final view element so that it is not overlapped by a small bar that I have across the bottom of the screen. I only want to do this when the child views grow past the visible region of the listview. I am trying to use this code to achieve this:
#Override
protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// If there are hidden listview items we need to add a small padding to the last one so that it
// partially hidden by the bottom sliding drawer handle
if (this.getLastVisiblePosition() - this.getFirstVisiblePosition() + 1 > this.getCount()) {
LinearLayout v2 = (LinearLayout) this.getChildAt(this.getCount() - 1);
v2.setPadding(v2.getPaddingLeft(), v2.getPaddingTop(), v2.getPaddingRight(),
v2.getPaddingBottom() + 5);
}
}
However, the values returned by getLastVisiblePostion(), getFirstVisiblePostion() and getCount() do not reflect what the adapter holds yet. I am assuming this is because the Adapter has not yet notified the ListView of the data, but I cannot figure out where the ListView would actually know about the data and thus have the correct values. This code is being run when the Activity is being loaded.
At what point in the rendering process will I have access to this data? I should also say that I use an AsyncTask to load the data from a database and then create the Adapter in there and add it to the list view. Is there an event I can use within the ListView that will fire when the adapter adds data/causes the listview to render the new items?
I'm not sure if it's the best solution, but I know that in one of the branches of a pull-to-refresh implementation for Android, the height of the list is compared against the total height of all the list items. From what you're saying, it sounds like that's the same information you're looking for in order to determine whether to apply extra padding or not.
The relevant parts of the implementation are in the following three methods:
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mHeight == -1) { // do it only once
mHeight = getHeight(); // getHeight only returns useful data after first onDraw()
adaptFooterHeight();
}
}
/**
* Adapts the height of the footer view.
*/
private void adaptFooterHeight() {
int itemHeight = getTotalItemHeight();
int footerAndHeaderSize = mFooterView.getHeight()
+ (mRefreshViewHeight - mRefreshOriginalTopPadding);
int actualItemsSize = itemHeight - footerAndHeaderSize;
if (mHeight < actualItemsSize) {
mFooterView.setHeight(0);
} else {
int h = mHeight - actualItemsSize;
mFooterView.setHeight(h);
setSelection(1);
}
}
/**
* Calculates the combined height of all items in the adapter.
*
* Modified from http://iserveandroid.blogspot.com/2011/06/how-to-calculate-lsitviews-total.html
*
* #return
*/
private int getTotalItemHeight() {
ListAdapter adapter = getAdapter();
int listviewElementsheight = 0;
for(int i =0; i < adapter.getCount(); i++) {
View mView = adapter.getView(i, null, this);
mView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
listviewElementsheight+= mView.getMeasuredHeight();
}
return listviewElementsheight;
}
The full source code can be found here on GitHub.
I had a similar issue when getFirstVisiblePosition() returned number greater than getCount().
Fixed it by calling listView.setAdapter(listView.getAdapter()).
I have a problem with my custom ViewGroup. I layout 3 of the children in a row and use a Scroller to scroll to the middle child. Based on the touch input of the user I change the children that should be displayed and request a new layout. Then I layout the children in a new order. But when one child has been displayed in a previous layout run the children lie on top of each other. I checked that the children get layout in the right way and I think the old layout is not deleted and the new children get just drawn on top of the old layout. Is there a way to ensure that the old layout gets cleared or something?
Here is my code:
#Override
public boolean onTouchEvent(MotionEvent event) {
...
case MotionEvent.ACTION_UP:
if(old != current)
this.requestLayout();
else
this.scrollTo(getWidth(), 0);
...
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// show current-1, current, current+1
int count = 0;
for(int i = 1; i >= -1; i--) {
// determine index of child
// the mod does a modulo
int index = mod(current-i, getChildCount());
// position in row from left to right
this.getChildAt(index).layout(count*this.getWidth(), 0, (count+1)*this.getWidth(), height);
count++;
}
// scroll to middle view
this.scrollTo(getWidth(), 0);
...
}
Try to call invalidate() at the end of onLayout().