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