1 /* 2 * Copyright (C) 2023 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 17 package com.android.permissioncontroller.safetycenter.ui 18 19 import android.content.Context 20 import android.util.AttributeSet 21 import android.view.View 22 import android.view.ViewGroup 23 import android.view.ViewTreeObserver 24 import androidx.preference.Preference 25 import androidx.preference.PreferenceScreen 26 import androidx.preference.PreferenceViewHolder 27 import com.android.permissioncontroller.R 28 import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity 29 import com.android.settingslib.widget.FooterPreference 30 import kotlin.math.max 31 32 /** 33 * A preference that adds an empty space to the bottom of a Safety Center subpage. 34 * 35 * Due to the logic of [CollapsingToolbarBaseActivity], its content won't be scrollable if it fits 36 * the single page. This logic conflicts with the UX of collapsible and expandable items of Safety 37 * Center, and with some other use cases (i.e. opening the page from Search might scroll to bottom 38 * while the scroll is disabled). In such cases user won't be able to expand the collapsed toolbar 39 * by scrolling the screen content. 40 * 41 * [SpacerPreference] makes the page to be slightly bigger than the screen size to unlock the scroll 42 * regardless of the content length and to mitigate this UX problem. 43 * 44 * If a [FooterPreference] is added to the same [PreferenceScreen], its order should be decreased to 45 * keep it with the last visible content above the [SpacerPreference]. 46 */ 47 internal class SpacerPreference(context: Context, attrs: AttributeSet) : 48 Preference(context, attrs) { 49 50 init { 51 setLayoutResource(R.layout.preference_spacer) 52 isVisible = SafetyCenterUiFlags.getShowSubpages() 53 // spacer should be the last item on screen 54 setOrder(Int.MAX_VALUE - 1) 55 } 56 57 private var maxKnownToolbarHeight = 0 58 onBindViewHoldernull59 override fun onBindViewHolder(holder: PreferenceViewHolder) { 60 super.onBindViewHolder(holder) 61 val spacer = holder.itemView 62 63 // we should ensure we won't add multiple listeners to the same view, 64 // and Preferences API does not allow to do cleanups when onViewRecycled, 65 // so we are keeping a track of the added listener attaching it as a tag to the View 66 val listener: View.OnLayoutChangeListener = 67 spacer.tag as? View.OnLayoutChangeListener 68 ?: object : View.OnLayoutChangeListener { 69 override fun onLayoutChange( 70 v: View?, 71 left: Int, 72 top: Int, 73 right: Int, 74 bottom: Int, 75 oldLeft: Int, 76 oldTop: Int, 77 oldRight: Int, 78 oldBottom: Int, 79 ) { 80 adjustHeight(spacer) 81 } 82 } 83 .also { spacer.tag = it } 84 85 spacer.removeOnLayoutChangeListener(listener) 86 spacer.addOnLayoutChangeListener(listener) 87 } 88 adjustHeightnull89 private fun adjustHeight(spacer: View) { 90 val root = spacer.rootView as? ViewGroup 91 if (root == null) { 92 return 93 } 94 95 val contentParent = 96 root.findViewById<ViewGroup>( 97 com.android.settingslib.collapsingtoolbar.R.id.content_parent 98 ) 99 if (contentParent == null) { 100 return 101 } 102 // when opening the Subpage from Search the layout pass may be triggered 103 // differently due to the auto-scroll to highlight a specific item, 104 // and in this case we need to wait the content parent to be measured 105 if (contentParent.height == 0) { 106 val globalLayoutObserver = 107 object : ViewTreeObserver.OnGlobalLayoutListener { 108 override fun onGlobalLayout() { 109 contentParent.viewTreeObserver.removeOnGlobalLayoutListener(this) 110 adjustHeight(spacer) 111 } 112 } 113 contentParent.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutObserver) 114 return 115 } 116 117 val collapsingToolbar = 118 root.findViewById<View>( 119 com.android.settingslib.collapsingtoolbar.R.id.collapsing_toolbar 120 ) 121 maxKnownToolbarHeight = max(maxKnownToolbarHeight, collapsingToolbar!!.height) 122 123 val contentHeight = spacer.top + maxKnownToolbarHeight 124 val desiredSpacerHeight = 125 if (contentHeight > contentParent.height) { 126 // making it 0 height will remove if from recyclerview 127 1 128 } else { 129 // to unlock the scrolling we need spacer to go slightly beyond the screen 130 contentParent.height - contentHeight + 1 131 } 132 133 val layoutParams = spacer.layoutParams 134 if (layoutParams.height != desiredSpacerHeight) { 135 layoutParams.height = desiredSpacerHeight 136 spacer.layoutParams = layoutParams 137 // need to let RecyclerView to update scroll position 138 spacer.post(::notifyChanged) 139 } 140 } 141 } 142