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