Bottom user education view shown in work tab

Introduced a BottomUserEducationView for similar use case.

Screenshot: https://hsv.googleplex.com/4856820942241792

BUG=69963630

Change-Id: Ia818ee44fa5ce97ad1778f33e6a9a3a36cea5017
This commit is contained in:
Tony Mak 2017-11-29 18:39:49 +00:00 committed by Sunny Goyal
parent 4018f3df14
commit 191b688cc8
11 changed files with 369 additions and 145 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.launcher3.views.BottomUserEducationView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?android:attr/colorAccent"
android:elevation="2dp"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingRight="20dp">
<ImageView
android:layout_width="134dp"
android:layout_height="134dp"
android:layout_gravity="center_vertical"
android:src="@drawable/work_tab_user_education"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="12dp"
android:paddingStart="24dp"
android:paddingTop="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="roboto-medium"
android:text="@string/bottom_work_tab_user_education_title"
android:textColor="@android:color/white"
android:textSize="20sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/bottom_work_tab_user_education_body"
android:textColor="@android:color/white"
android:textSize="14sp"/>
</LinearLayout>
</com.android.launcher3.views.BottomUserEducationView>

View File

@ -326,5 +326,9 @@
<!-- Label of the work mode toggle -->
<string name="work_profile_toggle_label">Work profile</string>
<!-- Title in bottom user education view in work tab -->
<string name="bottom_work_tab_user_education_title">Find work apps here</string>
<!-- Body text in bottom user education view in work tab -->
<string name="bottom_work_tab_user_education_body">Each work app has an orange badge, which means it\'s kept secure by your organization. Work apps can be moved to your Home Screen for easier access.</string>
</resources>

View File

@ -42,7 +42,8 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch
TYPE_WIDGETS_BOTTOM_SHEET,
TYPE_WIDGET_RESIZE_FRAME,
TYPE_WIDGETS_FULL_SHEET,
TYPE_QUICKSTEP_PREVIEW
TYPE_QUICKSTEP_PREVIEW,
TYPE_ON_BOARD_POPUP
})
@Retention(RetentionPolicy.SOURCE)
public @interface FloatingViewType {}
@ -52,10 +53,11 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch
public static final int TYPE_WIDGET_RESIZE_FRAME = 1 << 3;
public static final int TYPE_WIDGETS_FULL_SHEET = 1 << 4;
public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 5;
public static final int TYPE_ON_BOARD_POPUP = 1 << 6;
public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
| TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
| TYPE_QUICKSTEP_PREVIEW;
| TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP;
protected boolean mIsOpen;

View File

@ -60,6 +60,7 @@ import com.android.launcher3.util.ComponentKeyMapper;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.TransformingTouchDelegate;
import com.android.launcher3.views.BottomUserEducationView;
import com.android.launcher3.views.SlidingTabStrip;
import java.util.HashMap;
@ -515,6 +516,9 @@ public class AllAppsContainerView extends RelativeLayout implements DragSource,
if (mAH[pos].recyclerView != null) {
mAH[pos].recyclerView.bindFastScrollbar();
}
if (pos == AdapterHolder.WORK) {
BottomUserEducationView.showIfNeeded(mLauncher);
}
}
@Override

View File

