android - RecyclerView - Horizontal LinearLayoutManager create / bind methods called way too often -
currently i'm @ end of ideas on following issue linearlayoutmanagers , recyclerviews on android:
what scenario wanted achieve
a horizontal recyclerview on user can swipe fast without limitations on fling. items being fullscreen sized making them big recyclerview itself. when fling has stopped or user stops manually, recycler should scroll 1 item (mimicing viewpager bit) (i'm using support revision 25.1.0)
code snippets
the pager-class itself
public class velocitypager extends recyclerview { private int mcurrentitem = 0; @nonnull private linearlayoutmanager mlayoutmanager; @nullable private onpagechangelistener monpagechangelistener = null; @nonnull private rect mviewrect = new rect(); @nonnull private onscrolllistener monscrolllistener = new onscrolllistener() { private int mlastitem = 0; @override public void onscrolled(recyclerview recyclerview, int dx, int dy) { if (monpagechangelistener == null) return; mcurrentitem = mlayoutmanager.findfirstvisibleitemposition(); final view view = mlayoutmanager.findviewbyposition(mcurrentitem); view.getlocalvisiblerect(mviewrect); final float offset = (float) mviewrect.left / ((view) view.getparent()).getwidth(); monpagechangelistener.onpagescrolled(mcurrentitem, offset, 0); if (mcurrentitem != mlastitem) { monpagechangelistener.onpageselected(mcurrentitem); mlastitem = mcurrentitem; } } @override public void onscrollstatechanged(recyclerview recyclerview, int newstate) { if (monpagechangelistener == null) return; monpagechangelistener.onpagescrollstatechanged(newstate); } }; public velocitypager(@nonnull context context) { this(context, null); } public velocitypager(@nonnull context context, @nullable attributeset attrs) { this(context, attrs, 0); } public velocitypager(@nonnull context context, @nullable attributeset attrs, int defstyle) { super(context, attrs, defstyle); mlayoutmanager = createlayoutmanager(); init(); } @nonnull private linearlayoutmanager createlayoutmanager() { return new linearlayoutmanager(getcontext(), linearlayoutmanager.horizontal, false); } @override protected void onattachedtowindow() { super.onattachedtowindow(); addonscrolllistener(monscrolllistener); } @override protected void ondetachedfromwindow() { super.ondetachedfromwindow(); removeonscrolllistener(monscrolllistener); } @override public void onscrollstatechanged(int state) { // if tap on phone while recyclerview scrolling stop in middle. // code fixes this. code not strictly necessary improves behaviour. if (state == scroll_state_idle) { linearlayoutmanager linearlayoutmanager = (linearlayoutmanager) getlayoutmanager(); int screenwidth = resources.getsystem().getdisplaymetrics().widthpixels; // views on screen int lastvisibleitemposition = linearlayoutmanager.findlastvisibleitemposition(); view lastview = linearlayoutmanager.findviewbyposition(lastvisibleitemposition); int firstvisibleitemposition = linearlayoutmanager.findfirstvisibleitemposition(); view firstview = linearlayoutmanager.findviewbyposition(firstvisibleitemposition); // distance need scroll int leftmargin = (screenwidth - lastview.getwidth()) / 2; int rightmargin = (screenwidth - firstview.getwidth()) / 2 + firstview.getwidth(); int leftedge = lastview.getleft(); int rightedge = firstview.getright(); int scrolldistanceleft = leftedge - leftmargin; int scrolldistanceright = rightmargin - rightedge; if (leftedge > screenwidth / 2) { smoothscrollby(-scrolldistanceright, 0); } else if (rightedge < screenwidth / 2) { smoothscrollby(scrolldistanceleft, 0); } } } private void init() { setlayoutmanager(mlayoutmanager); setitemanimator(new defaultitemanimator()); sethasfixedsize(true); } public void setcurrentitem(int index, boolean smoothscroll) { if (monpagechangelistener != null) { monpagechangelistener.onpageselected(index); } if (smoothscroll) smoothscrolltoposition(index); if (!smoothscroll) scrolltoposition(index); } public int getcurrentitem() { return mcurrentitem; } public void setonpagechangelistener(@nullable onpagechangelistener onpagechangelistener) { monpagechangelistener = onpagechangelistener; } public interface onpagechangelistener { /** * method invoked when current page scrolled, either part * of programmatically initiated smooth scroll or user initiated touch scroll. * * @param position position index of first page being displayed. * page position+1 visible if positionoffset nonzero. * @param positionoffset value [0, 1) indicating offset page @ position. * @param positionoffsetpixels value in pixels indicating offset position. */ void onpagescrolled(int position, float positionoffset, int positionoffsetpixels); /** * method invoked when new page becomes selected. animation not * complete. * * @param position position index of new selected page. */ void onpageselected(int position); /** * called when scroll state changes. useful discovering when user * begins dragging, when pager automatically settling current page, * or when stopped/idle. * * @param state new scroll state. * @see velocitypager#scroll_state_idle * @see velocitypager#scroll_state_dragging * @see velocitypager#scroll_state_settling */ void onpagescrollstatechanged(int state); } }
the item's xml layout
(note: root view has clickable other purposes inside app)
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true"> <linearlayout android:id="@+id/icon_container_top" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignparentend="true" android:layout_alignparentright="true" android:layout_alignparenttop="true" android:layout_gravity="top|end" android:layout_marginend="16dp" android:layout_marginright="16dp" android:layout_margintop="16dp" android:alpha="0" android:background="@drawable/info_background" android:orientation="horizontal" android:padding="4dp" tools:alpha="1"> <imageview android:id="@+id/delete" style="@style/selectableitembackground" android:layout_width="wrap_content" android:layout_height="wrap_content" android:clickable="true" android:contentdescription="@string/desc_delete" android:padding="12dp" android:src="@drawable/ic_delete_white_24dp" android:tint="@color/icons" /> </linearlayout> <linearlayout android:id="@+id/icon_container_bottom" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignparentend="true" android:layout_alignparentright="true" android:layout_centervertical="true" android:layout_marginbottom="16dp" android:layout_marginend="16dp" android:layout_marginright="16dp" android:alpha="0" android:background="@drawable/info_background" android:orientation="vertical" android:padding="4dp" tools:alpha="1"> <imageview android:id="@+id/size" style="@style/selectableitembackground" android:layout_width="wrap_content" android:layout_height="wrap_content" android:clickable="true" android:contentdescription="@string/desc_size" android:padding="12dp" android:src="@drawable/ic_straighten_white_24dp" android:tint="@color/icons" /> <imageview android:id="@+id/palette" style="@style/selectableitembackground" android:layout_width="wrap_content" android:layout_height="wrap_content" android:clickable="true" android:contentdescription="@string/desc_palette" android:padding="12dp" android:src="@drawable/ic_palette_white_24dp" android:tint="@color/icons" /> </linearlayout> </relativelayout>
the xml layout pager itself
(quite nested? might cause of problem? don't know... )
<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.drawerlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawer" android:layout_width="match_parent" android:layout_height="match_parent" android:fitssystemwindows="true" tools:opendrawer="end"> <swiperefreshlayout android:id="@+id/refresh_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.coordinatorlayout android:id="@+id/coordinator" android:layout_width="match_parent" android:layout_height="match_parent" android:fitssystemwindows="false"> <framelayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.my.example.optionalviewpager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="horizontal" app:layout_behavior="com.my.example.moveupbehavior" /> <android.support.v7.widget.toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionbarsize" android:background="@android:color/transparent" android:clickable="false" android:fitssystemwindows="false" app:contentinsetleft="0dp" app:contentinsetstart="0dp" app:contentinsetstartwithnavigation="0dp" app:layout_collapsemode="pin" app:navigationicon="@drawable/ic_menu_white_24dp" /> </android.support.design.widget.coordinatorlayout> </swiperefreshlayout> <include layout="@layout/layout_drawer" /> </android.support.v4.widget.drawerlayout>
part of adapter relevant viewholders
@override public int getitemcount() { return dataset.size(); } @override public myviewholder oncreateviewholder(viewgroup parent, int viewtype) { log.v("adapter", "createviewholder"); final layoutinflater layoutinflater = layoutinflater.from(parent.getcontext()); final view rootview = layoutinflater.inflate(r.layout.page, parent, false); return new myviewholder(rootview); } @override public void onbindviewholder(myviewholder page, int position) { log.v("adapter", string.format("bindviewholder(%d)", position)); final viewdata viewdata = dataset.get(position); page.bind(viewdata); listener.onviewadded(position, viewdata.getdata()); } @override public void onviewrecycled(myviewholder page) { if (page.getdata() == null) return; listener.onviewremoved(page.getdata().id); } @override public int getitemviewtype(int position) { return 0; }
the viewholder
public class myviewholder extends recyclerview.viewholder implements mylistener { @bindview(r.id.info_container) viewgroup minfocontainer; @bindview(r.id.icon_container_top) viewgroup miconcontainertop; @bindview(r.id.icon_container_bottom) viewgroup miconcontainerbottom; @bindview(r.id.info_rows) viewgroup minforows; @bindview(r.id.loading) view micloading; @bindview(r.id.sync_status) view micsyncstatus; @bindview(r.id.delete) view micdelete; @bindview(r.id.ic_fav) view micfavorite; @bindview(r.id.size) view micsize; @bindview(r.id.palette) view micpalette; @bindview(r.id.name) textview mname; @bindview(r.id.length) textview mlength; @bindview(r.id.threads) textview mthreads; @bindview(r.id.price) textview mprice; @nullable private mymodel mmodel = null; @nullable private activity mactivity; public myviewholder(view itemview) { super(itemview); butterknife.bind(this, itemview); mactivity= (activity) itemview.getcontext(); if (mactivity!= null) mactivity.addmylistener(this); } @onclick(r.id.delete) protected void clickdeletebtn() { if (mactivity == null || mactivity.getmode() != mode.edit) return; if (mmodel == null) return; animations.pop(micdelete); final int modelid = mmodel.id; if (mmodel.delete()) { mactivity.delete(modelid); } } @onclick(r.id.size) protected void clicksizebtn() { if (mactivity== null) return; mactivity.setuimode(mode.edit_size); animations.pop(micsize); } @onclick(r.id.palette) protected void clickpalettebtn() { if (mactivity== null) return; mactivity.setuimode(mode.edit_length); animations.pop(micpalette); } private void initmodelviews() { if (mdata == null) return; final locale locale = locale.getdefault(); mname.setvalue(string.format(locale, "model#%d", mmodel.id)); mlength.setvalue(html.fromhtml(string.format(locale, itemview.getcontext().getstring(r.string.template_length), mmodel.meters))); } /** * set icon container off screen @ beginning */ private void prepareviews() { new expectanim().expect(miconcontainertop).tobe(outofscreen(gravity.end), visible()) .toanimation() .setnow(); new expectanim().expect(miconcontainerbottom).tobe(outofscreen(gravity.end), visible()) .toanimation() .setnow(); } @nullable public mymodel getdata() { return mmodel; } private void enableedit() { new expectanim() .expect(miconcontainerbottom) .tobe(atitsoriginalposition()) .toanimation() .start(); } private void disableedit() { new expectanim() .expect(miconcontainerbottom) .tobe(outofscreen(gravity.end)) .toanimation() .start(); } private void enableinfo() { new expectanim() .expect(minfocontainer) .tobe(atitsoriginalposition()) .toanimation() .start(); } private void disableinfo() { new expectanim() .expect(minfocontainer) .tobe(outofscreen(gravity.bottom)) .toanimation() .start(); } private void enabledelete() { if (miconcontainertop == null) return; new expectanim() .expect(miconcontainertop) .tobe(atitsoriginalposition(), visible()) .toanimation() .start(); } private void disabledelete() { if (miconcontainertop == null) return; new expectanim() .expect(miconcontainertop) .tobe(outofscreen(gravity.end), invisible()) .toanimation() .start(); } public void bind(@nonnull final viewdata viewdata) { mmodel = viewdata.getdata(); prepareviews(); initmodelviews(); } }
so, here's issue these!
when intializing adapter insert 15 17 items via observable. seems correct:
but when swiping horizontally recyclerview's callbacks seem totally messed , produce weird results:
do see recycler not try recycle old viewholders @ all? image shows small portion of "spamming" going on. create new viewholder more 2 times same position while scroll recycler slowly!
another side problem is: listener should allow me pass bind / recycle events underlying game engine create destroy entities on screen. due excessive spamming of events create entities excessively!
i excpected recycler create new viewholder first (let's in example 17) times , reuse items how should.
please help, i'm stuck on problem 2 days , i'm frustrated after searching people same issues without luck. thank you!
there's problem viewholder
recycling. i'm guessing animations you're running inside myviewholder
might prevent recyclerview
recycling holders properly. make sure cancel animations @ point, e.g. in recyclerview.adapter#onviewdetachedfromwindow()
.
after you've fixed this, suggest follow @eugenpechanec's suggestion reduce amount of custom calculations done in onscrolllistener
s. it's better rely on support library classes , tweak behavior little.
Comments
Post a Comment