Skip to content

Instantly share code, notes, and snippets.

@aenain
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aenain/8a3efdf7f96fff0d6807 to your computer and use it in GitHub Desktop.
Save aenain/8a3efdf7f96fff0d6807 to your computer and use it in GitHub Desktop.
Fix etsy's staggered grid view when one column is higher than the other by more than grid view height. https://github.com/etsy/AndroidStaggeredGrid/issues/66
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_width="match_parent">
<LinearLayout
android:id="@+id/content"
style="@style/GridItem">
<com.etsy.android.grid.util.DynamicHeightImageView
android:id="@+id/image"
style="@style/Image.FullWidth" />
<TextView
android:id="@+id/title"
style="@style/GridItem.PrimaryHeader" />
<TextView
android:id="@+id/description"
style="@style/GridItem.Text" />
</LinearLayout>
</LinearLayout>
package no.bstcm.loyaltyapp.core.util;
import android.view.View;
import android.widget.AbsListView;
import com.etsy.android.grid.StaggeredGridView;
import no.bstcm.loyaltyapp.core.R;
/**
* Grid library has a very interesting bug related to how views are recycled.
* If last item is higher than grid view, then other column disappears
* or is weirdly moved.
*
* To fix that let's display a placeholder that ensures that columns have the same height
* in case the difference between them is greater than grid view height.
*
* Issues:
* @see https://github.com/etsy/AndroidStaggeredGrid/issues/21
* @see https://github.com/etsy/AndroidStaggeredGrid/issues/66
*
* Example:
* How to alter adapter to use it?
*
* public class GridAdapter extends ArrayAdapter<...>
* implements GridHeightPlaceholder.AdapterCallbacks {
*
* private StaggeredGridView lookupView;
* private final GridHeightPlaceholder placeholder;
*
* public GridAdapter(Context context, List<...> list) {
* super(context, R.layout.fragment_grid_item, list);
* placeholder = new GridHeightPlaceholder(this);
* }
*
* public void setLookupView(StaggeredGridView lookupView) {
* this.lookupView = lookupView;
* if (lookupView != null) {
* placeholder.listenToScroll();
* } else {
* placeholder.stopListeningToScroll();
* placeholder.onDetach();
* }
* }
*
* @Override
* public StaggeredGridView getLookupView() {
* return lookupView;
* }
*
* @Override
* public int getCount() {
* int count = super.getCount();
* if (placeholder.isDisplayed()) {
* count++;
* }
* return count;
* }
*
* @Override
* public View getView(int position, View convertView, ViewGroup parent) {
* if (convertView == null) {
* LayoutInflater inflater = (LayoutInflater) getContext()
* .getSystemService(android.app.Activity.LAYOUT_INFLATER_SERVICE);
* convertView = inflater.inflate(R.layout.fragment_grid_item, null);
* }
*
* if (position >= super.getCount()) {
* placeholder.bindView(convertView);
* } else {
* // regular view binding for model getItem(position)
* }
*
* return convertView;
* }
*
* // ...
*
* }
*
* Later in activity or fragment:
* StaggeredGridView sectionView = (StaggeredGridView) findViewById(R.id.grid);
* GridAdapter adapter = new GridAdapter(this, getList());
* adapter.setLookupView(sectionView);
* sectionView.setAdapter(adapter);
*
* NOTE!
*
* It is also a good idea to cleanup when i.e. activity is destroyed.
* All you need to do is to call GridAdapter.setLookupView(null).
*/
public class GridHeightPlaceholder implements AbsListView.OnScrollListener {
private AdapterCallbacks adapter;
private int height = 0, screenWidth = 0;
private StaggeredGridViewProxy gridView;
private WeakReference<View> viewRef;
public GridHeightPlaceholder(AdapterCallbacks adapter) {
this.adapter = adapter;
}
public void listenToScroll() {
getGridView().setOnScrollListener(this);
}
public void stopListeningToScroll() {
if (gridView != null) {
gridView.setOnScrollListener(null);
}
}
public void onDetach() {
if (gridView != null) {
stopListeningToScroll();
gridView.onDetach();
gridView = null;
}
if (viewRef != null) {
viewRef.clear();
}
}
public boolean isDisplayed() {
return height > 0;
}
public void bindView(View view) {
View content = view.findViewById(R.id.content);
viewRef = new WeakReference<>(content);
content.getLayoutParams().height = Math.round(height / 2);
content.requestLayout();
content.setAlpha(0);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem + visibleItemCount >= totalItemCount) {
StaggeredGridViewProxy gridView = getGridView();
int[] columnHeights = new int[]{gridView.getColumnBottom(0), gridView.getColumnBottom(1)};
int columnHeightDiff, newHeight;
View itemView = getPlaceholderView();
if (itemView != null) {
int column = getColumn(itemView);
columnHeights[column] -= itemView.getHeight();
}
columnHeightDiff = Math.abs(columnHeights[0] - columnHeights[1]);
if (height > columnHeightDiff || columnHeightDiff > gridView.getMeasuredHeight()) {
newHeight = columnHeightDiff - getGridView().getItemMargin();
if (newHeight != height) {
height = newHeight;
adapter.notifyDataSetChanged();
}
}
}
}
public int getColumn(View view) {
int[] coords = new int[2];
view.getLocationInWindow(coords);
if (coords[0] >= Math.floor(getScreenWidth(view.getContext()) / 2)) {
return 1;
} else {
return 0;
}
}
private int getScreenWidth(Context context) {
if (screenWidth <= 0) {
DisplayMetrics metrics = new DisplayMetrics();
WindowManager window = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
window.getDefaultDisplay().getMetrics(metrics);
screenWidth = metrics.widthPixels;
}
return screenWidth;
}
private View getPlaceholderView() {
if (viewRef != null) {
return viewRef.get();
} else {
return null;
}
}
private StaggeredGridViewProxy getGridView() {
if (gridView == null) {
gridView = new StaggeredGridViewProxy(adapter.getLookupView());
}
return gridView;
}
public static interface AdapterCallbacks {
public void notifyDataSetChanged();
public StaggeredGridView getLookupView();
}
}
package no.bstcm.loyaltyapp.core.util;
import android.view.View;
import android.widget.AbsListView;
import com.etsy.android.grid.StaggeredGridView;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import no.bstcm.loyaltyapp.core.R;
public class StaggeredGridViewProxy {
private final WeakReference<StaggeredGridView> targetView;
private Field columnBottomsField = null;
public StaggeredGridViewProxy(StaggeredGridView view) {
this.targetView = new WeakReference<StaggeredGridView>(view);
}
public void onDetach() {
StaggeredGridView view = targetView.get();
if (view != null) {
view.setOnScrollListener(null);
targetView.clear();
}
}
public int getColumnBottom(int column) {
if (columnBottomsField == null) {
try {
columnBottomsField = StaggeredGridView.class.getDeclaredField("mColumnBottoms");
columnBottomsField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
return 0;
}
}
try {
if (columnBottomsField.getType().isArray()) {
Object array = columnBottomsField.get(targetView.get());
return (int) Array.get(array, column);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return 0;
}
public int getItemMargin() {
return (int) targetView.get().getResources().getDimension(R.dimen.grid_item_margin);
}
public View findViewWithTag(Object tag) {
return targetView.get().findViewWithTag(tag);
}
public void setOnScrollListener(AbsListView.OnScrollListener listener) {
targetView.get().setOnScrollListener(listener);
}
public int getMeasuredHeight() {
return targetView.get().getMeasuredHeight();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment