1 /* 2 * Copyright (C) 2020 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.wm.shell.bubbles 17 18 import android.content.Context 19 import android.graphics.Color 20 import android.graphics.PointF 21 import android.view.KeyEvent 22 import android.view.LayoutInflater 23 import android.view.View 24 import android.view.ViewGroup 25 import android.widget.LinearLayout 26 import android.widget.TextView 27 import com.android.internal.util.ContrastColorUtil 28 import com.android.wm.shell.R 29 import com.android.wm.shell.animation.Interpolators 30 31 /** 32 * User education view to highlight the collapsed stack of bubbles. Shown only the first time a user 33 * taps the stack. 34 */ 35 class StackEducationView( 36 context: Context, 37 private val positioner: BubblePositioner, 38 private val manager: Manager 39 ) : LinearLayout(context) { 40 41 companion object { 42 const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding" 43 private const val ANIMATE_DURATION: Long = 200 44 private const val ANIMATE_DURATION_SHORT: Long = 40 45 } 46 47 /** Callbacks to notify managers of [StackEducationView] about events. */ 48 interface Manager { 49 /** Notifies whether backpress should be intercepted. */ updateWindowFlagsForBackpressnull50 fun updateWindowFlagsForBackpress(interceptBack: Boolean) 51 } 52 53 private val view by lazy { requireViewById<View>(R.id.stack_education_layout) } <lambda>null54 private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) } <lambda>null55 private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) } 56 57 var isHiding = false 58 private set 59 60 init { 61 LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this) 62 63 visibility = View.GONE 64 elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() 65 66 // BubbleStackView forces LTR by default 67 // since most of Bubble UI direction depends on positioning by the user. 68 // This view actually lays out differently in RTL, so we set layout LOCALE here. 69 layoutDirection = View.LAYOUT_DIRECTION_LOCALE 70 } 71 setLayoutDirectionnull72 override fun setLayoutDirection(layoutDirection: Int) { 73 super.setLayoutDirection(layoutDirection) 74 setDrawableDirection(layoutDirection == LAYOUT_DIRECTION_LTR) 75 } 76 onFinishInflatenull77 override fun onFinishInflate() { 78 super.onFinishInflate() 79 layoutDirection = resources.configuration.layoutDirection 80 setTextColor() 81 } 82 onAttachedToWindownull83 override fun onAttachedToWindow() { 84 super.onAttachedToWindow() 85 setFocusableInTouchMode(true) 86 setOnKeyListener(object : OnKeyListener { 87 override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean { 88 // if the event is a key down event on the enter button 89 if (event.action == KeyEvent.ACTION_UP && 90 keyCode == KeyEvent.KEYCODE_BACK && !isHiding) { 91 hide(false) 92 return true 93 } 94 return false 95 } 96 }) 97 } 98 onDetachedFromWindownull99 override fun onDetachedFromWindow() { 100 super.onDetachedFromWindow() 101 setOnKeyListener(null) 102 manager.updateWindowFlagsForBackpress(false /* interceptBack */) 103 } 104 setTextColornull105 private fun setTextColor() { 106 val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, 107 android.R.attr.textColorPrimaryInverse)) 108 val bgColor = ta.getColor(0 /* index */, Color.BLACK) 109 var textColor = ta.getColor(1 /* index */, Color.WHITE) 110 ta.recycle() 111 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) 112 titleTextView.setTextColor(textColor) 113 descTextView.setTextColor(textColor) 114 } 115 setDrawableDirectionnull116 private fun setDrawableDirection(isOnLeft: Boolean) { 117 view.setBackgroundResource( 118 if (isOnLeft) R.drawable.bubble_stack_user_education_bg 119 else R.drawable.bubble_stack_user_education_bg_rtl 120 ) 121 } 122 123 /** 124 * If necessary, shows the user education view for the bubble stack. This appears the first time 125 * a user taps on a bubble. 126 * 127 * @return true if user education was shown and wasn't showing before, false otherwise. 128 */ shownull129 fun show(stackPosition: PointF): Boolean { 130 isHiding = false 131 if (visibility == VISIBLE) return false 132 133 manager.updateWindowFlagsForBackpress(true /* interceptBack */) 134 layoutParams.width = 135 if (positioner.isLargeScreen || positioner.isLandscape) 136 context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width) 137 else ViewGroup.LayoutParams.MATCH_PARENT 138 139 val isStackOnLeft = positioner.isStackOnLeft(stackPosition) 140 (view.layoutParams as MarginLayoutParams).apply { 141 // Update the horizontal margins depending on the stack position 142 val edgeMargin = 143 resources.getDimensionPixelSize(R.dimen.bubble_user_education_margin_horizontal) 144 leftMargin = if (isStackOnLeft) 0 else edgeMargin 145 rightMargin = if (isStackOnLeft) edgeMargin else 0 146 } 147 148 val stackPadding = 149 context.resources.getDimensionPixelSize(R.dimen.bubble_user_education_stack_padding) 150 setAlpha(0f) 151 setVisibility(View.VISIBLE) 152 setDrawableDirection(isOnLeft = isStackOnLeft) 153 post { 154 requestFocus() 155 with(view) { 156 if (isStackOnLeft) { 157 setPadding( 158 positioner.bubbleSize + stackPadding, 159 paddingTop, 160 paddingRight, 161 paddingBottom 162 ) 163 translationX = 0f 164 } else { 165 setPadding( 166 paddingLeft, 167 paddingTop, 168 positioner.bubbleSize + stackPadding, 169 paddingBottom 170 ) 171 translationX = (positioner.screenRect.right - width - stackPadding).toFloat() 172 } 173 translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 174 } 175 animate() 176 .setDuration(ANIMATE_DURATION) 177 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 178 .alpha(1f) 179 } 180 updateStackEducationSeen() 181 return true 182 } 183 184 /** 185 * If necessary, hides the stack education view. 186 * 187 * @param isExpanding if true this indicates the hide is happening due to the bubble being 188 * expanded, false if due to a touch outside of the bubble stack. 189 */ hidenull190 fun hide(isExpanding: Boolean) { 191 if (visibility != VISIBLE || isHiding) return 192 isHiding = true 193 194 manager.updateWindowFlagsForBackpress(false /* interceptBack */) 195 animate() 196 .alpha(0f) 197 .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) 198 .withEndAction { visibility = GONE } 199 } 200 updateStackEducationSeennull201 private fun updateStackEducationSeen() { 202 context 203 .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) 204 .edit() 205 .putBoolean(PREF_STACK_EDUCATION, true) 206 .apply() 207 } 208 } 209