diff --git a/app/src/main/java/tw/y_studio/ptt/ui/RecyclerItemClickListener.java b/app/src/main/java/tw/y_studio/ptt/ui/RecyclerItemClickListener.java index eeb32ec..53762c0 100644 --- a/app/src/main/java/tw/y_studio/ptt/ui/RecyclerItemClickListener.java +++ b/app/src/main/java/tw/y_studio/ptt/ui/RecyclerItemClickListener.java @@ -74,7 +74,7 @@ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { View childView = rv.findChildViewUnder(e.getX(), e.getY()); if (childView != null && clickListener != null && gestureDetector.onTouchEvent(e)) { - if (e.getY() > 0 && e.getY() > decorator.getStickyHolderHight()) { + if (e.getY() > 0 && e.getY() > decorator.getStickyHolderHeight()) { clickListener.onItemClick(childView, rv.getChildAdapterPosition(childView)); } else { clickListener.onItemClick(childView, 0); diff --git a/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyAdapter.java b/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyAdapter.java deleted file mode 100644 index daf2da1..0000000 --- a/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyAdapter.java +++ /dev/null @@ -1,47 +0,0 @@ -package tw.y_studio.ptt.ui.stickyheader; - -import android.view.ViewGroup; - -import androidx.recyclerview.widget.RecyclerView; - -public abstract class StickyAdapter< - SVH extends RecyclerView.ViewHolder, VH extends RecyclerView.ViewHolder> - extends RecyclerView.Adapter { - - /** - * This method gets called by {@link StickyHeaderItemDecorator} to fetch the position of the - * header item in the adapter that is used for (represents) item at specified position. - * - * @param itemPosition int. Adapter's position of the item for which to do the search of the - * position of the header item. - * @return int. Position of the header for an item in the adapter or RecyclerView.NO_POSITION - * (-1) if an item has no header. - */ - public abstract int getHeaderPositionForItem(int itemPosition); - - /** - * This method gets called by {@link StickyHeaderItemDecorator} to setup the header View. - * - * @param holder RecyclerView.ViewHolder. Holder to bind the data on. - * @param headerPosition int. Position of the header item in the adapter. - */ - public abstract void onBindHeaderViewHolder(SVH holder, int headerPosition); - - /** - * Called only twice when {@link StickyHeaderItemDecorator} needs a new {@link - * RecyclerView.ViewHolder} to represent a sticky header item. Those two instances will be - * cached and used to represent a current top sticky header and the moving one. - * - *

You can either create a new View manually or inflate it from an XML layout file. - * - *

The new ViewHolder will be used to display items of the adapter using {@link - * #onBindHeaderViewHolder(RecyclerView.ViewHolder, int)}. Since it will be re-used to display - * different items in the data set, it is a good idea to cache references to sub views of the - * View to avoid unnecessary View.findViewById(int) calls. - * - * @param parent The ViewGroup to resolve a layout params. - * @return A new ViewHolder that holds a View of the given view type. - * @see #onBindHeaderViewHolder(RecyclerView.ViewHolder, int) - */ - public abstract SVH onCreateHeaderViewHolder(ViewGroup parent); -} diff --git a/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyAdapter.kt b/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyAdapter.kt new file mode 100644 index 0000000..3d76963 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyAdapter.kt @@ -0,0 +1,43 @@ +package tw.y_studio.ptt.ui.stickyheader + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +abstract class StickyAdapter : RecyclerView.Adapter() { + /** + * This method gets called by [StickyHeaderItemDecorator] to fetch the position of the + * header item in the adapter that is used for (represents) item at specified position. + * + * @param itemPosition int. Adapter's position of the item for which to do the search of the + * position of the header item. + * @return int. Position of the header for an item in the adapter or RecyclerView.NO_POSITION + * (-1) if an item has no header. + */ + abstract fun getHeaderPositionForItem(itemPosition: Int): Int + + /** + * This method gets called by [StickyHeaderItemDecorator] to setup the header View. + * + * @param holder RecyclerView.ViewHolder. Holder to bind the data on. + * @param headerPosition int. Position of the header item in the adapter. + */ + abstract fun onBindHeaderViewHolder(holder: SVH, headerPosition: Int) + + /** + * Called only twice when [StickyHeaderItemDecorator] needs a new [ ] to represent a sticky header item. Those two instances will be + * cached and used to represent a current top sticky header and the moving one. + * + * + * You can either create a new View manually or inflate it from an XML layout file. + * + * + * The new ViewHolder will be used to display items of the adapter using [ ][.onBindHeaderViewHolder]. Since it will be re-used to display + * different items in the data set, it is a good idea to cache references to sub views of the + * View to avoid unnecessary View.findViewById(int) calls. + * + * @param parent The ViewGroup to resolve a layout params. + * @return A new ViewHolder that holds a View of the given view type. + * @see .onBindHeaderViewHolder + */ + abstract fun onCreateHeaderViewHolder(parent: ViewGroup): SVH +} diff --git a/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyHeaderItemDecorator.java b/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyHeaderItemDecorator.java deleted file mode 100644 index 5ad46f5..0000000 --- a/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyHeaderItemDecorator.java +++ /dev/null @@ -1,209 +0,0 @@ -package tw.y_studio.ptt.ui.stickyheader; - -import android.graphics.Canvas; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -public class StickyHeaderItemDecorator extends RecyclerView.ItemDecoration { - private StickyAdapter adapter; - private int currentStickyPosition = RecyclerView.NO_POSITION; - private RecyclerView recyclerView; - private RecyclerView.ViewHolder currentStickyHolder; - private View lastViewOverlappedByHeader = null; - private int stickyHolderHight = 0; - - public int getStickyHolderHight() { - return stickyHolderHight; - } - - public StickyHeaderItemDecorator(@NonNull StickyAdapter adapter) { - this.adapter = adapter; - } - - public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { - if (this.recyclerView == recyclerView) { - return; // nothing to do - } - if (this.recyclerView != null) { - destroyCallbacks(this.recyclerView); - } - this.recyclerView = recyclerView; - if (recyclerView != null) { - currentStickyHolder = adapter.onCreateHeaderViewHolder(recyclerView); - fixLayoutSize(); - setupCallbacks(); - } - } - - private void setupCallbacks() { - recyclerView.addItemDecoration(this); - } - - private void destroyCallbacks(RecyclerView recyclerView) { - recyclerView.removeItemDecoration(this); - } - - @Override - public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { - super.onDrawOver(c, parent, state); - - RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); - if (layoutManager == null) { - return; - } - - int topChildPosition = RecyclerView.NO_POSITION; - if (layoutManager instanceof LinearLayoutManager) { - topChildPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); - } else { - View topChild = parent.getChildAt(0); - if (topChild != null) { - topChildPosition = parent.getChildAdapterPosition(topChild); - } - } - - if (topChildPosition == RecyclerView.NO_POSITION) { - return; - } - - View viewOverlappedByHeader = - getChildInContact(parent, currentStickyHolder.itemView.getBottom()); - if (viewOverlappedByHeader == null) { - if (lastViewOverlappedByHeader != null) { - viewOverlappedByHeader = lastViewOverlappedByHeader; - } else { - viewOverlappedByHeader = parent.getChildAt(topChildPosition); - } - } - lastViewOverlappedByHeader = viewOverlappedByHeader; - - int overlappedByHeaderPosition = parent.getChildAdapterPosition(viewOverlappedByHeader); - int overlappedHeaderPosition; - int preOverlappedPosition; - if (overlappedByHeaderPosition > 0) { - preOverlappedPosition = - adapter.getHeaderPositionForItem(overlappedByHeaderPosition - 1); - overlappedHeaderPosition = adapter.getHeaderPositionForItem(overlappedByHeaderPosition); - } else { - preOverlappedPosition = adapter.getHeaderPositionForItem(topChildPosition); - overlappedHeaderPosition = preOverlappedPosition; - } - - if (preOverlappedPosition == RecyclerView.NO_POSITION) { - return; - } - - if (preOverlappedPosition != overlappedHeaderPosition - && shouldMoveHeader(viewOverlappedByHeader)) { - updateStickyHeader(topChildPosition, overlappedByHeaderPosition); - moveHeader(c, viewOverlappedByHeader); - } else { - updateStickyHeader(topChildPosition, RecyclerView.NO_POSITION); - drawHeader(c); - } - } - - // shouldMoveHeader returns the sticky header should move or not. - // This method is for avoiding sinking/departing the sticky header into/from top of screen - private boolean shouldMoveHeader(View viewOverlappedByHeader) { - int dy = (viewOverlappedByHeader.getTop() - viewOverlappedByHeader.getHeight()); - return (viewOverlappedByHeader.getTop() >= 0 && dy <= 0); - } - - @SuppressWarnings("unchecked") - private void updateStickyHeader(int topChildPosition, int contactChildPosition) { - int headerPositionForItem = adapter.getHeaderPositionForItem(topChildPosition); - if (headerPositionForItem != currentStickyPosition - && headerPositionForItem != RecyclerView.NO_POSITION) { - adapter.onBindHeaderViewHolder(currentStickyHolder, headerPositionForItem); - currentStickyPosition = headerPositionForItem; - } else if (headerPositionForItem != RecyclerView.NO_POSITION) { - adapter.onBindHeaderViewHolder(currentStickyHolder, headerPositionForItem); - } - } - - private void drawHeader(Canvas c) { - c.save(); - c.translate(0, 0); - currentStickyHolder.itemView.draw(c); - c.restore(); - } - - private void moveHeader(Canvas c, View nextHeader) { - c.save(); - c.translate(0, nextHeader.getTop() - nextHeader.getHeight()); - currentStickyHolder.itemView.draw(c); - c.restore(); - } - - private View getChildInContact(RecyclerView parent, int contactPoint) { - View childInContact = null; - for (int i = 0; i < parent.getChildCount(); i++) { - View child = parent.getChildAt(i); - if (child.getBottom() > contactPoint) { - if (child.getTop() <= contactPoint) { - // This child overlaps the contactPoint - childInContact = child; - break; - } - } - } - return childInContact; - } - - private void fixLayoutSize() { - recyclerView - .getViewTreeObserver() - .addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - - @Override - public void onGlobalLayout() { - recyclerView - .getViewTreeObserver() - .removeOnGlobalLayoutListener(this); - // Specs for parent (RecyclerView) - int widthSpec = - View.MeasureSpec.makeMeasureSpec( - recyclerView.getWidth(), View.MeasureSpec.EXACTLY); - int heightSpec = - View.MeasureSpec.makeMeasureSpec( - recyclerView.getHeight(), - View.MeasureSpec.UNSPECIFIED); - - // Specs for children (headers) - int childWidthSpec = - ViewGroup.getChildMeasureSpec( - widthSpec, - recyclerView.getPaddingLeft() - + recyclerView.getPaddingRight(), - currentStickyHolder.itemView.getLayoutParams() - .width); - int childHeightSpec = - ViewGroup.getChildMeasureSpec( - heightSpec, - recyclerView.getPaddingTop() - + recyclerView.getPaddingBottom(), - currentStickyHolder.itemView.getLayoutParams() - .height); - - currentStickyHolder.itemView.measure( - childWidthSpec, childHeightSpec); - - currentStickyHolder.itemView.layout( - 0, - 0, - currentStickyHolder.itemView.getMeasuredWidth(), - currentStickyHolder.itemView.getMeasuredHeight()); - stickyHolderHight = - currentStickyHolder.itemView.getMeasuredHeight(); - } - }); - } -} diff --git a/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyHeaderItemDecorator.kt b/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyHeaderItemDecorator.kt new file mode 100644 index 0000000..ec054da --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/ui/stickyheader/StickyHeaderItemDecorator.kt @@ -0,0 +1,185 @@ +package tw.y_studio.ptt.ui.stickyheader + +import android.graphics.Canvas +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +class StickyHeaderItemDecorator( + private val adapter: StickyAdapter +) : ItemDecoration() { + + private var currentStickyPosition = RecyclerView.NO_POSITION + private var recyclerView: RecyclerView? = null + private var currentStickyHolder: RecyclerView.ViewHolder? = null + private var lastViewOverlappedByHeader: View? = null + + var stickyHolderHeight = 0 + + fun attachToRecyclerView(recyclerView: RecyclerView?) { + if (this.recyclerView === recyclerView) { + return + } + if (this.recyclerView != null) { + destroyCallbacks(this.recyclerView) + } + this.recyclerView = recyclerView + if (recyclerView != null) { + currentStickyHolder = adapter.onCreateHeaderViewHolder(recyclerView) + fixLayoutSize() + setupCallbacks() + } + } + + private fun setupCallbacks() { + recyclerView?.addItemDecoration(this) + } + + private fun destroyCallbacks(recyclerView: RecyclerView?) { + recyclerView?.removeItemDecoration(this) + } + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(c, parent, state) + val layoutManager = parent.layoutManager ?: return + var topChildPosition = RecyclerView.NO_POSITION + if (layoutManager is LinearLayoutManager) { + topChildPosition = layoutManager.findFirstVisibleItemPosition() + } else { + val topChild = parent.getChildAt(0) + if (topChild != null) { + topChildPosition = parent.getChildAdapterPosition(topChild) + } + } + if (topChildPosition == RecyclerView.NO_POSITION) { + return + } + + val viewOverlappedByHeader = getChildInContact(parent, currentStickyHolder!!.itemView.bottom) + lastViewOverlappedByHeader = + viewOverlappedByHeader ?: lastViewOverlappedByHeader ?: parent.getChildAt(topChildPosition) + val overlappedByHeaderPosition = parent.getChildAdapterPosition(viewOverlappedByHeader!!) + val overlappedHeaderPosition: Int + val preOverlappedPosition: Int + if (overlappedByHeaderPosition > 0) { + preOverlappedPosition = adapter.getHeaderPositionForItem(overlappedByHeaderPosition - 1) + overlappedHeaderPosition = adapter.getHeaderPositionForItem(overlappedByHeaderPosition) + } else { + preOverlappedPosition = adapter.getHeaderPositionForItem(topChildPosition) + overlappedHeaderPosition = preOverlappedPosition + } + if (preOverlappedPosition == RecyclerView.NO_POSITION) { + return + } + if (preOverlappedPosition != overlappedHeaderPosition && shouldMoveHeader(viewOverlappedByHeader)) { + updateStickyHeader(topChildPosition, overlappedByHeaderPosition) + moveHeader(c, viewOverlappedByHeader) + } else { + updateStickyHeader(topChildPosition, RecyclerView.NO_POSITION) + drawHeader(c) + } + } + + // shouldMoveHeader returns the sticky header should move or not. + // This method is for avoiding sinking/departing the sticky header into/from top of screen + private fun shouldMoveHeader(viewOverlappedByHeader: View?): Boolean { + return viewOverlappedByHeader?.let { + val dy = it.top - it.height + it.top >= 0 && dy <= 0 + } ?: false + } + + private fun updateStickyHeader( + topChildPosition: Int, + @Suppress("UNUSED_PARAMETER") contactChildPosition: Int + ) { + val headerPositionForItem = adapter.getHeaderPositionForItem(topChildPosition) + if (headerPositionForItem != currentStickyPosition && headerPositionForItem != RecyclerView.NO_POSITION) { + adapter.onBindHeaderViewHolder(currentStickyHolder, headerPositionForItem) + currentStickyPosition = headerPositionForItem + } else if (headerPositionForItem != RecyclerView.NO_POSITION) { + adapter.onBindHeaderViewHolder(currentStickyHolder, headerPositionForItem) + } + } + + private fun drawHeader(c: Canvas) { + currentStickyHolder?.let { viewHolder -> + c.save() + c.translate(0f, 0f) + viewHolder.itemView.draw(c) + c.restore() + } + } + + private fun moveHeader(c: Canvas, nextHeader: View) { + currentStickyHolder?.let { viewHolder -> + c.save() + c.translate(0f, (nextHeader.top - nextHeader.height).toFloat()) + viewHolder.itemView.draw(c) + c.restore() + } + } + + private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? { + var childInContact: View? = null + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + if (child.bottom > contactPoint) { + if (child.top <= contactPoint) { + // This child overlaps the contactPoint + childInContact = child + break + } + } + } + return childInContact + } + + private fun fixLayoutSize() { + recyclerView + ?.viewTreeObserver + ?.addOnGlobalLayoutListener( + object : OnGlobalLayoutListener { + override fun onGlobalLayout() { + recyclerView!! + .viewTreeObserver + .removeOnGlobalLayoutListener(this) + // Specs for parent (RecyclerView) + val widthSpec = View.MeasureSpec.makeMeasureSpec( + recyclerView!!.width, View.MeasureSpec.EXACTLY + ) + val heightSpec = View.MeasureSpec.makeMeasureSpec( + recyclerView!!.height, + View.MeasureSpec.UNSPECIFIED + ) + + // Specs for children (headers) + val childWidthSpec = ViewGroup.getChildMeasureSpec( + widthSpec, + recyclerView!!.paddingLeft + + recyclerView!!.paddingRight, + currentStickyHolder!!.itemView.layoutParams.width + ) + val childHeightSpec = ViewGroup.getChildMeasureSpec( + heightSpec, + recyclerView!!.paddingTop + + recyclerView!!.paddingBottom, + currentStickyHolder!!.itemView.layoutParams.height + ) + currentStickyHolder!!.itemView.measure( + childWidthSpec, childHeightSpec + ) + currentStickyHolder!!.itemView.layout( + 0, + 0, + currentStickyHolder!!.itemView.measuredWidth, + currentStickyHolder!!.itemView.measuredHeight + ) + stickyHolderHeight = currentStickyHolder!!.itemView.measuredHeight + } + }) + } +}