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.config.FeatureFlags; 33 import com.android.launcher3.util.Themes; 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; 78 private final int mMaxAlpha; 79 80 private final Rect mDirtyRect = new Rect(); 81 private boolean mIsDirty = false; 82 83 private T mLastFocusedItem; 84 85 private T mCurrentItem; 86 private T mTargetItem; 87 /** 88 * The fraction indicating the position of the focusRect between {@link #mCurrentItem} 89 * & {@link #mTargetItem} 90 */ 91 private float mShift; 92 93 private ObjectAnimator mCurrentAnimation; 94 private float mAlpha; 95 private float mRadius; 96 ItemFocusIndicatorHelper(View container, int color)97 public ItemFocusIndicatorHelper(View container, int color) { 98 mContainer = container; 99 100 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 101 mMaxAlpha = Color.alpha(color); 102 mPaint.setColor(0xFF000000 | color); 103 104 setAlpha(0); 105 mShift = 0; 106 if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { 107 mRadius = Themes.getDialogCornerRadius(container.getContext()); 108 } 109 } 110 setAlpha(float alpha)111 protected void setAlpha(float alpha) { 112 mAlpha = alpha; 113 mPaint.setAlpha((int) (mAlpha * mMaxAlpha)); 114 } 115 116 @Override onAnimationUpdate(ValueAnimator animation)117 public void onAnimationUpdate(ValueAnimator animation) { 118 invalidateDirty(); 119 } 120 invalidateDirty()121 protected void invalidateDirty() { 122 if (mIsDirty) { 123 mContainer.invalidate(mDirtyRect); 124 mIsDirty = false; 125 } 126 127 Rect newRect = getDrawRect(); 128 if (newRect != null) { 129 mContainer.invalidate(newRect); 130 } 131 } 132 133 /** 134 * Draws the indicator on the canvas 135 */ draw(Canvas c)136 public void draw(Canvas c) { 137 if (mAlpha <= 0) return; 138 139 Rect newRect = getDrawRect(); 140 if (newRect != null) { 141 mDirtyRect.set(newRect); 142 c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top, 143 (float) mDirtyRect.right, (float) mDirtyRect.bottom, 144 mRadius, mRadius, mPaint); 145 mIsDirty = true; 146 } 147 } 148 getDrawRect()149 private Rect getDrawRect() { 150 if (mCurrentItem != null && shouldDraw(mCurrentItem)) { 151 viewToRect(mCurrentItem, sTempRect1); 152 153 if (mShift > 0 && mTargetItem != null) { 154 viewToRect(mTargetItem, sTempRect2); 155 return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2); 156 } else { 157 return sTempRect1; 158 } 159 } 160 return null; 161 } 162 163 /** 164 * Returns true if the provided item is valid 165 */ shouldDraw(T item)166 protected boolean shouldDraw(T item) { 167 return true; 168 } 169 changeFocus(T item, boolean hasFocus)170 protected void changeFocus(T item, boolean hasFocus) { 171 if (hasFocus) { 172 endCurrentAnimation(); 173 174 if (mAlpha > MIN_VISIBLE_ALPHA) { 175 mTargetItem = item; 176 177 mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, 178 PropertyValuesHolder.ofFloat(ALPHA, 1), 179 PropertyValuesHolder.ofFloat(SHIFT, 1)); 180 mCurrentAnimation.addListener(new ViewSetListener(item, true)); 181 } else { 182 setCurrentItem(item); 183 184 mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, 185 PropertyValuesHolder.ofFloat(ALPHA, 1)); 186 } 187 188 mLastFocusedItem = item; 189 } else { 190 if (mLastFocusedItem == item) { 191 mLastFocusedItem = null; 192 endCurrentAnimation(); 193 mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, 194 PropertyValuesHolder.ofFloat(ALPHA, 0)); 195 mCurrentAnimation.addListener(new ViewSetListener(null, false)); 196 } 197 } 198 199 // invalidate once 200 invalidateDirty(); 201 202 mLastFocusedItem = hasFocus ? item : null; 203 if (mCurrentAnimation != null) { 204 mCurrentAnimation.addUpdateListener(this); 205 mCurrentAnimation.setDuration(ANIM_DURATION).start(); 206 } 207 } 208 endCurrentAnimation()209 protected void endCurrentAnimation() { 210 if (mCurrentAnimation != null) { 211 mCurrentAnimation.cancel(); 212 mCurrentAnimation = null; 213 } 214 } 215 setCurrentItem(T item)216 protected void setCurrentItem(T item) { 217 mCurrentItem = item; 218 mShift = 0; 219 mTargetItem = null; 220 } 221 222 /** 223 * Gets the position of the item relative to {@link #mContainer}. 224 */ viewToRect(T item, Rect outRect)225 public abstract void viewToRect(T item, Rect outRect); 226 227 private class ViewSetListener extends AnimatorListenerAdapter { 228 private final T mItemToSet; 229 private final boolean mCallOnCancel; 230 private boolean mCalled = false; 231 ViewSetListener(T item, boolean callOnCancel)232 ViewSetListener(T item, boolean callOnCancel) { 233 mItemToSet = item; 234 mCallOnCancel = callOnCancel; 235 } 236 237 @Override onAnimationCancel(Animator animation)238 public void onAnimationCancel(Animator animation) { 239 if (!mCallOnCancel) { 240 mCalled = true; 241 } 242 } 243 244 @Override onAnimationEnd(Animator animation)245 public void onAnimationEnd(Animator animation) { 246 if (!mCalled) { 247 setCurrentItem(mItemToSet); 248 mCalled = true; 249 } 250 } 251 } 252 } 253