• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.keyboard;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.ObjectAnimator;
21 import android.animation.PropertyValuesHolder;
22 import android.animation.RectEvaluator;
23 import android.animation.ValueAnimator;
24 import android.animation.ValueAnimator.AnimatorUpdateListener;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Paint;
28 import android.graphics.Rect;
29 import android.util.FloatProperty;
30 import android.view.View;
31 
32 import com.android.launcher3.Flags;
33 import com.android.launcher3.R;
34 
35 /**
36  * A helper class to draw background of a focused item.
37  * @param <T> Item type
38  */
39 public abstract class ItemFocusIndicatorHelper<T> implements AnimatorUpdateListener {
40 
41     private static final float MIN_VISIBLE_ALPHA = 0.2f;
42     private static final long ANIM_DURATION = 150;
43 
44     public static final FloatProperty<ItemFocusIndicatorHelper> ALPHA =
45             new FloatProperty<ItemFocusIndicatorHelper>("alpha") {
46 
47                 @Override
48                 public void setValue(ItemFocusIndicatorHelper object, float value) {
49                     object.setAlpha(value);
50                 }
51 
52                 @Override
53                 public Float get(ItemFocusIndicatorHelper object) {
54                     return object.mAlpha;
55                 }
56             };
57 
58     public static final FloatProperty<ItemFocusIndicatorHelper> SHIFT =
59             new FloatProperty<ItemFocusIndicatorHelper>("shift") {
60 
61                 @Override
62                 public void setValue(ItemFocusIndicatorHelper object, float value) {
63                     object.mShift = value;
64                 }
65 
66                 @Override
67                 public Float get(ItemFocusIndicatorHelper object) {
68                     return object.mShift;
69                 }
70             };
71 
72     private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
73     private static final Rect sTempRect1 = new Rect();
74     private static final Rect sTempRect2 = new Rect();
75 
76     private final View mContainer;
77     protected final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
78     private final Paint mInnerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
79     private final int mMaxAlpha;
80 
81     private final Rect mDirtyRect = new Rect();
82     private boolean mIsDirty = false;
83 
84     private T mLastFocusedItem;
85 
86     private T mCurrentItem;
87     private T mTargetItem;
88     /**
89      * The fraction indicating the position of the focusRect between {@link #mCurrentItem}
90      * & {@link #mTargetItem}
91      */
92     private float mShift;
93 
94     private ObjectAnimator mCurrentAnimation;
95     private float mAlpha;
96     private float mRadius;
97     private float mInnerRadius;
98 
ItemFocusIndicatorHelper(View container, int... colors)99     public ItemFocusIndicatorHelper(View container, int... colors) {
100         mContainer = container;
101 
102         mPaint.setColor(0xFF000000 | colors[0]);
103         if (Flags.enableFocusOutline() && colors.length > 1) {
104             mPaint.setStyle(Paint.Style.STROKE);
105             mPaint.setStrokeWidth(container.getResources().getDimensionPixelSize(
106                     R.dimen.focus_outline_stroke_width));
107             mRadius = container.getResources().getDimensionPixelSize(
108                     R.dimen.focus_outline_radius);
109 
110             mInnerPaint.setStyle(Paint.Style.STROKE);
111             mInnerPaint.setColor(0xFF000000 | colors[1]);
112             mInnerPaint.setStrokeWidth(container.getResources().getDimensionPixelSize(
113                     R.dimen.focus_outline_stroke_width));
114             mInnerRadius = container.getResources().getDimensionPixelSize(
115                     R.dimen.focus_inner_outline_radius);
116         } else {
117             mPaint.setStyle(Paint.Style.FILL);
118             mRadius = container.getResources().getDimensionPixelSize(
119                     R.dimen.grid_visualization_rounding_radius);
120         }
121         mMaxAlpha = Color.alpha(colors[0]);
122 
123         setAlpha(0);
124         mShift = 0;
125     }
126 
setAlpha(float alpha)127     protected void setAlpha(float alpha) {
128         mAlpha = alpha;
129         mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
130         mInnerPaint.setAlpha((int) (mAlpha * mMaxAlpha));
131     }
132 
133     @Override
onAnimationUpdate(ValueAnimator animation)134     public void onAnimationUpdate(ValueAnimator animation) {
135         invalidateDirty();
136     }
137 
invalidateDirty()138     protected void invalidateDirty() {
139         if (mIsDirty) {
140             mContainer.invalidate(mDirtyRect);
141             mIsDirty = false;
142         }
143 
144         Rect newRect = getDrawRect();
145         if (newRect != null) {
146             mContainer.invalidate(newRect);
147         }
148     }
149 
150     /**
151      * Draws the indicator on the canvas
152      */
draw(Canvas c)153     public void draw(Canvas c) {
154         if (mAlpha <= 0) return;
155 
156         Rect newRect = getDrawRect();
157         if (newRect != null) {
158             if (Flags.enableFocusOutline()) {
159                 int strokeWidth = (int) mPaint.getStrokeWidth();
160                 // Inset for inner outline. Stroke is drawn with half outside and half inside
161                 // the view. Inset by half stroke width to move the whole stroke inside the view
162                 // and avoid other views occluding it. Inset one more stroke width to leave space
163                 // for outer outline.
164                 newRect.inset((int) (strokeWidth * 1.5), (int) (strokeWidth * 1.5));
165                 c.drawRoundRect((float) newRect.left, (float) newRect.top,
166                         (float) newRect.right, (float) newRect.bottom,
167                         mInnerRadius, mInnerRadius, mInnerPaint);
168 
169                 // Inset outward for drawing outer outline
170                 newRect.inset(-strokeWidth, -strokeWidth);
171             }
172             mDirtyRect.set(newRect);
173             c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top,
174                     (float) mDirtyRect.right, (float) mDirtyRect.bottom,
175                     mRadius, mRadius, mPaint);
176             mIsDirty = true;
177         }
178     }
179 
getDrawRect()180     private Rect getDrawRect() {
181         if (mCurrentItem != null && shouldDraw(mCurrentItem)) {
182             viewToRect(mCurrentItem, sTempRect1);
183 
184             if (mShift > 0 && mTargetItem != null) {
185                 viewToRect(mTargetItem, sTempRect2);
186                 return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
187             } else {
188                 return sTempRect1;
189             }
190         }
191         return null;
192     }
193 
194     /**
195      * Returns true if the provided item is valid
196      */
shouldDraw(T item)197     protected boolean shouldDraw(T item) {
198         return true;
199     }
200 
changeFocus(T item, boolean hasFocus)201     protected void changeFocus(T item, boolean hasFocus) {
202         if (mLastFocusedItem != item && !hasFocus) {
203             return;
204         }
205 
206         if (hasFocus) {
207             endCurrentAnimation();
208 
209             if (mAlpha > MIN_VISIBLE_ALPHA) {
210                 mTargetItem = item;
211 
212                 mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
213                         PropertyValuesHolder.ofFloat(ALPHA, 1),
214                         PropertyValuesHolder.ofFloat(SHIFT, 1));
215                 mCurrentAnimation.addListener(new ViewSetListener(item, true));
216             } else {
217                 setCurrentItem(item);
218 
219                 mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
220                         PropertyValuesHolder.ofFloat(ALPHA, 1));
221             }
222 
223             mLastFocusedItem = item;
224         } else {
225             if (mLastFocusedItem == item) {
226                 mLastFocusedItem = null;
227                 endCurrentAnimation();
228                 mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
229                         PropertyValuesHolder.ofFloat(ALPHA, 0));
230                 mCurrentAnimation.addListener(new ViewSetListener(null, false));
231             }
232         }
233 
234         // invalidate once
235         invalidateDirty();
236 
237         mLastFocusedItem = hasFocus ? item : null;
238         if (mCurrentAnimation != null) {
239             mCurrentAnimation.addUpdateListener(this);
240             mCurrentAnimation.setDuration(ANIM_DURATION).start();
241         }
242     }
243 
endCurrentAnimation()244     protected void endCurrentAnimation() {
245         if (mCurrentAnimation != null) {
246             mCurrentAnimation.cancel();
247             mCurrentAnimation = null;
248         }
249     }
250 
setCurrentItem(T item)251     protected void setCurrentItem(T item) {
252         mCurrentItem = item;
253         // Set it to end value directly to skip the animation for outline
254         mShift = Flags.enableFocusOutline() ? 1 : 0;
255         mTargetItem = null;
256     }
257 
258     /**
259      * Gets the position of the item relative to {@link #mContainer}.
260      */
viewToRect(T item, Rect outRect)261     public abstract void viewToRect(T item, Rect outRect);
262 
263     private class ViewSetListener extends AnimatorListenerAdapter {
264         private final T mItemToSet;
265         private final boolean mCallOnCancel;
266         private boolean mCalled = false;
267 
ViewSetListener(T item, boolean callOnCancel)268         ViewSetListener(T item, boolean callOnCancel) {
269             mItemToSet = item;
270             mCallOnCancel = callOnCancel;
271         }
272 
273         @Override
onAnimationCancel(Animator animation)274         public void onAnimationCancel(Animator animation) {
275             if (!mCallOnCancel) {
276                 mCalled = true;
277             }
278         }
279 
280         @Override
onAnimationEnd(Animator animation)281         public void onAnimationEnd(Animator animation) {
282             if (!mCalled) {
283                 setCurrentItem(mItemToSet);
284                 mCalled = true;
285             }
286         }
287     }
288 }
289