1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.calendar 17 18 import android.content.Context 19 import android.graphics.Color 20 import android.util.AttributeSet 21 import android.view.Gravity 22 import android.view.View 23 import android.view.ViewGroup 24 import android.widget.AbsListView 25 import android.widget.AbsListView.OnScrollListener 26 import android.widget.Adapter 27 import android.widget.FrameLayout 28 import android.widget.ListView 29 30 /** 31 * Implements a ListView class with a sticky header at the top. The header is 32 * per section and it is pinned to the top as long as its section is at the top 33 * of the view. If it is not, the header slides up or down (depending on the 34 * scroll movement) and the header of the current section slides to the top. 35 * Notes: 36 * 1. The class uses the first available child ListView as the working 37 * ListView. If no ListView child exists, the class will create a default one. 38 * 2. The ListView's adapter must be passed to this class using the 'setAdapter' 39 * method. The adapter must implement the HeaderIndexer interface. If no adapter 40 * is specified, the class will try to extract it from the ListView 41 * 3. The class registers itself as a listener to scroll events (OnScrollListener), if the 42 * ListView needs to receive scroll events, it must register its listener using 43 * this class' setOnScrollListener method. 44 * 4. Headers for the list view must be added before using the StickyHeaderListView 45 * 5. The implementation should register to listen to dataset changes. Right now this is not done 46 * since a change the dataset in a listview forces a call to OnScroll. The needed code is 47 * commented out. 48 */ 49 class StickyHeaderListView(context: Context, attrs: AttributeSet?) : 50 FrameLayout(context, attrs), OnScrollListener { 51 protected var mChildViewsCreated = false 52 protected var mDoHeaderReset = false 53 protected var mContext: Context? = null 54 protected var mAdapter: Adapter? = null 55 protected var mIndexer: HeaderIndexer? = null 56 protected var mHeaderHeightListener: HeaderHeightListener? = null 57 protected var mStickyHeader: View? = null 58 // A invisible header used when a section has no header 59 protected var mNonessentialHeader: View? = null 60 protected var mListView: ListView? = null 61 protected var mListener: AbsListView.OnScrollListener? = null 62 private var mSeparatorWidth = 0 63 private var mSeparatorView: View? = null 64 private var mLastStickyHeaderHeight = 0 65 66 // This code is needed only if dataset changes do not force a call to OnScroll 67 // protected DataSetObserver mListDataObserver = null; 68 protected var mCurrentSectionPos = -1 // Position of section that has its header on the 69 70 // top of the view 71 protected var mNextSectionPosition = -1 // Position of next section's header 72 protected var mListViewHeadersCount = 0 73 74 /** 75 * Interface that must be implemented by the ListView adapter to provide headers locations 76 * and number of items under each header. 77 * 78 */ 79 interface HeaderIndexer { 80 /** 81 * Calculates the position of the header of a specific item in the adapter's data set. 82 * For example: Assuming you have a list with albums and songs names: 83 * Album A, song 1, song 2, ...., song 10, Album B, song 1, ..., song 7. A call to 84 * this method with the position of song 5 in Album B, should return the position 85 * of Album B. 86 * @param position - Position of the item in the ListView dataset 87 * @return Position of header. -1 if the is no header 88 */ getHeaderPositionFromItemPositionnull89 fun getHeaderPositionFromItemPosition(position: Int): Int 90 91 /** 92 * Calculates the number of items in the section defined by the header (not including 93 * the header). 94 * For example: A list with albums and songs, the method should return 95 * the number of songs names (without the album name). 96 * 97 * @param headerPosition - the value returned by 'getHeaderPositionFromItemPosition' 98 * @return Number of items. -1 on error. 99 */ 100 fun getHeaderItemsNumber(headerPosition: Int): Int 101 } 102 103 /*** 104 * 105 * Interface that is used to update the sticky header's height 106 * 107 */ 108 interface HeaderHeightListener { 109 /*** 110 * Updated a change in the sticky header's size 111 * 112 * @param height - new height of sticky header 113 */ 114 fun OnHeaderHeightChanged(height: Int) 115 } 116 117 /** 118 * Sets the adapter to be used by the class to get views of headers 119 * 120 * @param adapter - The adapter. 121 */ setAdapternull122 fun setAdapter(adapter: Adapter?) { 123 // This code is needed only if dataset changes do not force a call to 124 // OnScroll 125 // if (mAdapter != null && mListDataObserver != null) { 126 // mAdapter.unregisterDataSetObserver(mListDataObserver); 127 // } 128 if (adapter != null) { 129 mAdapter = adapter 130 // This code is needed only if dataset changes do not force a call 131 // to OnScroll 132 // mAdapter.registerDataSetObserver(mListDataObserver); 133 } 134 } 135 136 /** 137 * Sets the indexer object (that implements the HeaderIndexer interface). 138 * 139 * @param indexer - The indexer. 140 */ setIndexernull141 fun setIndexer(indexer: HeaderIndexer?) { 142 mIndexer = indexer 143 } 144 145 /** 146 * Sets the list view that is displayed 147 * @param lv - The list view. 148 */ setListViewnull149 fun setListView(lv: ListView?) { 150 mListView = lv 151 mListView?.setOnScrollListener(this) 152 mListViewHeadersCount = mListView?.getHeaderViewsCount() as Int 153 } 154 155 /** 156 * Sets an external OnScroll listener. Since the StickyHeaderListView sets 157 * itself as the scroll events listener of the listview, this method allows 158 * the user to register another listener that will be called after this 159 * class listener is called. 160 * 161 * @param listener - The external listener. 162 */ setOnScrollListenernull163 fun setOnScrollListener(listener: AbsListView.OnScrollListener?) { 164 mListener = listener 165 } 166 setHeaderHeightListenernull167 fun setHeaderHeightListener(listener: HeaderHeightListener?) { 168 mHeaderHeightListener = listener 169 } 170 171 /** 172 * Scroll status changes listener 173 * 174 * @param view - the scrolled view 175 * @param scrollState - new scroll state. 176 */ 177 @Override onScrollStateChangednull178 override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) { 179 if (mListener != null) { 180 mListener?.onScrollStateChanged(view, scrollState) 181 } 182 } 183 184 /** 185 * Scroll events listener 186 * 187 * @param view - the scrolled view 188 * @param firstVisibleItem - the index (in the list's adapter) of the top 189 * visible item. 190 * @param visibleItemCount - the number of visible items in the list 191 * @param totalItemCount - the total number items in the list 192 */ 193 @Override onScrollnull194 override fun onScroll( 195 view: AbsListView?, 196 firstVisibleItem: Int, 197 visibleItemCount: Int, 198 totalItemCount: Int 199 ) { 200 updateStickyHeader(firstVisibleItem) 201 if (mListener != null) { 202 mListener?.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount) 203 } 204 } 205 206 /** 207 * Sets a separator below the sticky header, which will be visible while the sticky header 208 * is not scrolling up. 209 * @param color - color of separator 210 * @param width - width in pixels of separator 211 */ setHeaderSeparatornull212 fun setHeaderSeparator(color: Int, width: Int) { 213 mSeparatorView = View(mContext) 214 val params: ViewGroup.LayoutParams = LayoutParams( 215 LayoutParams.MATCH_PARENT, 216 width, Gravity.TOP 217 ) 218 mSeparatorView?.setLayoutParams(params) 219 mSeparatorView?.setBackgroundColor(color) 220 mSeparatorWidth = width 221 this.addView(mSeparatorView) 222 } 223 updateStickyHeadernull224 protected fun updateStickyHeader(firstVisibleItemInput: Int) { 225 // Try to make sure we have an adapter to work with (may not succeed). 226 var firstVisibleItem = firstVisibleItemInput 227 if (mAdapter == null && mListView != null) { 228 setAdapter(mListView?.getAdapter()) 229 } 230 firstVisibleItem -= mListViewHeadersCount 231 if (mAdapter != null && mIndexer != null && mDoHeaderReset) { 232 233 // Get the section header position 234 var sectionSize = 0 235 val sectionPos = mIndexer!!.getHeaderPositionFromItemPosition(firstVisibleItem) 236 237 // New section - set it in the header view 238 var newView = false 239 if (sectionPos != mCurrentSectionPos) { 240 241 // No header for current position , use the nonessential invisible one, 242 // hide the separator 243 if (sectionPos == -1) { 244 sectionSize = 0 245 this.removeView(mStickyHeader) 246 mStickyHeader = mNonessentialHeader 247 if (mSeparatorView != null) { 248 mSeparatorView?.setVisibility(View.GONE) 249 } 250 newView = true 251 } else { 252 // Create a copy of the header view to show on top 253 sectionSize = mIndexer!!.getHeaderItemsNumber(sectionPos) 254 val v: View? = 255 mAdapter?.getView(sectionPos + mListViewHeadersCount, null, mListView) 256 v?.measure( 257 MeasureSpec.makeMeasureSpec( 258 mListView?.getWidth() as Int, 259 MeasureSpec.EXACTLY 260 ), MeasureSpec.makeMeasureSpec( 261 mListView?.getHeight() as Int, 262 MeasureSpec.AT_MOST 263 ) 264 ) 265 this.removeView(mStickyHeader) 266 mStickyHeader = v 267 newView = true 268 } 269 mCurrentSectionPos = sectionPos 270 mNextSectionPosition = sectionSize + sectionPos + 1 271 } 272 273 // Do transitions 274 // If position of bottom of last item in a section is smaller than the height of the 275 // sticky header - shift drawable of header. 276 if (mStickyHeader != null) { 277 val sectionLastItemPosition = mNextSectionPosition - firstVisibleItem - 1 278 var stickyHeaderHeight: Int = mStickyHeader?.getHeight() as Int 279 if (stickyHeaderHeight == 0) { 280 stickyHeaderHeight = mStickyHeader?.getMeasuredHeight() as Int 281 } 282 283 // Update new header height 284 if (mHeaderHeightListener != null && 285 mLastStickyHeaderHeight != stickyHeaderHeight 286 ) { 287 mLastStickyHeaderHeight = stickyHeaderHeight 288 mHeaderHeightListener!!.OnHeaderHeightChanged(stickyHeaderHeight) 289 } 290 val SectionLastView: View? = mListView?.getChildAt(sectionLastItemPosition) 291 if (SectionLastView != null && SectionLastView.getBottom() <= stickyHeaderHeight) { 292 val lastViewBottom: Int = SectionLastView.getBottom() 293 mStickyHeader?.setTranslationY(lastViewBottom.toFloat() - 294 stickyHeaderHeight.toFloat()) 295 if (mSeparatorView != null) { 296 mSeparatorView?.setVisibility(View.GONE) 297 } 298 } else if (stickyHeaderHeight != 0) { 299 mStickyHeader?.setTranslationY(0f) 300 if (mSeparatorView != null && 301 mStickyHeader?.equals(mNonessentialHeader) == false) { 302 mSeparatorView?.setVisibility(View.VISIBLE) 303 } 304 } 305 if (newView) { 306 mStickyHeader?.setVisibility(View.INVISIBLE) 307 this.addView(mStickyHeader) 308 if (mSeparatorView != null && 309 mStickyHeader?.equals(mNonessentialHeader) == false) { 310 val params: FrameLayout.LayoutParams = LayoutParams( 311 LayoutParams.MATCH_PARENT, 312 mSeparatorWidth 313 ) 314 params.setMargins(0, mStickyHeader?.getMeasuredHeight() as Int, 0, 0) 315 mSeparatorView?.setLayoutParams(params) 316 mSeparatorView?.setVisibility(View.VISIBLE) 317 } 318 mStickyHeader?.setVisibility(View.VISIBLE) 319 } 320 } 321 } 322 } 323 324 @Override onFinishInflatenull325 protected override fun onFinishInflate() { 326 super.onFinishInflate() 327 if (!mChildViewsCreated) { 328 setChildViews() 329 } 330 mDoHeaderReset = true 331 } 332 333 @Override onAttachedToWindownull334 protected override fun onAttachedToWindow() { 335 super.onAttachedToWindow() 336 if (!mChildViewsCreated) { 337 setChildViews() 338 } 339 mDoHeaderReset = true 340 } 341 342 // Resets the sticky header when the adapter data set was changed 343 // This code is needed only if dataset changes do not force a call to OnScroll 344 // protected void onDataChanged() { 345 // Should do a call to updateStickyHeader if needed 346 // } setChildViewsnull347 private fun setChildViews() { 348 // Find a child ListView (if any) 349 val iChildNum: Int = getChildCount() 350 for (i in 0 until iChildNum) { 351 val v: Object = getChildAt(i) as Object 352 if (v is ListView) { 353 setListView(v as ListView) 354 } 355 } 356 357 // No child ListView - add one 358 if (mListView == null) { 359 setListView(ListView(mContext)) 360 } 361 362 // Create a nonessential view , it will be used in case a section has no header 363 mNonessentialHeader = View(mContext) 364 val params: ViewGroup.LayoutParams = LayoutParams( 365 LayoutParams.MATCH_PARENT, 366 1, Gravity.TOP 367 ) 368 mNonessentialHeader?.setLayoutParams(params) 369 mNonessentialHeader?.setBackgroundColor(Color.TRANSPARENT) 370 mChildViewsCreated = true 371 } 372 373 companion object { 374 private const val TAG = "StickyHeaderListView" 375 } 376 377 /** 378 * Constructor 379 * 380 * @param context - application context. 381 * @param attrs - layout attributes. 382 */ 383 init { 384 mContext = context 385 } 386 }