| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.calendar |
| |
| import android.content.Context |
| import android.graphics.Color |
| import android.util.AttributeSet |
| import android.view.Gravity |
| import android.view.View |
| import android.view.ViewGroup |
| import android.widget.AbsListView |
| import android.widget.AbsListView.OnScrollListener |
| import android.widget.Adapter |
| import android.widget.FrameLayout |
| import android.widget.ListView |
| |
| /** |
| * Implements a ListView class with a sticky header at the top. The header is |
| * per section and it is pinned to the top as long as its section is at the top |
| * of the view. If it is not, the header slides up or down (depending on the |
| * scroll movement) and the header of the current section slides to the top. |
| * Notes: |
| * 1. The class uses the first available child ListView as the working |
| * ListView. If no ListView child exists, the class will create a default one. |
| * 2. The ListView's adapter must be passed to this class using the 'setAdapter' |
| * method. The adapter must implement the HeaderIndexer interface. If no adapter |
| * is specified, the class will try to extract it from the ListView |
| * 3. The class registers itself as a listener to scroll events (OnScrollListener), if the |
| * ListView needs to receive scroll events, it must register its listener using |
| * this class' setOnScrollListener method. |
| * 4. Headers for the list view must be added before using the StickyHeaderListView |
| * 5. The implementation should register to listen to dataset changes. Right now this is not done |
| * since a change the dataset in a listview forces a call to OnScroll. The needed code is |
| * commented out. |
| */ |
| class StickyHeaderListView(context: Context, attrs: AttributeSet?) : |
| FrameLayout(context, attrs), OnScrollListener { |
| protected var mChildViewsCreated = false |
| protected var mDoHeaderReset = false |
| protected var mContext: Context? = null |
| protected var mAdapter: Adapter? = null |
| protected var mIndexer: HeaderIndexer? = null |
| protected var mHeaderHeightListener: HeaderHeightListener? = null |
| protected var mStickyHeader: View? = null |
| // A invisible header used when a section has no header |
| protected var mNonessentialHeader: View? = null |
| protected var mListView: ListView? = null |
| protected var mListener: AbsListView.OnScrollListener? = null |
| private var mSeparatorWidth = 0 |
| private var mSeparatorView: View? = null |
| private var mLastStickyHeaderHeight = 0 |
| |
| // This code is needed only if dataset changes do not force a call to OnScroll |
| // protected DataSetObserver mListDataObserver = null; |
| protected var mCurrentSectionPos = -1 // Position of section that has its header on the |
| |
| // top of the view |
| protected var mNextSectionPosition = -1 // Position of next section's header |
| protected var mListViewHeadersCount = 0 |
| |
| /** |
| * Interface that must be implemented by the ListView adapter to provide headers locations |
| * and number of items under each header. |
| * |
| */ |
| interface HeaderIndexer { |
| /** |
| * Calculates the position of the header of a specific item in the adapter's data set. |
| * For example: Assuming you have a list with albums and songs names: |
| * Album A, song 1, song 2, ...., song 10, Album B, song 1, ..., song 7. A call to |
| * this method with the position of song 5 in Album B, should return the position |
| * of Album B. |
| * @param position - Position of the item in the ListView dataset |
| * @return Position of header. -1 if the is no header |
| */ |
| fun getHeaderPositionFromItemPosition(position: Int): Int |
| |
| /** |
| * Calculates the number of items in the section defined by the header (not including |
| * the header). |
| * For example: A list with albums and songs, the method should return |
| * the number of songs names (without the album name). |
| * |
| * @param headerPosition - the value returned by 'getHeaderPositionFromItemPosition' |
| * @return Number of items. -1 on error. |
| */ |
| fun getHeaderItemsNumber(headerPosition: Int): Int |
| } |
| |
| /*** |
| * |
| * Interface that is used to update the sticky header's height |
| * |
| */ |
| interface HeaderHeightListener { |
| /*** |
| * Updated a change in the sticky header's size |
| * |
| * @param height - new height of sticky header |
| */ |
| fun OnHeaderHeightChanged(height: Int) |
| } |
| |
| /** |
| * Sets the adapter to be used by the class to get views of headers |
| * |
| * @param adapter - The adapter. |
| */ |
| fun setAdapter(adapter: Adapter?) { |
| // This code is needed only if dataset changes do not force a call to |
| // OnScroll |
| // if (mAdapter != null && mListDataObserver != null) { |
| // mAdapter.unregisterDataSetObserver(mListDataObserver); |
| // } |
| if (adapter != null) { |
| mAdapter = adapter |
| // This code is needed only if dataset changes do not force a call |
| // to OnScroll |
| // mAdapter.registerDataSetObserver(mListDataObserver); |
| } |
| } |
| |
| /** |
| * Sets the indexer object (that implements the HeaderIndexer interface). |
| * |
| * @param indexer - The indexer. |
| */ |
| fun setIndexer(indexer: HeaderIndexer?) { |
| mIndexer = indexer |
| } |
| |
| /** |
| * Sets the list view that is displayed |
| * @param lv - The list view. |
| */ |
| fun setListView(lv: ListView?) { |
| mListView = lv |
| mListView?.setOnScrollListener(this) |
| mListViewHeadersCount = mListView?.getHeaderViewsCount() as Int |
| } |
| |
| /** |
| * Sets an external OnScroll listener. Since the StickyHeaderListView sets |
| * itself as the scroll events listener of the listview, this method allows |
| * the user to register another listener that will be called after this |
| * class listener is called. |
| * |
| * @param listener - The external listener. |
| */ |
| fun setOnScrollListener(listener: AbsListView.OnScrollListener?) { |
| mListener = listener |
| } |
| |
| fun setHeaderHeightListener(listener: HeaderHeightListener?) { |
| mHeaderHeightListener = listener |
| } |
| |
| /** |
| * Scroll status changes listener |
| * |
| * @param view - the scrolled view |
| * @param scrollState - new scroll state. |
| */ |
| @Override |
| override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { |
| if (mListener != null) { |
| mListener?.onScrollStateChanged(view, scrollState) |
| } |
| } |
| |
| /** |
| * Scroll events listener |
| * |
| * @param view - the scrolled view |
| * @param firstVisibleItem - the index (in the list's adapter) of the top |
| * visible item. |
| * @param visibleItemCount - the number of visible items in the list |
| * @param totalItemCount - the total number items in the list |
| */ |
| @Override |
| override fun onScroll( |
| view: AbsListView?, |
| firstVisibleItem: Int, |
| visibleItemCount: Int, |
| totalItemCount: Int |
| ) { |
| updateStickyHeader(firstVisibleItem) |
| if (mListener != null) { |
| mListener?.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount) |
| } |
| } |
| |
| /** |
| * Sets a separator below the sticky header, which will be visible while the sticky header |
| * is not scrolling up. |
| * @param color - color of separator |
| * @param width - width in pixels of separator |
| */ |
| fun setHeaderSeparator(color: Int, width: Int) { |
| mSeparatorView = View(mContext) |
| val params: ViewGroup.LayoutParams = LayoutParams( |
| LayoutParams.MATCH_PARENT, |
| width, Gravity.TOP |
| ) |
| mSeparatorView?.setLayoutParams(params) |
| mSeparatorView?.setBackgroundColor(color) |
| mSeparatorWidth = width |
| this.addView(mSeparatorView) |
| } |
| |
| protected fun updateStickyHeader(firstVisibleItemInput: Int) { |
| // Try to make sure we have an adapter to work with (may not succeed). |
| var firstVisibleItem = firstVisibleItemInput |
| if (mAdapter == null && mListView != null) { |
| setAdapter(mListView?.getAdapter()) |
| } |
| firstVisibleItem -= mListViewHeadersCount |
| if (mAdapter != null && mIndexer != null && mDoHeaderReset) { |
| |
| // Get the section header position |
| var sectionSize = 0 |
| val sectionPos = mIndexer!!.getHeaderPositionFromItemPosition(firstVisibleItem) |
| |
| // New section - set it in the header view |
| var newView = false |
| if (sectionPos != mCurrentSectionPos) { |
| |
| // No header for current position , use the nonessential invisible one, |
| // hide the separator |
| if (sectionPos == -1) { |
| sectionSize = 0 |
| this.removeView(mStickyHeader) |
| mStickyHeader = mNonessentialHeader |
| if (mSeparatorView != null) { |
| mSeparatorView?.setVisibility(View.GONE) |
| } |
| newView = true |
| } else { |
| // Create a copy of the header view to show on top |
| sectionSize = mIndexer!!.getHeaderItemsNumber(sectionPos) |
| val v: View? = |
| mAdapter?.getView(sectionPos + mListViewHeadersCount, null, mListView) |
| v?.measure( |
| MeasureSpec.makeMeasureSpec( |
| mListView?.getWidth() as Int, |
| MeasureSpec.EXACTLY |
| ), MeasureSpec.makeMeasureSpec( |
| mListView?.getHeight() as Int, |
| MeasureSpec.AT_MOST |
| ) |
| ) |
| this.removeView(mStickyHeader) |
| mStickyHeader = v |
| newView = true |
| } |
| mCurrentSectionPos = sectionPos |
| mNextSectionPosition = sectionSize + sectionPos + 1 |
| } |
| |
| // Do transitions |
| // If position of bottom of last item in a section is smaller than the height of the |
| // sticky header - shift drawable of header. |
| if (mStickyHeader != null) { |
| val sectionLastItemPosition = mNextSectionPosition - firstVisibleItem - 1 |
| var stickyHeaderHeight: Int = mStickyHeader?.getHeight() as Int |
| if (stickyHeaderHeight == 0) { |
| stickyHeaderHeight = mStickyHeader?.getMeasuredHeight() as Int |
| } |
| |
| // Update new header height |
| if (mHeaderHeightListener != null && |
| mLastStickyHeaderHeight != stickyHeaderHeight |
| ) { |
| mLastStickyHeaderHeight = stickyHeaderHeight |
| mHeaderHeightListener!!.OnHeaderHeightChanged(stickyHeaderHeight) |
| } |
| val SectionLastView: View? = mListView?.getChildAt(sectionLastItemPosition) |
| if (SectionLastView != null && SectionLastView.getBottom() <= stickyHeaderHeight) { |
| val lastViewBottom: Int = SectionLastView.getBottom() |
| mStickyHeader?.setTranslationY(lastViewBottom.toFloat() - |
| stickyHeaderHeight.toFloat()) |
| if (mSeparatorView != null) { |
| mSeparatorView?.setVisibility(View.GONE) |
| } |
| } else if (stickyHeaderHeight != 0) { |
| mStickyHeader?.setTranslationY(0f) |
| if (mSeparatorView != null && |
| mStickyHeader?.equals(mNonessentialHeader) == false) { |
| mSeparatorView?.setVisibility(View.VISIBLE) |
| } |
| } |
| if (newView) { |
| mStickyHeader?.setVisibility(View.INVISIBLE) |
| this.addView(mStickyHeader) |
| if (mSeparatorView != null && |
| mStickyHeader?.equals(mNonessentialHeader) == false) { |
| val params: FrameLayout.LayoutParams = LayoutParams( |
| LayoutParams.MATCH_PARENT, |
| mSeparatorWidth |
| ) |
| params.setMargins(0, mStickyHeader?.getMeasuredHeight() as Int, 0, 0) |
| mSeparatorView?.setLayoutParams(params) |
| mSeparatorView?.setVisibility(View.VISIBLE) |
| } |
| mStickyHeader?.setVisibility(View.VISIBLE) |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected override fun onFinishInflate() { |
| super.onFinishInflate() |
| if (!mChildViewsCreated) { |
| setChildViews() |
| } |
| mDoHeaderReset = true |
| } |
| |
| @Override |
| protected override fun onAttachedToWindow() { |
| super.onAttachedToWindow() |
| if (!mChildViewsCreated) { |
| setChildViews() |
| } |
| mDoHeaderReset = true |
| } |
| |
| // Resets the sticky header when the adapter data set was changed |
| // This code is needed only if dataset changes do not force a call to OnScroll |
| // protected void onDataChanged() { |
| // Should do a call to updateStickyHeader if needed |
| // } |
| private fun setChildViews() { |
| // Find a child ListView (if any) |
| val iChildNum: Int = getChildCount() |
| for (i in 0 until iChildNum) { |
| val v: Object = getChildAt(i) as Object |
| if (v is ListView) { |
| setListView(v as ListView) |
| } |
| } |
| |
| // No child ListView - add one |
| if (mListView == null) { |
| setListView(ListView(mContext)) |
| } |
| |
| // Create a nonessential view , it will be used in case a section has no header |
| mNonessentialHeader = View(mContext) |
| val params: ViewGroup.LayoutParams = LayoutParams( |
| LayoutParams.MATCH_PARENT, |
| 1, Gravity.TOP |
| ) |
| mNonessentialHeader?.setLayoutParams(params) |
| mNonessentialHeader?.setBackgroundColor(Color.TRANSPARENT) |
| mChildViewsCreated = true |
| } |
| |
| companion object { |
| private const val TAG = "StickyHeaderListView" |
| } |
| |
| /** |
| * Constructor |
| * |
| * @param context - application context. |
| * @param attrs - layout attributes. |
| */ |
| init { |
| mContext = context |
| } |
| } |