@ -0,0 +1,182 @@
/*
* Copyright (C) 2017 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.launcher3.views;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Interpolator;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.touch.SwipeDetector;
/**
* Extension of AbstractFloatingView with common methods for sliding in from bottom
*/
public abstract class AbstractSlideInView extends AbstractFloatingView
implements SwipeDetector.Listener {
protected static Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
@Override
public Float get(AbstractSlideInView view) {
return view.mTranslationShift;
}
@Override
public void set(AbstractSlideInView view, Float value) {
view.setTranslationShift(value);
}
};
protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
protected static final float TRANSLATION_SHIFT_OPENED = 0f;
protected final Launcher mLauncher;
protected final SwipeDetector mSwipeDetector;
protected final ObjectAnimator mOpenCloseAnimator;
protected View mContent;
protected Interpolator mScrollInterpolator;
// range [0, 1], 0=> completely open, 1=> completely closed
protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
protected boolean mNoIntercept;
public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mLauncher = Launcher.getLauncher(context);
mScrollInterpolator = Interpolators.SCROLL_CUBIC;
mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL);
mOpenCloseAnimator = LauncherAnimUtils.ofPropertyValuesHolder(this);
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mSwipeDetector.finishedScrolling();
}
});
}
protected void setTranslationShift(float translationShift) {
mTranslationShift = translationShift;
mContent.setTranslationY(mTranslationShift * mContent.getHeight());
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (mNoIntercept) {
return false;
}
int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
SwipeDetector.DIRECTION_NEGATIVE : 0;
mSwipeDetector.setDetectableScrollConditions(
directionsToDetectScroll, false);
mSwipeDetector.onTouchEvent(ev);
return mSwipeDetector.isDraggingOrSettling()
|| !mLauncher.getDragLayer().isEventOverView(mContent, ev);
}
@Override
public boolean onControllerTouchEvent(MotionEvent ev) {
mSwipeDetector.onTouchEvent(ev);
if (ev.getAction() == MotionEvent.ACTION_UP && mSwipeDetector.isIdleState()) {
// If we got ACTION_UP without ever starting swipe, close the panel.
if (!mLauncher.getDragLayer().isEventOverView(mContent, ev)) {
close(true);
}
}
return true;
}
/* SwipeDetector.Listener */
@Override
public void onDragStart(boolean start) { }
@Override
public boolean onDrag(float displacement, float velocity) {
float range = mContent.getHeight();
displacement = Utilities.boundToRange(displacement, 0, range);
setTranslationShift(displacement / range);
return true;
}
@Override
public void onDragEnd(float velocity, boolean fling) {
if ((fling && velocity > 0) || mTranslationShift > 0.5f) {
mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(
velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
close(true);
} else {
mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
mOpenCloseAnimator.setDuration(
SwipeDetector.calculateDuration(velocity, mTranslationShift))
.setInterpolator(Interpolators.DEACCEL);
mOpenCloseAnimator.start();
}
}
protected void handleClose(boolean animate, long defaultDuration) {
if (mIsOpen && !animate) {
mOpenCloseAnimator.cancel();
setTranslationShift(TRANSLATION_SHIFT_CLOSED);
onCloseComplete();
return;
}
if (!mIsOpen || mOpenCloseAnimator.isRunning()) {
return;
}
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
onCloseComplete();
}
});
if (mSwipeDetector.isIdleState()) {
mOpenCloseAnimator
.setDuration(defaultDuration)
.setInterpolator(Interpolators.ACCEL);
} else {
mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
}
mOpenCloseAnimator.start();
}
protected void onCloseComplete() {
mIsOpen = false;
mLauncher.getDragLayer().removeView(this);
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (C) 2017 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.launcher3.views;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators;
public class BottomUserEducationView extends AbstractSlideInView implements Insettable {
private static final String KEY_SHOWED_BOTTOM_USER_EDUCATION = "showed_bottom_user_education";
private static final int DEFAULT_CLOSE_DURATION = 200;
private final Rect mInsets = new Rect();
public BottomUserEducationView(Context context, AttributeSet attr) {
this(context, attr, 0);
}
public BottomUserEducationView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContent = this;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
setTranslationShift(mTranslationShift);
}
@Override
public void logActionCommand(int command) {
// Since this is on-boarding popup, it is not a user controlled action.
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_ON_BOARD_POPUP) != 0;
}
@Override
public void setInsets(Rect insets) {
// Extend behind left, right, and bottom insets.
int leftInset = insets.left - mInsets.left;
int rightInset = insets.right - mInsets.right;
int bottomInset = insets.bottom - mInsets.bottom;
mInsets.set(insets);
setPadding(getPaddingLeft() + leftInset, getPaddingTop(),
getPaddingRight() + rightInset, getPaddingBottom() + bottomInset);
}
@Override
protected void handleClose(boolean animate) {
handleClose(animate, DEFAULT_CLOSE_DURATION);
if (animate) {
// We animate only when the user is visible, which is a proxy for an explicit
// close action.
mLauncher.getSharedPrefs().edit()
.putBoolean(KEY_SHOWED_BOTTOM_USER_EDUCATION, true).apply();
}
}
private void open(boolean animate) {
if (mIsOpen || mOpenCloseAnimator.isRunning()) {
return;
}
mIsOpen = true;
if (animate) {
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mOpenCloseAnimator.start();
} else {
setTranslationShift(TRANSLATION_SHIFT_OPENED);
}
}
public static void showIfNeeded(Launcher launcher) {
if (launcher.getSharedPrefs().getBoolean(KEY_SHOWED_BOTTOM_USER_EDUCATION, false)) {
return;
}
LayoutInflater layoutInflater = LayoutInflater.from(launcher);
BottomUserEducationView bottomUserEducationView =
(BottomUserEducationView) layoutInflater.inflate(
R.layout.work_tab_bottom_user_education_view, launcher.getDragLayer(),
false);
launcher.getDragLayer().addView(bottomUserEducationView);
bottomUserEducationView.open(true);
}
}

