Bubbles user education for stack & manage
Two types of education 1) highlighting the bubble when it first appears on screen (after user tap) 2) highlighting manage button when a bubble is expanded * Both of these are added in BubbleStackView & respond to theme changes * Prefs to save whether user has seen either education * Simple animations in / out; we can tune them later * Adjusts the starting position of the stack to work with user education positioning * Adds a secure setting string to force user education to show for demo purposes. Test: manual: 1) get bubble for first time 2) tap on flyout => flyout disappears & user education shows 3) get another update to that bubble => note that it doesn't show bc user education is displaying 1) get bubble for first time 2) tap on bubble => flyout disappears & user education shows 3) tap outside of the user education => user education hides 1) get bubble for first time 2) wait => after flyout disappears, user education shows 3) tap on the bubble => user education hides & bubble expands, after expansion "manage" user education appears 4a) interact with content in the activity view 4b) collapse the stack (via back or tapping outside) 4c) tap on the manage button 4d) select a different bubble => each of 4x) should dismiss the manage button user education Check that switching themes (and dark mode) gets the right colour for user education backgrounds & text. Bug: 148105621 Change-Id: I8e6401573c1ac21dfe636aa1132461fe04ce67ac Merged-In: I8e6401573c1ac21dfe636aa1132461fe04ce67ac
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?android:attr/colorAccent"/>
|
||||
<corners
|
||||
android:radius="?android:attr/dialogCornerRadius" />
|
||||
</shape>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?android:attr/colorAccent"/>
|
||||
<corners
|
||||
android:bottomRightRadius="360dp"
|
||||
android:topRightRadius="360dp" />
|
||||
</shape>
|
||||
48
packages/SystemUI/res/layout/bubble_stack_user_education.xml
Normal file
48
packages/SystemUI/res/layout/bubble_stack_user_education.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/user_education_view"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:paddingTop="48dp"
|
||||
android:paddingBottom="48dp"
|
||||
android:paddingStart="@dimen/bubble_stack_user_education_side_inset"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/bubble_stack_user_education_bg">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_education_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="16dp"
|
||||
android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
|
||||
android:maxLines="1"
|
||||
android:text="@string/bubbles_user_education_title"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Headline"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_education_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/bubbles_user_education_description"
|
||||
android:fontFamily="@*android:string/config_bodyFontFamily"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<com.android.systemui.bubbles.BubbleManageEducationView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/manage_education_view"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="@dimen/bubbles_manage_education_width">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/user_education_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:text="@string/bubbles_user_education_manage"
|
||||
android:fontFamily="@*android:string/config_bodyFontFamily"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"
|
||||
android:background="@drawable/bubble_manage_user_education_bg"
|
||||
/>
|
||||
|
||||
<View
|
||||
android:id="@+id/user_education_pointer"
|
||||
android:layout_width="@dimen/bubble_pointer_width"
|
||||
android:layout_height="@dimen/bubble_pointer_height"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
</com.android.systemui.bubbles.BubbleManageEducationView>
|
||||
@@ -1159,7 +1159,7 @@
|
||||
<!-- Extra padding around the dismiss target for bubbles -->
|
||||
<dimen name="bubble_dismiss_slop">16dp</dimen>
|
||||
<!-- Height of button allowing users to adjust settings for bubbles. -->
|
||||
<dimen name="bubble_settings_size">48dp</dimen>
|
||||
<dimen name="bubble_manage_button_height">48dp</dimen>
|
||||
<!-- How far, horizontally, to animate the expanded view over when animating in/out. -->
|
||||
<dimen name="bubble_expanded_animate_x_distance">100dp</dimen>
|
||||
<!-- How far, vertically, to animate the expanded view over when animating in/out. -->
|
||||
@@ -1175,16 +1175,22 @@
|
||||
<!-- How far offscreen the bubble stack rests. Cuts off padding and part of icon bitmap. -->
|
||||
<dimen name="bubble_stack_offscreen">9dp</dimen>
|
||||
<!-- How far down the screen the stack starts. -->
|
||||
<dimen name="bubble_stack_starting_offset_y">96dp</dimen>
|
||||
<dimen name="bubble_stack_starting_offset_y">120dp</dimen>
|
||||
<!-- Space between the pointer triangle and the bubble expanded view -->
|
||||
<dimen name="bubble_pointer_margin">8dp</dimen>
|
||||
<!-- Height of the permission prompt shown with bubbles -->
|
||||
<dimen name="bubble_permission_height">120dp</dimen>
|
||||
<!-- Padding applied to the bubble dismiss target. Touches in this padding cause the bubbles to
|
||||
snap to the dismiss target. -->
|
||||
<dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
|
||||
<dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
|
||||
|
||||
<!-- Bubbles user education views -->
|
||||
<dimen name="bubbles_manage_education_width">160dp</dimen>
|
||||
<!-- The inset from the top bound of the manage button to place the user education. -->
|
||||
<dimen name="bubbles_manage_education_top_inset">10dp</dimen>
|
||||
<!-- Size of padding for the user education cling, this should at minimum be larger than
|
||||
individual_bubble_size + some padding. -->
|
||||
<dimen name="bubble_stack_user_education_side_inset">72dp</dimen>
|
||||
|
||||
<!-- Size of the RAT type for CellularTile -->
|
||||
<dimen name="celltile_rat_type_size">10sp</dimen>
|
||||
|
||||
|
||||
@@ -2586,6 +2586,12 @@
|
||||
<string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
|
||||
<!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] -->
|
||||
<string name="bubble_dismiss_text">Dismiss</string>
|
||||
<!-- Title text for the bubbles feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=60]-->
|
||||
<string name="bubbles_user_education_title">Keep chats up front</string>
|
||||
<!-- Descriptive text for the bubble feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=NONE] -->
|
||||
<string name="bubbles_user_education_description">New chats from <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> will appear as bubbles. Tap a bubble to open it. Drag to move it.\n\nTap the bubble</string>
|
||||
<!-- Text for the bubble "manage" button feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=80]-->
|
||||
<string name="bubbles_user_education_manage">Tap Manage to turn off bubbles from this app</string>
|
||||
|
||||
<!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] -->
|
||||
<string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string>
|
||||
|
||||
@@ -57,7 +57,9 @@ public final class Prefs {
|
||||
Key.SEEN_RINGER_GUIDANCE_COUNT,
|
||||
Key.QS_HAS_TURNED_OFF_MOBILE_DATA,
|
||||
Key.TOUCHED_RINGER_TOGGLE,
|
||||
Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP
|
||||
Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP,
|
||||
Key.HAS_SEEN_BUBBLES_EDUCATION,
|
||||
Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION
|
||||
})
|
||||
public @interface Key {
|
||||
@Deprecated
|
||||
@@ -103,6 +105,8 @@ public final class Prefs {
|
||||
String QS_HAS_TURNED_OFF_MOBILE_DATA = "QsHasTurnedOffMobileData";
|
||||
String TOUCHED_RINGER_TOGGLE = "TouchedRingerToggle";
|
||||
String HAS_SEEN_ODI_CAPTIONS_TOOLTIP = "HasSeenODICaptionsTooltip";
|
||||
String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding";
|
||||
String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding";
|
||||
}
|
||||
|
||||
public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.systemui.bubbles;
|
||||
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -41,6 +44,20 @@ public class BubbleDebugConfig {
|
||||
static final boolean DEBUG_BUBBLE_EXPANDED_VIEW = false;
|
||||
static final boolean DEBUG_EXPERIMENTS = true;
|
||||
static final boolean DEBUG_OVERFLOW = false;
|
||||
static final boolean DEBUG_USER_EDUCATION = false;
|
||||
|
||||
private static final boolean FORCE_SHOW_USER_EDUCATION = false;
|
||||
private static final String FORCE_SHOW_USER_EDUCATION_SETTING =
|
||||
"force_show_bubbles_user_education";
|
||||
|
||||
/**
|
||||
* @return whether we should force show user education for bubbles. Used for debugging & demos.
|
||||
*/
|
||||
static boolean forceShowUserEducation(Context context) {
|
||||
boolean forceShow = Settings.Secure.getInt(context.getContentResolver(),
|
||||
FORCE_SHOW_USER_EDUCATION_SETTING, 0) != 0;
|
||||
return FORCE_SHOW_USER_EDUCATION || forceShow;
|
||||
}
|
||||
|
||||
static String formatBubblesString(List<Bubble> bubbles, Bubble selected) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -243,7 +243,7 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
|
||||
mPointerView.setVisibility(INVISIBLE);
|
||||
|
||||
mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.bubble_settings_size);
|
||||
R.dimen.bubble_manage_button_height);
|
||||
mSettingsIcon = findViewById(R.id.settings_button);
|
||||
mSettingsIcon.setOnClickListener(this);
|
||||
|
||||
@@ -530,6 +530,16 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
|
||||
mPointerView.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position of the manage button displayed in the expanded view. Used for placing user
|
||||
* education about the manage button.
|
||||
*/
|
||||
public Rect getManageButtonLocationOnScreen() {
|
||||
mTempLoc = mSettingsIcon.getLocationOnScreen();
|
||||
return new Rect(mTempLoc[0], mTempLoc[1], mTempLoc[0] + mSettingsIcon.getWidth(),
|
||||
mTempLoc[1] + mSettingsIcon.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and releases an ActivityView if one was previously created for this bubble.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.systemui.bubbles;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.util.ContrastColorUtil;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.recents.TriangleShape;
|
||||
|
||||
/**
|
||||
* Educational view to highlight the manage button that allows a user to configure the settings
|
||||
* for the bubble. Shown only the first time a user expands a bubble.
|
||||
*/
|
||||
public class BubbleManageEducationView extends LinearLayout {
|
||||
|
||||
private View mPointerView;
|
||||
private View mManageView;
|
||||
|
||||
public BubbleManageEducationView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public BubbleManageEducationView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public BubbleManageEducationView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public BubbleManageEducationView(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
mManageView = findViewById(R.id.manage_education_view);
|
||||
|
||||
final TypedArray ta = mContext.obtainStyledAttributes(
|
||||
new int[] {android.R.attr.colorAccent,
|
||||
android.R.attr.textColorPrimaryInverse});
|
||||
final int bgColor = ta.getColor(0, Color.BLACK);
|
||||
int textColor = ta.getColor(1, Color.WHITE);
|
||||
ta.recycle();
|
||||
|
||||
textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
|
||||
((TextView) findViewById(R.id.user_education_description)).setTextColor(textColor);
|
||||
|
||||
final Resources res = getResources();
|
||||
final int pointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
|
||||
final int pointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
|
||||
|
||||
ShapeDrawable triangleShape =
|
||||
new ShapeDrawable(TriangleShape.create(
|
||||
pointerWidth, pointerHeight, false /* isPointingUp */));
|
||||
triangleShape.setTint(bgColor);
|
||||
|
||||
mPointerView = findViewById(R.id.user_education_pointer);
|
||||
mPointerView.setBackground(triangleShape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the x value this pointer should point to.
|
||||
*/
|
||||
public void setPointerPosition(int x) {
|
||||
mPointerView.setTranslationX(x - (mPointerView.getWidth() / 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the position for the manage view.
|
||||
*/
|
||||
public void setManageViewPosition(int x, int y) {
|
||||
mManageView.setTranslationX(x);
|
||||
mManageView.setTranslationY(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the height of the view that shows the educational text and pointer.
|
||||
*/
|
||||
public int getManageViewHeight() {
|
||||
return mManageView.getHeight();
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,13 @@ package com.android.systemui.bubbles;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
|
||||
import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION;
|
||||
import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION;
|
||||
import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
|
||||
import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
|
||||
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
|
||||
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION;
|
||||
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
|
||||
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
|
||||
|
||||
@@ -33,6 +37,8 @@ import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Paint;
|
||||
@@ -57,6 +63,7 @@ import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -66,7 +73,9 @@ import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.ContrastColorUtil;
|
||||
import com.android.internal.widget.ViewClippingUtil;
|
||||
import com.android.systemui.Prefs;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.bubbles.animation.ExpandedAnimationController;
|
||||
import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
|
||||
@@ -88,6 +97,10 @@ import java.util.List;
|
||||
public class BubbleStackView extends FrameLayout {
|
||||
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
|
||||
|
||||
/** Animation durations for bubble stack user education views. **/
|
||||
private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
|
||||
private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
|
||||
|
||||
/** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
|
||||
static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
|
||||
|
||||
@@ -171,6 +184,12 @@ public class BubbleStackView extends FrameLayout {
|
||||
* previous one animates out.
|
||||
*/
|
||||
private Runnable mAfterFlyoutHidden;
|
||||
/**
|
||||
* Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
|
||||
* once it collapses.
|
||||
*/
|
||||
@Nullable
|
||||
private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
|
||||
|
||||
/** Layout change listener that moves the stack to the nearest valid position on rotation. */
|
||||
private OnLayoutChangeListener mOrientationChangedListener;
|
||||
@@ -319,6 +338,14 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
private BubbleOverflow mBubbleOverflow;
|
||||
|
||||
private boolean mShouldShowUserEducation;
|
||||
private boolean mAnimatingEducationAway;
|
||||
private View mUserEducationView;
|
||||
|
||||
private boolean mShouldShowManageEducation;
|
||||
private BubbleManageEducationView mManageEducationView;
|
||||
private boolean mAnimatingManageEducationAway;
|
||||
|
||||
public BubbleStackView(Context context, BubbleData data,
|
||||
@Nullable SurfaceSynchronizer synchronizer,
|
||||
FloatingContentCoordinator floatingContentCoordinator) {
|
||||
@@ -361,6 +388,8 @@ public class BubbleStackView extends FrameLayout {
|
||||
mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
|
||||
mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
|
||||
|
||||
setUpUserEducation();
|
||||
|
||||
mBubbleContainer = new PhysicsAnimationLayout(context);
|
||||
mBubbleContainer.setActiveController(mStackAnimationController);
|
||||
mBubbleContainer.setElevation(elevation);
|
||||
@@ -500,10 +529,50 @@ public class BubbleStackView extends FrameLayout {
|
||||
});
|
||||
}
|
||||
|
||||
void showExpandedViewContents(int displayId) {
|
||||
if (mExpandedBubble != null
|
||||
&& mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
|
||||
mExpandedBubble.setContentVisibility(true);
|
||||
private void setUpUserEducation() {
|
||||
if (mUserEducationView != null) {
|
||||
removeView(mUserEducationView);
|
||||
}
|
||||
mShouldShowUserEducation = shouldShowBubblesEducation();
|
||||
if (DEBUG_USER_EDUCATION) {
|
||||
Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
|
||||
}
|
||||
if (mShouldShowUserEducation) {
|
||||
mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
|
||||
false /* attachToRoot */);
|
||||
mUserEducationView.setVisibility(GONE);
|
||||
|
||||
final TypedArray ta = mContext.obtainStyledAttributes(
|
||||
new int[] {android.R.attr.colorAccent,
|
||||
android.R.attr.textColorPrimaryInverse});
|
||||
final int bgColor = ta.getColor(0, Color.BLACK);
|
||||
int textColor = ta.getColor(1, Color.WHITE);
|
||||
ta.recycle();
|
||||
textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
|
||||
|
||||
TextView title = mUserEducationView.findViewById(R.id.user_education_title);
|
||||
TextView description = mUserEducationView.findViewById(R.id.user_education_description);
|
||||
title.setTextColor(textColor);
|
||||
description.setTextColor(textColor);
|
||||
|
||||
addView(mUserEducationView);
|
||||
}
|
||||
|
||||
if (mManageEducationView != null) {
|
||||
removeView(mManageEducationView);
|
||||
}
|
||||
mShouldShowManageEducation = shouldShowManageEducation();
|
||||
if (DEBUG_USER_EDUCATION) {
|
||||
Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
|
||||
}
|
||||
if (mShouldShowManageEducation) {
|
||||
mManageEducationView = (BubbleManageEducationView)
|
||||
mInflater.inflate(R.layout.bubbles_manage_button_education, this,
|
||||
false /* attachToRoot */);
|
||||
mManageEducationView.setVisibility(GONE);
|
||||
mManageEducationView.setElevation(mBubbleElevation);
|
||||
|
||||
addView(mManageEducationView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,6 +608,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
public void onThemeChanged() {
|
||||
setUpFlyout();
|
||||
setUpOverflow();
|
||||
setUpUserEducation();
|
||||
}
|
||||
|
||||
/** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
|
||||
@@ -731,7 +801,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
Bubble getExpandedBubble() {
|
||||
if (mExpandedBubble == null
|
||||
|| (mExpandedBubble.getIconView() == mBubbleOverflow.getBtn()
|
||||
&& mExpandedBubble.getKey() == BubbleOverflow.KEY)) {
|
||||
&& BubbleOverflow.KEY.equals(mExpandedBubble.getKey()))) {
|
||||
return null;
|
||||
}
|
||||
return (Bubble) mExpandedBubble;
|
||||
@@ -743,6 +813,12 @@ public class BubbleStackView extends FrameLayout {
|
||||
Log.d(TAG, "addBubble: " + bubble);
|
||||
}
|
||||
|
||||
if (getBubbleCount() == 0 && mShouldShowUserEducation) {
|
||||
// Override the default stack position if we're showing user education.
|
||||
mStackAnimationController.setStackPosition(
|
||||
mStackAnimationController.getDefaultStartPosition());
|
||||
}
|
||||
|
||||
if (getBubbleCount() == 0) {
|
||||
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
|
||||
}
|
||||
@@ -880,6 +956,109 @@ public class BubbleStackView extends FrameLayout {
|
||||
notifyExpansionChanged(mExpandedBubble, mIsExpanded);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, false otherwise.
|
||||
*/
|
||||
private boolean maybeShowStackUserEducation() {
|
||||
if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
|
||||
Bubble b = mBubbleData.getSelectedBubble();
|
||||
TextView description = mUserEducationView.findViewById(R.id.user_education_description);
|
||||
description.setText(mContext.getString(
|
||||
R.string.bubbles_user_education_description, b.getAppName()));
|
||||
|
||||
mUserEducationView.setAlpha(0);
|
||||
mUserEducationView.setVisibility(VISIBLE);
|
||||
// Post so we have height of mUserEducationView
|
||||
mUserEducationView.post(() -> {
|
||||
final int viewHeight = mUserEducationView.getHeight();
|
||||
PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
|
||||
final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
|
||||
mUserEducationView.setTranslationY(translationY);
|
||||
mUserEducationView.animate()
|
||||
.setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
|
||||
.setInterpolator(FAST_OUT_SLOW_IN)
|
||||
.alpha(1);
|
||||
});
|
||||
Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If necessary, hides the user education view for the bubble stack.
|
||||
*
|
||||
* @param fromExpansion 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.
|
||||
*/
|
||||
void hideStackUserEducation(boolean fromExpansion) {
|
||||
if (mShouldShowUserEducation
|
||||
&& mUserEducationView.getVisibility() == VISIBLE
|
||||
&& !mAnimatingEducationAway) {
|
||||
mAnimatingEducationAway = true;
|
||||
mUserEducationView.animate()
|
||||
.alpha(0)
|
||||
.setDuration(fromExpansion
|
||||
? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
|
||||
: ANIMATE_STACK_USER_EDUCATION_DURATION)
|
||||
.withEndAction(() -> {
|
||||
mAnimatingEducationAway = false;
|
||||
mShouldShowUserEducation = shouldShowBubblesEducation();
|
||||
mUserEducationView.setVisibility(GONE);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If necessary, toggles the user education view for the manage button. This is shown when the
|
||||
* bubble stack is expanded for the first time.
|
||||
*
|
||||
* @param show whether the user education view should show or not.
|
||||
*/
|
||||
void maybeShowManageEducation(boolean show) {
|
||||
if (mManageEducationView == null) {
|
||||
return;
|
||||
}
|
||||
if (show
|
||||
&& mShouldShowManageEducation
|
||||
&& mManageEducationView.getVisibility() != VISIBLE
|
||||
&& mIsExpanded) {
|
||||
mManageEducationView.setAlpha(0);
|
||||
mManageEducationView.setVisibility(VISIBLE);
|
||||
mManageEducationView.post(() -> {
|
||||
final Rect position =
|
||||
mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
|
||||
final int viewHeight = mManageEducationView.getManageViewHeight();
|
||||
final int inset = getResources().getDimensionPixelSize(
|
||||
R.dimen.bubbles_manage_education_top_inset);
|
||||
mManageEducationView.bringToFront();
|
||||
mManageEducationView.setManageViewPosition(position.left,
|
||||
position.top - viewHeight + inset);
|
||||
mManageEducationView.setPointerPosition(position.centerX() - position.left);
|
||||
mManageEducationView.animate()
|
||||
.setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
|
||||
.setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
|
||||
});
|
||||
Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
|
||||
} else if (!show
|
||||
&& mManageEducationView.getVisibility() == VISIBLE
|
||||
&& !mAnimatingManageEducationAway) {
|
||||
mManageEducationView.animate()
|
||||
.alpha(0)
|
||||
.setDuration(mIsExpansionAnimating
|
||||
? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
|
||||
: ANIMATE_STACK_USER_EDUCATION_DURATION)
|
||||
.withEndAction(() -> {
|
||||
mAnimatingManageEducationAway = false;
|
||||
mShouldShowManageEducation = shouldShowManageEducation();
|
||||
mManageEducationView.setVisibility(GONE);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the stack of bubbles.
|
||||
*
|
||||
@@ -923,7 +1102,17 @@ public class BubbleStackView extends FrameLayout {
|
||||
return null;
|
||||
} else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
|
||||
return mFlyout;
|
||||
} else if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
|
||||
View bubbleChild = mBubbleContainer.getChildAt(0);
|
||||
if (isIntersecting(bubbleChild, x, y)) {
|
||||
return this;
|
||||
} else if (isIntersecting(mUserEducationView, x, y)) {
|
||||
return mUserEducationView;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack.
|
||||
return this;
|
||||
}
|
||||
@@ -932,22 +1121,6 @@ public class BubbleStackView extends FrameLayout {
|
||||
return mFlyout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses the stack of bubbles.
|
||||
* <p>
|
||||
* Must be called from the main thread.
|
||||
*
|
||||
* @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
|
||||
*/
|
||||
@Deprecated
|
||||
@MainThread
|
||||
void collapseStack() {
|
||||
if (DEBUG_BUBBLE_STACK_VIEW) {
|
||||
Log.d(TAG, "collapseStack()");
|
||||
}
|
||||
mBubbleData.setExpanded(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
|
||||
*/
|
||||
@@ -957,25 +1130,16 @@ public class BubbleStackView extends FrameLayout {
|
||||
if (DEBUG_BUBBLE_STACK_VIEW) {
|
||||
Log.d(TAG, "collapseStack(endRunnable)");
|
||||
}
|
||||
collapseStack();
|
||||
mBubbleData.setExpanded(false);
|
||||
// TODO - use the runnable at end of animation
|
||||
endRunnable.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the stack of bubbles.
|
||||
* <p>
|
||||
* Must be called from the main thread.
|
||||
*
|
||||
* @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
|
||||
*/
|
||||
@Deprecated
|
||||
@MainThread
|
||||
void expandStack() {
|
||||
if (DEBUG_BUBBLE_STACK_VIEW) {
|
||||
Log.d(TAG, "expandStack()");
|
||||
void showExpandedViewContents(int displayId) {
|
||||
if (mExpandedBubble != null
|
||||
&& mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
|
||||
mExpandedBubble.setContentVisibility(true);
|
||||
}
|
||||
mBubbleData.setExpanded(true);
|
||||
}
|
||||
|
||||
private void beforeExpandedViewAnimation() {
|
||||
@@ -995,11 +1159,12 @@ public class BubbleStackView extends FrameLayout {
|
||||
mIsExpanded = false;
|
||||
final BubbleViewProvider previouslySelected = mExpandedBubble;
|
||||
beforeExpandedViewAnimation();
|
||||
maybeShowManageEducation(false);
|
||||
|
||||
if (DEBUG_BUBBLE_STACK_VIEW) {
|
||||
Log.d(TAG, "animateCollapse");
|
||||
Log.d(TAG, BubbleDebugConfig.formatBubblesString(this.getBubblesOnScreen(),
|
||||
this.getExpandedBubble()));
|
||||
Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
|
||||
getExpandedBubble()));
|
||||
}
|
||||
updateOverflowBtnVisibility(/* apply */ false);
|
||||
mBubbleContainer.cancelAllAnimations();
|
||||
@@ -1021,6 +1186,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
private void animateExpansion() {
|
||||
mIsExpanded = true;
|
||||
hideStackUserEducation(true /* fromExpansion */);
|
||||
beforeExpandedViewAnimation();
|
||||
|
||||
mBubbleContainer.setActiveController(mExpandedAnimationController);
|
||||
@@ -1028,6 +1194,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
mExpandedAnimationController.expandFromStack(() -> {
|
||||
updatePointerPosition();
|
||||
afterExpandedViewAnimation();
|
||||
maybeShowManageEducation(true);
|
||||
} /* after */);
|
||||
|
||||
mExpandedViewContainer.setTranslationX(getCollapsedX());
|
||||
@@ -1074,11 +1241,19 @@ public class BubbleStackView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the collapsed stack is tapped on. */
|
||||
void onStackTapped() {
|
||||
if (!maybeShowStackUserEducation()) {
|
||||
mBubbleData.setExpanded(true);
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a drag operation on an individual bubble has started. */
|
||||
public void onBubbleDragStart(View bubble) {
|
||||
if (DEBUG_BUBBLE_STACK_VIEW) {
|
||||
Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
|
||||
}
|
||||
maybeShowManageEducation(false);
|
||||
mExpandedAnimationController.prepareForBubbleDrag(bubble);
|
||||
}
|
||||
|
||||
@@ -1129,6 +1304,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
hideStackUserEducation(false /* fromExpansion */);
|
||||
springInDismissTarget();
|
||||
mStackAnimationController.moveStackFromTouch(x, y);
|
||||
}
|
||||
@@ -1191,14 +1367,13 @@ public class BubbleStackView extends FrameLayout {
|
||||
mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
|
||||
* once it collapses.
|
||||
*/
|
||||
@Nullable private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
|
||||
|
||||
void onFlyoutTapped() {
|
||||
mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
|
||||
if (maybeShowStackUserEducation()) {
|
||||
// If we're showing user education, don't open the bubble show the education first
|
||||
mBubbleToExpandAfterFlyoutCollapse = null;
|
||||
} else {
|
||||
mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
|
||||
}
|
||||
|
||||
mFlyout.removeCallbacks(mHideFlyout);
|
||||
mHideFlyout.run();
|
||||
@@ -1221,6 +1396,8 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
mFlyout.removeCallbacks(mHideFlyout);
|
||||
animateFlyoutCollapsed(shouldDismiss, velX);
|
||||
|
||||
maybeShowStackUserEducation();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1432,6 +1609,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
if (flyoutMessage == null
|
||||
|| flyoutMessage.message == null
|
||||
|| !bubble.showFlyout()
|
||||
|| (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
|
||||
|| isExpanded()
|
||||
|| mIsExpansionAnimating
|
||||
|| mIsGestureInProgress
|
||||
@@ -1517,7 +1695,12 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
@Override
|
||||
public void getBoundsOnScreen(Rect outRect) {
|
||||
// If the bubble menu is open, the entire screen should capture touch events.
|
||||
if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
|
||||
// When user education shows then capture all touches
|
||||
outRect.set(0, 0, getWidth(), getHeight());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mIsExpanded) {
|
||||
if (getBubbleCount() > 0) {
|
||||
mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
|
||||
@@ -1704,6 +1887,18 @@ public class BubbleStackView extends FrameLayout {
|
||||
return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
|
||||
}
|
||||
|
||||
/** Whether the educational view should appear for bubbles. **/
|
||||
private boolean shouldShowBubblesEducation() {
|
||||
return BubbleDebugConfig.forceShowUserEducation(getContext())
|
||||
|| !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
|
||||
}
|
||||
|
||||
/** Whether the educational view should appear for the expanded view "manage" button. **/
|
||||
private boolean shouldShowManageEducation() {
|
||||
return BubbleDebugConfig.forceShowUserEducation(getContext())
|
||||
|| !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
|
||||
}
|
||||
|
||||
/** For debugging only */
|
||||
List<Bubble> getBubblesOnScreen() {
|
||||
List<Bubble> bubbles = new ArrayList<>();
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.android.systemui.Dependency;
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing,
|
||||
@@ -92,6 +91,7 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
// anything, collapse the stack.
|
||||
if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
|
||||
mBubbleData.setExpanded(false);
|
||||
mStack.hideStackUserEducation(false /* fromExpansion */);
|
||||
resetForNextGesture();
|
||||
return false;
|
||||
}
|
||||
@@ -102,6 +102,7 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
|
||||
// Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
|
||||
// of expanded view).
|
||||
mStack.maybeShowManageEducation(false);
|
||||
resetForNextGesture();
|
||||
return false;
|
||||
}
|
||||
@@ -217,9 +218,8 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
}
|
||||
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
|
||||
mBubbleData.setExpanded(false);
|
||||
} else if (isStack || isFlyout) {
|
||||
// Toggle expansion
|
||||
mBubbleData.setExpanded(!mBubbleData.isExpanded());
|
||||
} else if (isStack) {
|
||||
mStack.onStackTapped();
|
||||
} else {
|
||||
final String key = ((BadgedImageView) mTouchedView).getKey();
|
||||
if (key == BubbleOverflow.KEY) {
|
||||
|
||||
@@ -879,9 +879,10 @@ public class StackAnimationController extends
|
||||
}
|
||||
|
||||
/** Moves the stack to a position instantly, with no animation. */
|
||||
private void setStackPosition(PointF pos) {
|
||||
public void setStackPosition(PointF pos) {
|
||||
Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
|
||||
mStackPosition.set(pos.x, pos.y);
|
||||
mRestingStackPosition = mStackPosition;
|
||||
|
||||
// If we're not the active controller, we don't want to physically move the bubble views.
|
||||
if (isActiveController()) {
|
||||
@@ -902,10 +903,10 @@ public class StackAnimationController extends
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the default stack position, which is on the top right. */
|
||||
private PointF getDefaultStartPosition() {
|
||||
/** Returns the default stack position, which is on the top left. */
|
||||
public PointF getDefaultStartPosition() {
|
||||
return new PointF(
|
||||
getAllowableStackPositionRegion().right,
|
||||
getAllowableStackPositionRegion().left,
|
||||
getAllowableStackPositionRegion().top + mStackStartingVerticalOffset);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user