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