1 /* 2 * Copyright (C) 2025 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.quickstep.views 17 18 import android.content.Context 19 import android.graphics.Canvas 20 import android.graphics.Rect 21 import android.util.AttributeSet 22 import android.util.FloatProperty 23 import android.widget.Button 24 import com.android.launcher3.Flags.enableFocusOutline 25 import com.android.launcher3.R 26 import com.android.launcher3.util.KFloatProperty 27 import com.android.launcher3.util.MultiPropertyDelegate 28 import com.android.launcher3.util.MultiValueAlpha 29 import com.android.quickstep.util.BorderAnimator 30 import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator 31 import kotlin.math.abs 32 import kotlin.math.min 33 34 class ClearAllButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : 35 Button(context, attrs) { 36 37 private val clearAllButtonAlpha = 38 object : MultiValueAlpha(this, Alpha.entries.size) { applynull39 override fun apply(value: Float) { 40 super.apply(value) 41 isClickable = value >= 1f 42 } 43 } 44 var scrollAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.SCROLL) 45 var contentAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.CONTENT) 46 var visibilityAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.VISIBILITY) 47 var dismissAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.DISMISS) 48 49 var fullscreenProgress = 1f 50 set(value) { 51 if (field == value) { 52 return 53 } 54 field = value 55 applyPrimaryTranslation() 56 } 57 58 /** 59 * Moves ClearAllButton between carousel and 2 row grid. 60 * 61 * 0 = carousel; 1 = 2 row grid. 62 */ 63 var gridProgress = 1f 64 set(value) { 65 if (field == value) { 66 return 67 } 68 field = value 69 applyPrimaryTranslation() 70 } 71 72 private var normalTranslationPrimary = 0f 73 var fullscreenTranslationPrimary = 0f 74 set(value) { 75 if (field == value) { 76 return 77 } 78 field = value 79 applyPrimaryTranslation() 80 } 81 82 var gridTranslationPrimary = 0f 83 set(value) { 84 if (field == value) { 85 return 86 } 87 field = value 88 applyPrimaryTranslation() 89 } 90 91 /** Used to put the button at the middle in the secondary coordinate. */ 92 var taskAlignmentTranslationY = 0f 93 set(value) { 94 if (field == value) { 95 return 96 } 97 field = value 98 applySecondaryTranslation() 99 } 100 101 var gridScrollOffset = 0f 102 var scrollOffsetPrimary = 0f 103 104 private var sidePadding = 0 105 var borderEnabled = false 106 set(value) { 107 if (field == value) { 108 return 109 } 110 field = value 111 focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) 112 } 113 114 private val focusBorderAnimator: BorderAnimator? = 115 if (enableFocusOutline()) 116 createSimpleBorderAnimator( 117 context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_radius), 118 context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width), 119 this::getBorderBounds, 120 this, 121 context 122 .obtainStyledAttributes(attrs, R.styleable.ClearAllButton) 123 .getColor( 124 R.styleable.ClearAllButton_focusBorderColor, 125 BorderAnimator.DEFAULT_BORDER_COLOR, 126 ), 127 ) 128 else null 129 getBorderBoundsnull130 private fun getBorderBounds(bounds: Rect) { 131 bounds.set(0, 0, width, height) 132 val outlinePadding = 133 context.resources.getDimensionPixelSize(R.dimen.recents_clear_all_outline_padding) 134 // Make the value negative to form a padding between button and outline 135 bounds.inset(-outlinePadding, -outlinePadding) 136 } 137 onFocusChangednull138 public override fun onFocusChanged( 139 gainFocus: Boolean, 140 direction: Int, 141 previouslyFocusedRect: Rect?, 142 ) { 143 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) 144 if (borderEnabled) { 145 focusBorderAnimator?.setBorderVisibility(gainFocus, /* animated= */ true) 146 } 147 } 148 drawnull149 override fun draw(canvas: Canvas) { 150 focusBorderAnimator?.drawBorder(canvas) 151 super.draw(canvas) 152 } 153 onLayoutnull154 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 155 super.onLayout(changed, left, top, right, bottom) 156 sidePadding = 157 recentsView?.let { it.pagedOrientationHandler?.getClearAllSidePadding(it, isLayoutRtl) } 158 ?: 0 159 } 160 161 private val recentsView: RecentsView<*, *>? 162 get() = parent as? RecentsView<*, *>? 163 hasOverlappingRenderingnull164 override fun hasOverlappingRendering() = false 165 166 fun onRecentsViewScroll(scroll: Int, gridEnabled: Boolean) { 167 val recentsView = recentsView ?: return 168 169 val orientationSize = 170 recentsView.pagedOrientationHandler.getPrimaryValue(width, height).toFloat() 171 if (orientationSize == 0f) { 172 return 173 } 174 175 val clearAllScroll = recentsView.clearAllScroll 176 val adjustedScrollFromEdge = abs((scroll - clearAllScroll)).toFloat() 177 val shift = min(adjustedScrollFromEdge, orientationSize) 178 normalTranslationPrimary = if (isLayoutRtl) -shift else shift 179 if (!gridEnabled) { 180 normalTranslationPrimary += sidePadding.toFloat() 181 } 182 applyPrimaryTranslation() 183 applySecondaryTranslation() 184 var clearAllSpacing = recentsView.pageSpacing + recentsView.clearAllExtraPageSpacing 185 clearAllSpacing = if (isLayoutRtl) -clearAllSpacing else clearAllSpacing 186 scrollAlpha = 187 ((clearAllScroll + clearAllSpacing - scroll) / clearAllSpacing.toFloat()).coerceAtLeast( 188 0f 189 ) 190 } 191 getScrollAdjustmentnull192 fun getScrollAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean): Float { 193 var scrollAdjustment = 0f 194 if (fullscreenEnabled) { 195 scrollAdjustment += fullscreenTranslationPrimary 196 } 197 if (gridEnabled) { 198 scrollAdjustment += gridTranslationPrimary + gridScrollOffset 199 } 200 scrollAdjustment += scrollOffsetPrimary 201 return scrollAdjustment 202 } 203 getOffsetAdjustmentnull204 fun getOffsetAdjustment(fullscreenEnabled: Boolean, gridEnabled: Boolean) = 205 getScrollAdjustment(fullscreenEnabled, gridEnabled) 206 207 private fun applyPrimaryTranslation() { 208 val recentsView = recentsView ?: return 209 val orientationHandler = recentsView.pagedOrientationHandler 210 orientationHandler.primaryViewTranslate.set( 211 this, 212 (orientationHandler.getPrimaryValue(0f, taskAlignmentTranslationY) + 213 normalTranslationPrimary + 214 getFullscreenTrans(fullscreenTranslationPrimary) + 215 getGridTrans(gridTranslationPrimary)), 216 ) 217 } 218 applySecondaryTranslationnull219 private fun applySecondaryTranslation() { 220 val recentsView = recentsView ?: return 221 val orientationHandler = recentsView.pagedOrientationHandler 222 orientationHandler.secondaryViewTranslate.set( 223 this, 224 orientationHandler.getSecondaryValue(0f, taskAlignmentTranslationY), 225 ) 226 } 227 getFullscreenTransnull228 private fun getFullscreenTrans(endTranslation: Float) = 229 if (fullscreenProgress > 0) endTranslation else 0f 230 231 private fun getGridTrans(endTranslation: Float) = if (gridProgress > 0) endTranslation else 0f 232 233 companion object { 234 private enum class Alpha { 235 SCROLL, 236 CONTENT, 237 VISIBILITY, 238 DISMISS, 239 } 240 241 @JvmField 242 val VISIBILITY_ALPHA: FloatProperty<ClearAllButton> = 243 KFloatProperty(ClearAllButton::visibilityAlpha) 244 245 @JvmField 246 val DISMISS_ALPHA: FloatProperty<ClearAllButton> = 247 KFloatProperty(ClearAllButton::dismissAlpha) 248 } 249 } 250