/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.bubbles import android.content.Context import android.graphics.Color import android.graphics.PointF import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.View.OnKeyListener import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import com.android.internal.util.ContrastColorUtil import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators /** * User education view to highlight the collapsed stack of bubbles. * Shown only the first time a user taps the stack. */ class StackEducationView constructor( context: Context, positioner: BubblePositioner, controller: BubbleController ) : LinearLayout(context) { private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" else BubbleDebugConfig.TAG_BUBBLES private val ANIMATE_DURATION: Long = 200 private val ANIMATE_DURATION_SHORT: Long = 40 private val positioner: BubblePositioner = positioner private val controller: BubbleController = controller private val view by lazy { findViewById(R.id.stack_education_layout) } private val titleTextView by lazy { findViewById(R.id.stack_education_title) } private val descTextView by lazy { findViewById(R.id.stack_education_description) } private var isHiding = false init { LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this) visibility = View.GONE elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() // BubbleStackView forces LTR by default // since most of Bubble UI direction depends on positioning by the user. // This view actually lays out differently in RTL, so we set layout LOCALE here. layoutDirection = View.LAYOUT_DIRECTION_LOCALE } override fun setLayoutDirection(layoutDirection: Int) { super.setLayoutDirection(layoutDirection) setDrawableDirection() } override fun onFinishInflate() { super.onFinishInflate() layoutDirection = resources.configuration.layoutDirection setTextColor() } override fun onAttachedToWindow() { super.onAttachedToWindow() setFocusableInTouchMode(true) setOnKeyListener(object : OnKeyListener { override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean { // if the event is a key down event on the enter button if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK && !isHiding) { hide(false) return true } return false } }) } override fun onDetachedFromWindow() { super.onDetachedFromWindow() setOnKeyListener(null) controller.updateWindowFlagsForBackpress(false /* interceptBack */) } private fun setTextColor() { val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, android.R.attr.textColorPrimaryInverse)) val bgColor = ta.getColor(0 /* index */, Color.BLACK) var textColor = ta.getColor(1 /* index */, Color.WHITE) ta.recycle() textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) titleTextView.setTextColor(textColor) descTextView.setTextColor(textColor) } private fun setDrawableDirection() { view.setBackgroundResource( if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) R.drawable.bubble_stack_user_education_bg else R.drawable.bubble_stack_user_education_bg_rtl) } /** * If necessary, shows the user education view for the bubble stack. This appears the first * time a user taps on a bubble. * * @return true if user education was shown and wasn't showing before, false otherwise. */ fun show(stackPosition: PointF): Boolean { isHiding = false if (visibility == VISIBLE) return false controller.updateWindowFlagsForBackpress(true /* interceptBack */) layoutParams.width = if (positioner.isLargeScreen || positioner.isLandscape) context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width) else ViewGroup.LayoutParams.MATCH_PARENT val stackPadding = context.resources.getDimensionPixelSize( R.dimen.bubble_user_education_stack_padding) setAlpha(0f) setVisibility(View.VISIBLE) post { requestFocus() with(view) { if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) { setPadding(positioner.bubbleSize + stackPadding, paddingTop, paddingRight, paddingBottom) } else { setPadding(paddingLeft, paddingTop, positioner.bubbleSize + stackPadding, paddingBottom) if (positioner.isLargeScreen || positioner.isLandscape) { translationX = (positioner.screenRect.right - width - stackPadding) .toFloat() } else { translationX = 0f } } translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 } animate() .setDuration(ANIMATE_DURATION) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .alpha(1f) } setShouldShow(false) return true } /** * If necessary, hides the stack education view. * * @param isExpanding if true this indicates the hide is happening due to the bubble being * expanded, false if due to a touch outside of the bubble stack. */ fun hide(isExpanding: Boolean) { if (visibility != VISIBLE || isHiding) return isHiding = true controller.updateWindowFlagsForBackpress(false /* interceptBack */) animate() .alpha(0f) .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) .withEndAction { visibility = GONE } } private fun setShouldShow(shouldShow: Boolean) { context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) .edit().putBoolean(PREF_STACK_EDUCATION, !shouldShow).apply() } } const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"