filipkowicz / HeaderItemDecorationExample

Simple Example how to use HeaderItemDecoration from gist https://gist.github.com/filipkowicz/1a769001fae407b8813ab4387c42fcbd
14 stars 5 forks source link

Animation updates #5

Open CFranksy opened 2 years ago

CFranksy commented 2 years ago

Hi Michal, really like your stuff here!

I tried adapting your code to my situation. I have a recycler view with expanding sections.

When expanding the sections the Item Decoration would take the title of the bottom header so I had to modify your code a little to get this to work.

To start with I had to change your DrawOver Method to use childAt(0) but then found I didn't need it what so ever and ended up removing:

//val topChild = parent.getChildAt(0) ?: return
val topChild = parent.findChildViewUnder(
    parent.paddingLeft.toFloat(),
    parent.paddingTop.toFloat() /*+ (currentHeader?.second?.itemView?.height ?: 0 )*/
) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
    return
}

I had to make a change to 'getHeaderPositionForItem' and changed it to:

private fun getHeaderPositionForItem(recyclerView: RecyclerView): Int {
        var headerPosition = RecyclerView.NO_POSITION
        var currentPosition: Int = when (recyclerView.layoutManager) {
            is LinearLayoutManager -> {
                (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
            }
            is GridLayoutManager -> {
                (recyclerView.layoutManager as GridLayoutManager).findFirstVisibleItemPosition()
            }
            else -> {
                0
            }
        }
        do {
            if (isHeader(currentPosition)) {
                headerPosition = currentPosition
                break
            }
            currentPosition -= 1
        } while (0 <= currentPosition && currentPosition <= recyclerView.adapter!!.itemCount)

        return headerPosition
    }

It's not 100% polished as my designer changed his mind about having sticky headers but there are some possible changes there that might help you to support more possible implementations of your work. I'll attach the whole file for better clarity of the chances. Great work tho! I really liked this :)

import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

/*
Sticky header implementation based of off the following git repo
https://github.com/filipkowicz/HeaderItemDecorationExample

Credit to Michal Filipek - filipkowicz
 */

