Merge "Unifying focus indicator handling for workspace and all-apps" into ub-launcher3-calgary

This commit is contained in:
Sunny Goyal
2016-06-08 22:27:08 +00:00
committed by Android (Google) Code Review
18 changed files with 438 additions and 313 deletions
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2011 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="@color/focused_background" />
</shape>
</item>
</selector>
-5
View File
@@ -30,11 +30,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.launcher3.FocusIndicatorView
android:id="@+id/focus_indicator"
android:layout_width="52dp"
android:layout_height="52dp" />
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
-5
View File
@@ -31,11 +31,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.launcher3.FocusIndicatorView
android:id="@+id/focus_indicator"
android:layout_width="52dp"
android:layout_height="52dp" />
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
-5
View File
@@ -30,11 +30,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.launcher3.FocusIndicatorView
android:id="@+id/focus_indicator"
android:layout_width="52dp"
android:layout_height="52dp" />
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
-1
View File
@@ -24,6 +24,5 @@
android:paddingTop="@dimen/all_apps_icon_top_bottom_padding"
android:paddingBottom="@dimen/all_apps_icon_top_bottom_padding"
android:focusable="true"
android:background="@drawable/focusable_view_bg"
launcher:iconDisplay="all_apps" />
@@ -24,6 +24,5 @@
android:paddingTop="@dimen/all_apps_prediction_icon_top_padding"
android:paddingBottom="@dimen/all_apps_prediction_icon_bottom_padding"
android:focusable="true"
android:background="@drawable/focusable_view_bg"
launcher:iconDisplay="all_apps" />
+7 -20
View File
@@ -22,27 +22,14 @@
android:elevation="5dp"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/folder_content_wrapper"
<com.android.launcher3.folder.FolderPagedView
android:id="@+id/folder_content"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- Actual size of the indicator doesn't matter as it is scaled to match the view size -->
<com.android.launcher3.FocusIndicatorView
android:id="@+id/focus_indicator"
android:layout_width="20dp"
android:layout_height="20dp" />
<com.android.launcher3.folder.FolderPagedView
android:id="@+id/folder_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="8dp"
launcher:pageIndicator="@+id/folder_page_indicator" />
</FrameLayout>
android:layout_height="match_parent"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="8dp"
launcher:pageIndicator="@+id/folder_page_indicator" />
<LinearLayout
android:id="@+id/folder_footer"
+7 -20
View File
@@ -22,27 +22,14 @@
android:elevation="5dp"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/folder_content_wrapper"
<com.android.launcher3.folder.FolderPagedView
android:id="@+id/folder_content"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- Actual size of the indicator doesn't matter as it is scaled to match the view size -->
<com.android.launcher3.FocusIndicatorView
android:id="@+id/focus_indicator"
android:layout_width="20dp"
android:layout_height="20dp" />
<com.android.launcher3.folder.FolderPagedView
android:id="@+id/folder_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
launcher:pageIndicator="@+id/folder_page_indicator" />
</FrameLayout>
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
launcher:pageIndicator="@+id/folder_page_indicator" />
<LinearLayout
android:id="@+id/folder_footer"
@@ -1,203 +0,0 @@
/*
* Copyright (C) 2011 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;
import android.animation.Animator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.View;
import com.android.launcher3.util.Thunk;
public class FocusIndicatorView extends View implements View.OnFocusChangeListener {
// It can be any number >0. The view is resized using scaleX and scaleY.
static final int DEFAULT_LAYOUT_SIZE = 100;
private static final float MIN_VISIBLE_ALPHA = 0.2f;
private static final long ANIM_DURATION = 150;
private final int[] mIndicatorPos = new int[2];
private final int[] mTargetViewPos = new int[2];
private Animator mCurrentAnimation;
private ViewAnimState mTargetState;
private View mLastFocusedView;
private boolean mInitiated;
private final OnFocusChangeListener mHideIndicatorOnFocusListener;
private Pair<View, Boolean> mPendingCall;
public FocusIndicatorView(Context context) {
this(context, null);
}
public FocusIndicatorView(Context context, AttributeSet attrs) {
super(context, attrs);
setAlpha(0);
setBackgroundColor(getResources().getColor(R.color.focused_background));
mHideIndicatorOnFocusListener = new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
endCurrentAnimation();
setAlpha(0);
}
}
};
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Redraw if it is already showing. This avoids a bug where the height changes by a small
// amount on connecting/disconnecting a bluetooth keyboard.
if (mLastFocusedView != null) {
mPendingCall = Pair.create(mLastFocusedView, Boolean.TRUE);
invalidate();
}
}
/**
* Sets the alpha of this FocusIndicatorView to 0 when a view with this listener receives focus.
*/
public View.OnFocusChangeListener getHideIndicatorOnFocusListener() {
return mHideIndicatorOnFocusListener;
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
mPendingCall = null;
if (!mInitiated && (getWidth() == 0)) {
// View not yet laid out. Wait until the view is ready to be drawn, so that be can
// get the location on screen.
mPendingCall = Pair.create(v, hasFocus);
invalidate();
return;
}
if (!mInitiated) {
// The parent view should always the a parent of the target view.
computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos);
mInitiated = true;
}
if (hasFocus) {
int indicatorWidth = getWidth();
int indicatorHeight = getHeight();
endCurrentAnimation();
ViewAnimState nextState = new ViewAnimState();
nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth;
nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight;
computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos);
nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2;
nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2;
if (getAlpha() > MIN_VISIBLE_ALPHA) {
mTargetState = nextState;
mCurrentAnimation = new LauncherViewPropertyAnimator(this)
.alpha(1)
.translationX(mTargetState.x)
.translationY(mTargetState.y)
.scaleX(mTargetState.scaleX)
.scaleY(mTargetState.scaleY);
} else {
applyState(nextState);
mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat(View.ALPHA, 1));
}
mLastFocusedView = v;
} else {
if (mLastFocusedView == v) {
mLastFocusedView = null;
endCurrentAnimation();
mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat(View.ALPHA, 0));
}
}
if (mCurrentAnimation != null) {
mCurrentAnimation.setDuration(ANIM_DURATION).start();
}
}
private void endCurrentAnimation() {
if (mCurrentAnimation != null) {
mCurrentAnimation.cancel();
mCurrentAnimation = null;
}
if (mTargetState != null) {
applyState(mTargetState);
mTargetState = null;
}
}
private void applyState(ViewAnimState state) {
setTranslationX(state.x);
setTranslationY(state.y);
setScaleX(state.scaleX);
setScaleY(state.scaleY);
}
@Override
protected void onDraw(Canvas canvas) {
if (mPendingCall != null) {
onFocusChange(mPendingCall.first, mPendingCall.second);
}
}
/**
* Computes the location of a view relative to {@param parent}, off-setting
* any shift due to page view scroll.
* @param pos an array of two integers in which to hold the coordinates
*/
private static void computeLocationRelativeToParent(View v, View parent, int[] pos) {
pos[0] = pos[1] = 0;
computeLocationRelativeToParentHelper(v, parent, pos);
// If a view is scaled, its position will also shift accordingly. For optimization, only
// consider this for the last node.
pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2;
pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2;
}
private static void computeLocationRelativeToParentHelper(View child,
View commonParent, int[] shift) {
View parent = (View) child.getParent();
shift[0] += child.getLeft();
shift[1] += child.getTop();
if (parent instanceof PagedView) {
PagedView page = (PagedView) parent;
shift[0] -= page.getScrollForPage(page.indexOfChild(child));
}
if (parent != commonParent) {
computeLocationRelativeToParentHelper(parent, commonParent, shift);
}
}
@Thunk static final class ViewAnimState {
float x, y, scaleX, scaleY;
}
}
+4 -2
View File
@@ -107,6 +107,7 @@ import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.dynamicui.ExtractedColors;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.logging.LoggerUtils;
import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.model.WidgetsModel;
@@ -363,7 +364,7 @@ public class Launcher extends Activity
private UserEventDispatcher mUserEventDispatcher;
public FocusIndicatorView mFocusHandler;
public ViewGroupFocusHelper mFocusHandler;
private boolean mRotationEnabled = false;
@Thunk void setOrientation() {
@@ -1340,8 +1341,9 @@ public class Launcher extends Activity
*/
private void setupViews() {
mLauncherView = findViewById(R.id.launcher);
mFocusHandler = (FocusIndicatorView) findViewById(R.id.focus_indicator);
mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
mFocusHandler = mDragLayer.getFocusIndicatorHelper();
mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
mPageIndicator = (PageIndicatorLine) mDragLayer.findViewById(R.id.page_indicator);
@@ -52,6 +52,7 @@ import com.android.launcher3.LauncherTransitionable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.keyboard.FocusedItemDecorator;
import com.android.launcher3.util.ComponentKey;
import java.nio.charset.Charset;
@@ -311,6 +312,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mAppsRecyclerView.addItemDecoration(mItemDecoration);
}
FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mAppsRecyclerView);
mAppsRecyclerView.addItemDecoration(focusedItemDecorator);
mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
// Precalculate the prediction icon and normal icon sizes
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
@@ -33,6 +33,7 @@ import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
@@ -348,6 +349,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
private BindViewCallback mBindViewCallback;
private AllAppsSearchBarController mSearchController;
private OnFocusChangeListener mIconFocusListener;
// The text to show when there are no search results and no market search handler.
private String mEmptySearchMessage;
@@ -412,6 +414,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
}
}
public void setIconFocusListener(OnFocusChangeListener focusListener) {
mIconFocusListener = focusListener;
}
/**
* Sets the last search query that was made, used to show when there are no results and to also
* seed the intent for searching the market.
@@ -461,26 +467,17 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
switch (viewType) {
case SECTION_BREAK_VIEW_TYPE:
return new ViewHolder(new View(parent.getContext()));
case ICON_VIEW_TYPE: {
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.all_apps_icon, parent, false);
icon.setOnTouchListener(mTouchListener);
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
.getLongPressTimeout());
icon.setFocusable(true);
return new ViewHolder(icon);
}
case ICON_VIEW_TYPE:
case PREDICTION_ICON_VIEW_TYPE: {
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.all_apps_prediction_bar_icon, parent, false);
viewType == ICON_VIEW_TYPE ? R.layout.all_apps_icon :
R.layout.all_apps_prediction_bar_icon, parent, false);
icon.setOnTouchListener(mTouchListener);
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
.getLongPressTimeout());
icon.setFocusable(true);
icon.setOnFocusChangeListener(mIconFocusListener);
return new ViewHolder(icon);
}
case EMPTY_SEARCH_VIEW_TYPE:
@@ -61,6 +61,7 @@ import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
@@ -111,6 +112,8 @@ public class DragLayer extends InsettableFrameLayout {
// Related to adjacent page hints
private final Rect mScrollChildPosition = new Rect();
private final ViewGroupFocusHelper mFocusIndicatorHelper;
private boolean mInScrollArea;
private boolean mShowPageHints;
private Drawable mLeftHoverDrawable;
@@ -144,6 +147,7 @@ public class DragLayer extends InsettableFrameLayout {
mLeftHoverDrawableActive = res.getDrawable(R.drawable.page_hover_left_active);
mRightHoverDrawableActive = res.getDrawable(R.drawable.page_hover_right_active);
mIsRtl = Utilities.isRtl(res);
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
}
public void setup(Launcher launcher, DragController dragController,
@@ -157,6 +161,10 @@ public class DragLayer extends InsettableFrameLayout {
onAccessibilityStateChanged(isAccessibilityEnabled);
}
public ViewGroupFocusHelper getFocusIndicatorHelper() {
return mFocusIndicatorHelper;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
@@ -966,6 +974,7 @@ public class DragLayer extends InsettableFrameLayout {
canvas.restore();
}
mFocusIndicatorHelper.draw(canvas);
super.dispatchDraw(canvas);
}
+6 -8
View File
@@ -98,7 +98,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
private static final String TAG = "Launcher.Folder";
/**
* We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this
* We avoid measuring {@link #mContent} with a 0 width or height, as this
* results in CellLayout being measured as UNSPECIFIED, which it does not support.
*/
private static final int MIN_CONTENT_DIMEN = 5;
@@ -147,7 +147,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
@Thunk FolderIcon mFolderIcon;
@Thunk FolderPagedView mContent;
@Thunk View mContentWrapper;
public ExtendedEditText mFolderName;
private PageIndicatorDots mPageIndicator;
@@ -226,7 +225,6 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContentWrapper = findViewById(R.id.folder_content_wrapper);
mContent = (FolderPagedView) findViewById(R.id.folder_content);
mContent.setFolder(this);
@@ -562,8 +560,8 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
reveal.setDuration(mMaterialExpandDuration);
reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
mContentWrapper.setAlpha(0f);
Animator iconsAlpha = ObjectAnimator.ofFloat(mContentWrapper, "alpha", 0f, 1f);
mContent.setAlpha(0f);
Animator iconsAlpha = ObjectAnimator.ofFloat(mContent, "alpha", 0f, 1f);
iconsAlpha.setDuration(mMaterialExpandDuration);
iconsAlpha.setStartDelay(mMaterialExpandStagger);
iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
@@ -581,12 +579,12 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
openFolderAnim = anim;
mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null);
mContent.setLayerType(LAYER_TYPE_HARDWARE, null);
mFooter.setLayerType(LAYER_TYPE_HARDWARE, null);
onCompleteRunnable = new Runnable() {
@Override
public void run() {
mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
mContent.setLayerType(LAYER_TYPE_NONE, null);
mFooter.setLayerType(LAYER_TYPE_NONE, null);
}
};
@@ -1122,7 +1120,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
mContent.setFixedSize(contentWidth, contentHeight);
mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec);
mContent.measure(contentAreaWidthSpec, contentAreaHeightSpec);
if (mContent.getChildCount() > 0) {
int cellIconGap = (mContent.getPageAt(0).getCellWidth()
@@ -18,6 +18,7 @@ package com.android.launcher3.folder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
@@ -30,7 +31,6 @@ import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
import com.android.launcher3.FocusIndicatorView;
import com.android.launcher3.IconCache;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
@@ -45,6 +45,7 @@ import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.pageindicators.PageIndicator;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.util.Thunk;
import java.util.ArrayList;
@@ -73,6 +74,7 @@ public class FolderPagedView extends PagedView {
private final LayoutInflater mInflater;
private final IconCache mIconCache;
private final ViewGroupFocusHelper mFocusIndicatorHelper;
@Thunk final HashMap<View, Runnable> mPendingAnimations = new HashMap<>();
@@ -90,7 +92,6 @@ public class FolderPagedView extends PagedView {
private int mGridCountY;
private Folder mFolder;
private FocusIndicatorView mFocusIndicatorView;
private PagedFolderKeyEventListener mKeyListener;
private PageIndicator mPageIndicator;
@@ -112,11 +113,11 @@ public class FolderPagedView extends PagedView {
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
setEdgeGlowColor(getResources().getColor(R.color.folder_edge_effect_color));
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
}
public void setFolder(Folder folder) {
mFolder = folder;
mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
mKeyListener = new PagedFolderKeyEventListener(folder);
mPageIndicator = (PageIndicator) folder.findViewById(R.id.folder_page_indicator);
}
@@ -162,6 +163,12 @@ public class FolderPagedView extends PagedView {
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
mFocusIndicatorHelper.draw(canvas);
super.dispatchDraw(canvas);
}
/**
* Binds items to the layout.
* @return list of items that could not be bound, probably because we hit the max size limit.
@@ -226,7 +233,7 @@ public class FolderPagedView extends PagedView {
textView.applyFromShortcutInfo(item, mIconCache);
textView.setOnClickListener(mFolder);
textView.setOnLongClickListener(mFolder);
textView.setOnFocusChangeListener(mFocusIndicatorView);
textView.setOnFocusChangeListener(mFocusIndicatorHelper);
textView.setOnKeyListener(mKeyListener);
textView.setLayoutParams(new CellLayout.LayoutParams(
@@ -0,0 +1,242 @@
/*
* Copyright (C) 2016 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.keyboard;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build.VERSION_CODES;
import android.util.Property;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import com.android.launcher3.R;
/**
* A helper class to draw background of a focused view.
*/
@TargetApi(VERSION_CODES.LOLLIPOP)
public abstract class FocusIndicatorHelper implements
OnFocusChangeListener, AnimatorUpdateListener {
private static final float MIN_VISIBLE_ALPHA = 0.2f;
private static final long ANIM_DURATION = 150;
public static final Property<FocusIndicatorHelper, Float> ALPHA =
new Property<FocusIndicatorHelper, Float>(Float.TYPE, "alpha") {
@Override
public void set(FocusIndicatorHelper object, Float value) {
object.setAlpha(value);
}
@Override
public Float get(FocusIndicatorHelper object) {
return object.mAlpha;
}
};
public static final Property<FocusIndicatorHelper, Float> SHIFT =
new Property<FocusIndicatorHelper, Float>(
Float.TYPE, "shift") {
@Override
public void set(FocusIndicatorHelper object, Float value) {
object.mShift = value;
}
@Override
public Float get(FocusIndicatorHelper object) {
return object.mShift;
}
};
private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
private static final Rect sTempRect1 = new Rect();
private static final Rect sTempRect2 = new Rect();
private final View mContainer;
private final Paint mPaint;
private final int mMaxAlpha;
private final Rect mDirtyRect = new Rect();
private boolean mIsDirty = false;
private View mLastFocusedView;
private View mCurrentView;
private View mTargetView;
/**
* The fraction indicating the position of the focusRect between {@link #mCurrentView}
* & {@link #mTargetView}
*/
private float mShift;
private ObjectAnimator mCurrentAnimation;
private float mAlpha;
public FocusIndicatorHelper(View container) {
mContainer = container;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
int color = container.getResources().getColor(R.color.focused_background);
mMaxAlpha = Color.alpha(color);
mPaint.setColor(0xFF000000 | color);
setAlpha(0);
mShift = 0;
}
protected void setAlpha(float alpha) {
mAlpha = alpha;
mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidateDirty();
}
protected void invalidateDirty() {
if (mIsDirty) {
mContainer.invalidate(mDirtyRect);
mIsDirty = false;
}
Rect newRect = getDrawRect();
if (newRect != null) {
mContainer.invalidate(newRect);
}
}
public void draw(Canvas c) {
if (mAlpha > 0) {
Rect newRect = getDrawRect();
if (newRect != null) {
mDirtyRect.set(newRect);
c.drawRect(mDirtyRect, mPaint);
mIsDirty = true;
}
}
}
private Rect getDrawRect() {
if (mCurrentView != null) {
viewToRect(mCurrentView, sTempRect1);
if (mShift > 0 && mTargetView != null) {
viewToRect(mTargetView, sTempRect2);
return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
} else {
return sTempRect1;
}
}
return null;
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
endCurrentAnimation();
if (mAlpha > MIN_VISIBLE_ALPHA) {
mTargetView = v;
mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat(ALPHA, 1),
PropertyValuesHolder.ofFloat(SHIFT, 1));
mCurrentAnimation.addListener(new ViewSetListener(v, true));
} else {
setCurrentView(v);
mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat(ALPHA, 1));
}
mLastFocusedView = v;
} else {
if (mLastFocusedView == v) {
mLastFocusedView = null;
endCurrentAnimation();
mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofFloat(ALPHA, 0));
mCurrentAnimation.addListener(new ViewSetListener(null, false));
}
}
// invalidate once
invalidateDirty();
mLastFocusedView = hasFocus ? v : null;
if (mCurrentAnimation != null) {
mCurrentAnimation.addUpdateListener(this);
mCurrentAnimation.setDuration(ANIM_DURATION).start();
}
}
protected void endCurrentAnimation() {
if (mCurrentAnimation != null) {
mCurrentAnimation.cancel();
mCurrentAnimation = null;
}
}
protected void setCurrentView(View v) {
mCurrentView = v;
mShift = 0;
mTargetView = null;
}
/**
* Gets the position of {@param v} relative to {@link #mContainer}.
*/
public abstract void viewToRect(View v, Rect outRect);
private class ViewSetListener extends AnimatorListenerAdapter {
private final View mViewToSet;
private final boolean mCallOnCancel;
private boolean mCalled = false;
public ViewSetListener(View v, boolean callOnCancel) {
mViewToSet = v;
mCallOnCancel = callOnCancel;
}
@Override
public void onAnimationCancel(Animator animation) {
if (!mCallOnCancel) {
mCalled = true;
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (!mCalled) {
setCurrentView(mViewToSet);
mCalled = true;
}
}
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2016 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.keyboard;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ItemDecoration;
import android.support.v7.widget.RecyclerView.State;
import android.view.View;
import android.view.View.OnFocusChangeListener;
/**
* {@link ItemDecoration} for drawing and animating focused view background.
*/
public class FocusedItemDecorator extends ItemDecoration {
private FocusIndicatorHelper mHelper;
public FocusedItemDecorator(View container) {
mHelper = new FocusIndicatorHelper(container) {
@Override
public void viewToRect(View v, Rect outRect) {
outRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
}
};
}
public OnFocusChangeListener getFocusListener() {
return mHelper;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, State state) {
mHelper.draw(c);
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2016 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.keyboard;
import android.graphics.Rect;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import com.android.launcher3.PagedView;
/**
* {@link FocusIndicatorHelper} for a generic view group.
*/
public class ViewGroupFocusHelper extends FocusIndicatorHelper {
private final View mContainer;
public ViewGroupFocusHelper(View container) {
super(container);
mContainer = container;
}
@Override
public void viewToRect(View v, Rect outRect) {
outRect.left = 0;
outRect.top = 0;
computeLocationRelativeToContainer(v, outRect);
// If a view is scaled, its position will also shift accordingly. For optimization, only
// consider this for the last node.
outRect.left += (1 - v.getScaleX()) * v.getWidth() / 2;
outRect.top += (1 - v.getScaleY()) * v.getHeight() / 2;
outRect.right = outRect.left + (int) (v.getScaleX() * v.getWidth());
outRect.bottom = outRect.top + (int) (v.getScaleY() * v.getHeight());
}
private void computeLocationRelativeToContainer(View child, Rect outRect) {
View parent = (View) child.getParent();
outRect.left += child.getLeft();
outRect.top += child.getTop();
if (parent != mContainer) {
if (parent instanceof PagedView) {
PagedView page = (PagedView) parent;
outRect.left -= page.getScrollForPage(page.indexOfChild(child));
}
computeLocationRelativeToContainer(parent, outRect);
}
}
/**
* Sets the alpha of this FocusIndicatorHelper to 0 when a view with this listener
* receives focus.
*/
public View.OnFocusChangeListener getHideIndicatorOnFocusListener() {
return new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
endCurrentAnimation();
setCurrentView(null);
setAlpha(0);
invalidateDirty();
}
}
};
}
}