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 ) 41 : LinearLayout(context) { 42 43 private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" 44 else BubbleDebugConfig.TAG_BUBBLES 45 46 private val ANIMATE_DURATION: Long = 200 47 private val ANIMATE_DURATION_SHORT: Long = 40 48 49 private val positioner: BubblePositioner = positioner 50 private val controller: BubbleController = controller 51 52 private val view by lazy { findViewById<View>(R.id.stack_education_layout) } 53 private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) } 54 private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) } 55 56 private var isHiding = false 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, 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) 133 context.resources.getDimensionPixelSize( 134 R.dimen.bubbles_user_education_width_large_screen) 135 else ViewGroup.LayoutParams.MATCH_PARENT 136 137 setAlpha(0f) 138 setVisibility(View.VISIBLE) 139 post { 140 requestFocus() 141 with(view) { 142 if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) { 143 setPadding(positioner.bubbleSize + paddingRight, paddingTop, paddingRight, 144 paddingBottom) 145 } else { 146 setPadding(paddingLeft, paddingTop, positioner.bubbleSize + paddingLeft, 147 paddingBottom) 148 } 149 translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 150 } 151 animate() 152 .setDuration(ANIMATE_DURATION) 153 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 154 .alpha(1f) 155 } 156 setShouldShow(false) 157 return true 158 } 159 160 /** 161 * If necessary, hides the stack education view. 162 * 163 * @param isExpanding if true this indicates the hide is happening due to the bubble being 164 * expanded, false if due to a touch outside of the bubble stack. 165 */ 166 fun hide(isExpanding: Boolean) { 167 if (visibility != VISIBLE || isHiding) return 168 isHiding = true 169 170 controller.updateWindowFlagsForBackpress(false /* interceptBack */) 171 animate() 172 .alpha(0f) 173 .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) 174 .withEndAction { visibility = GONE } 175 } 176 177 private fun setShouldShow(shouldShow: Boolean) { 178 context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) 179 .edit().putBoolean(PREF_STACK_EDUCATION, !shouldShow).apply() 180 } 181 } 182 183 const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"