class StickyHeaderItemDecoration(
    recyclerView: RecyclerView,
    private val isHeader: (itemPosition: Int) -> Boolean,
    private val listener: OnEventListener
) : RecyclerView.ItemDecoration() {

    interface OnEventListener {
        fun onStickyHeaderClicked(currentHeader: RecyclerView.ViewHolder?, motionEvent: MotionEvent)
    }

    private var currentHeader: Pair<Int, RecyclerView.ViewHolder>? = null

    init {
        recyclerView.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
            override fun onChanged() {
                // clear saved header as it can be outdated now
                currentHeader = null
            }
        })

        recyclerView.doOnEachNextLayout {
            // clear saved layout as it may need layout update
            currentHeader = null
        }
        // handle click on sticky header
        recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
            override fun onInterceptTouchEvent(
                recyclerView: RecyclerView,
                motionEvent: MotionEvent
            ): Boolean {
                return if (motionEvent.action == MotionEvent.ACTION_DOWN) {
                    if (motionEvent.y <= currentHeader?.second?.itemView?.bottom ?: 0) {
                        //header touched
                        listener.onStickyHeaderClicked(currentHeader?.second, motionEvent)
                        return true
                    } else false
                } else false
            }
        })
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        val headerView = getHeaderViewForItem(parent) ?: return

        val contactPoint = headerView.bottom + parent.paddingTop
        val childInContact = getChildInContact(parent, contactPoint) ?: return

        if (isHeader(parent.getChildAdapterPosition(childInContact))) {
            moveHeader(c, headerView, childInContact, parent.paddingTop)
            return
        }

        drawHeader(c, headerView, parent.paddingTop)
    }

    private fun getHeaderViewForItem(parent: RecyclerView): View? {
        if (parent.adapter == null) {
            return null
        }
        val headerPosition = getHeaderPositionForItem(parent)
        if (headerPosition == RecyclerView.NO_POSITION) return null
        val headerType = parent.adapter?.getItemViewType(headerPosition) ?: return null
        // if match reuse viewHolder
        if (currentHeader?.first == headerPosition && currentHeader?.second?.itemViewType == headerType) {
            return currentHeader?.second?.itemView
        }

        val headerHolder = parent.adapter?.createViewHolder(parent, headerType)
        if (headerHolder != null) {
            parent.adapter?.onBindViewHolder(headerHolder, headerPosition)
            fixLayoutSize(parent, headerHolder.itemView)
            // save for next draw
            currentHeader = headerPosition to headerHolder
        }
        return headerHolder?.itemView
    }

    private fun drawHeader(c: Canvas, header: View, paddingTop: Int) {
        c.save()

        //Enable to debug view
//        c.saveLayerAlpha(RectF(0f, 0f, c.width.toFloat(), c.height.toFloat()), 150)

        c.translate(0f, paddingTop.toFloat())
        header.draw(c)
        c.restore()
    }

    private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View, paddingTop: Int) {
        c.save()
        c.clipRect(0, paddingTop, c.width, paddingTop + currentHeader.height)

        //Enable to debug view
//        c.saveLayerAlpha(RectF(0f, 0f, c.width.toFloat(), c.height.toFloat()), 150)

        c.translate(0f, (nextHeader.top - currentHeader.height).toFloat() /*+ paddingTop*/)
        currentHeader.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)
            val mBounds = Rect()
            parent.getDecoratedBoundsWithMargins(child, mBounds)
            if (mBounds.bottom > contactPoint) {
                if (mBounds.top <= contactPoint) {
                    // This child overlaps the contactPoint
                    childInContact = child
                    break
                }
            }
        }
        return childInContact
    }

    /**
     * Properly measures and layouts the top sticky header.
     *
     * @param parent ViewGroup: RecyclerView in this case.
     */
    private fun fixLayoutSize(parent: ViewGroup, view: View) {
        // Specs for parent (RecyclerView)
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
        val heightSpec =
            View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)

        // Specs for children (headers)
        val childWidthSpec = ViewGroup.getChildMeasureSpec(
            widthSpec,
            parent.paddingLeft + parent.paddingRight,
            view.layoutParams.width
        )
        val childHeightSpec = ViewGroup.getChildMeasureSpec(
            heightSpec,
            parent.paddingTop + parent.paddingBottom,
            view.layoutParams.height
        )

        view.measure(childWidthSpec, childHeightSpec)
        view.layout(0, 0, view.measuredWidth, view.measuredHeight)
    }

    private fun getHeaderPositionForItem(recyclerView: RecyclerView): Int {
        var headerPosition = RecyclerView.NO_POSITION
        var currentPosition: Int = when (recyclerView.layoutManager) {
            is LinearLayoutManager -> {
                (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
            }
            is GridLayoutManager -> {
                (recyclerView.layoutManager as GridLayoutManager).findFirstVisibleItemPosition()
            }
            else -> {
                0
            }
        }
        do {
            if (isHeader(currentPosition)) {
                headerPosition = currentPosition
                break
            }
            currentPosition -= 1
        } while (0 <= currentPosition && currentPosition <= recyclerView.adapter!!.itemCount)

        return headerPosition
    }
}

private inline fun View.doOnEachNextLayout(crossinline action: (view: View) -> Unit) {
    addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
        action(
            view
        )
    }
}

Fragment:

class BookmarkListFragment : CoreBaseFragment(R.layout.fragment_bookmark_list),
    BookmarkListAdapter.Listener,
    GSSwipeToDeleteTouchHelper.OnEventListener,
    StickyHeaderItemDecoration.OnEventListener
{

private fun initUI() {
recyclerView.addItemDecoration(StickyHeaderItemDecoration(
            recyclerView,
            isHeader = this.adapter!!::isStickyHeader,
            listener = this
        ))
}

override fun onStickyHeaderClicked(currentHeader: RecyclerView.ViewHolder?, motionEvent: MotionEvent) {
        if (currentHeader == null) return

       //showMoreView is a textView within the Header Layout
        if (currentHeader is recyclerAdapter.HeaderViewHolder && motionEvent.x > currentHeader.itemView.showMoreView.x) {
            currentHeader.itemView.showMoreView.performClick()
        }
    }
}

Adapter

fun isStickyHeader(position: Int): Boolean {
        return getItemViewType(position) == HEADER
    }

    companion object {
        private const val HEADER = 0
        private const val CARDVIEW = 1
    }