View File

@ -15,94 +15,43 @@
*/
package com.android.launcher3.widget;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.animation.Interpolator;
import android.widget.Toast;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.graphics.GradientView;
import com.android.launcher3.touch.SwipeDetector;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.AbstractSlideInView;
/**
* Base class for various widgets popup
*/
abstract class BaseWidgetSheet extends AbstractFloatingView
implements OnClickListener, OnLongClickListener, DragSource, SwipeDetector.Listener {
abstract class BaseWidgetSheet extends AbstractSlideInView
implements OnClickListener, OnLongClickListener, DragSource {
protected static Property<BaseWidgetSheet, Float> TRANSLATION_SHIFT =
new Property<BaseWidgetSheet, Float>(Float.class, "translationShift") {
@Override
public Float get(BaseWidgetSheet view) {
return view.mTranslationShift;
}
@Override
public void set(BaseWidgetSheet view, Float value) {
view.setTranslationShift(value);
}
};
protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
protected static final float TRANSLATION_SHIFT_OPENED = 0f;
/* Touch handling related member variables. */
private Toast mWidgetInstructionToast;
protected final Launcher mLauncher;
protected final SwipeDetector mSwipeDetector;
protected final ObjectAnimator mOpenCloseAnimator;
protected View mContent;
protected GradientView mGradientView;
protected Interpolator mScrollInterpolator;
// range [0, 1], 0=> completely open, 1=> completely closed
protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
protected boolean mNoIntercept;
public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mLauncher = Launcher.getLauncher(context);
mScrollInterpolator = Interpolators.SCROLL_CUBIC;
mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL);
mOpenCloseAnimator = LauncherAnimUtils.ofPropertyValuesHolder(this);
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mSwipeDetector.finishedScrolling();
}
});
}
@Override
@ -130,9 +79,8 @@ abstract class BaseWidgetSheet extends AbstractFloatingView
}
protected void setTranslationShift(float translationShift) {
mTranslationShift = translationShift;
super.setTranslationShift(translationShift);
mGradientView.setAlpha(1 - mTranslationShift);
mContent.setTranslationY(mTranslationShift * mContent.getHeight());
}
private boolean beginDraggingWidget(WidgetCell v) {
@ -163,94 +111,8 @@ abstract class BaseWidgetSheet extends AbstractFloatingView
public void onDropCompleted(View target, DragObject d, boolean success) { }
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP && !mNoIntercept) {
// If we got ACTION_UP without ever returning true on intercept,
// the user never started dragging the bottom sheet.
if (!mLauncher.getDragLayer().isEventOverView(mContent, ev)) {
close(true);
return false;
}
}
if (mNoIntercept) {
return false;
}
int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
SwipeDetector.DIRECTION_NEGATIVE : 0;
mSwipeDetector.setDetectableScrollConditions(
directionsToDetectScroll, false);
mSwipeDetector.onTouchEvent(ev);
return mSwipeDetector.isDraggingOrSettling();
}
@Override
public boolean onControllerTouchEvent(MotionEvent ev) {
return mSwipeDetector.onTouchEvent(ev);
}
/* SwipeDetector.Listener */
@Override
public void onDragStart(boolean start) { }
@Override
public boolean onDrag(float displacement, float velocity) {
float range = mContent.getHeight();
displacement = Utilities.boundToRange(displacement, 0, range);
setTranslationShift(displacement / range);
return true;
}
@Override
public void onDragEnd(float velocity, boolean fling) {
if ((fling && velocity > 0) || mTranslationShift > 0.5f) {
mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(
velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
close(true);
} else {
mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
mOpenCloseAnimator.setDuration(
SwipeDetector.calculateDuration(velocity, mTranslationShift))
.setInterpolator(Interpolators.DEACCEL);
mOpenCloseAnimator.start();
}
}
protected void handleClose(boolean animate, long defaultDuration) {
if (!mIsOpen || mOpenCloseAnimator.isRunning()) {
return;
}
if (animate) {
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
onCloseComplete();
}
});
if (mSwipeDetector.isIdleState()) {
mOpenCloseAnimator
.setDuration(defaultDuration)
.setInterpolator(Interpolators.ACCEL);
} else {
mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
}
mOpenCloseAnimator.start();
} else {
setTranslationShift(TRANSLATION_SHIFT_CLOSED);
onCloseComplete();
}
}
protected void onCloseComplete() {
mIsOpen = false;
mLauncher.getDragLayer().removeView(this);
super.onCloseComplete();
mLauncher.getSystemUiController().updateUiState(
SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0);
}