• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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