1 /*
2  * Copyright (C) 2023 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 
17 package com.android.systemui.tv.privacy;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorInflater;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Canvas;
24 import android.graphics.ColorFilter;
25 import android.graphics.Paint;
26 import android.graphics.Path;
27 import android.graphics.PixelFormat;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.graphics.drawable.Drawable;
31 import android.util.Log;
32 import android.util.MathUtils;
33 import android.view.Gravity;
34 
35 import androidx.annotation.Keep;
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import com.android.systemui.tv.res.R;
40 
41 /**
42  * Drawable that can go from being the background of the privacy icons to a small dot.
43  * The icons are not included.
44  */
45 public class PrivacyChipDrawable extends Drawable {
46     private static final String TAG = PrivacyChipDrawable.class.getSimpleName();
47     private static final boolean DEBUG = false;
48 
49     private final Paint mChipPaint;
50     private final Paint mBgPaint;
51     private final Rect mTmpRect = new Rect();
52     private final Rect mBgRect = new Rect();
53     private final RectF mTmpRectF = new RectF();
54     private final Path mPath = new Path();
55     private final Animator mCollapse;
56     private final Animator mExpand;
57     private final int mLayoutDirection;
58     private final int mBgWidth;
59     private final int mBgHeight;
60     private final int mBgRadius;
61     private final int mDotSize;
62     private final float mExpandedChipRadius;
63     private final float mCollapsedChipRadius;
64 
65     private final boolean mCollapseToDot;
66 
67     private boolean mIsExpanded = true;
68     private float mCollapseProgress = 0f;
69 
PrivacyChipDrawable(Context context, int chipColorRes, boolean collapseToDot)70     public PrivacyChipDrawable(Context context, int chipColorRes, boolean collapseToDot) {
71         mCollapseToDot = collapseToDot;
72 
73         mChipPaint = new Paint();
74         mChipPaint.setStyle(Paint.Style.FILL);
75         mChipPaint.setColor(context.getColor(chipColorRes));
76         mChipPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
77 
78         mBgPaint = new Paint();
79         mBgPaint.setStyle(Paint.Style.FILL);
80         mBgPaint.setColor(context.getColor(R.color.privacy_chip_dot_bg_tint));
81         mBgPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
82 
83         Resources res = context.getResources();
84         mLayoutDirection = res.getConfiguration().getLayoutDirection();
85         mBgWidth = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_width);
86         mBgHeight = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_height);
87         mBgRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_radius);
88         mDotSize = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_size);
89 
90         mExpandedChipRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_radius);
91         mCollapsedChipRadius = res.getDimensionPixelSize(R.dimen.privacy_chip_dot_radius);
92 
93         mExpand = AnimatorInflater.loadAnimator(context, R.anim.privacy_chip_expand);
94         mExpand.setTarget(this);
95         mCollapse = AnimatorInflater.loadAnimator(context, R.anim.privacy_chip_collapse);
96         mCollapse.setTarget(this);
97     }
98 
99     /**
100      * @return how far the chip is currently collapsed.
101      * @see #setCollapseProgress(float)
102      */
103     @Keep
getCollapseProgress()104     public float getCollapseProgress() {
105         return mCollapseProgress;
106     }
107 
108     /**
109      * Sets the collapsing progress of the chip to its collapsed state.
110      * @param pct How far the chip is collapsed, in the range 0-1.
111      *            0=fully expanded, 1=fully collapsed.
112      */
113     @Keep
setCollapseProgress(float pct)114     public void setCollapseProgress(float pct) {
115         mCollapseProgress = pct;
116         invalidateSelf();
117     }
118 
119     @Override
draw(@onNull Canvas canvas)120     public void draw(@NonNull Canvas canvas) {
121         if (mCollapseProgress > 0f) {
122             // draw background
123             getBackgroundBounds(mBgRect);
124             mTmpRectF.set(mBgRect);
125             canvas.drawRoundRect(mTmpRectF, mBgRadius, mBgRadius, mBgPaint);
126         }
127 
128         getForegroundBounds(mTmpRectF);
129         float radius = MathUtils.lerp(
130                 mExpandedChipRadius,
131                 mCollapseToDot ? mCollapsedChipRadius : mBgRadius,
132                 mCollapseProgress);
133 
134         canvas.drawRoundRect(mTmpRectF, radius, radius, mChipPaint);
135     }
136 
getBackgroundBounds(Rect out)137     private void getBackgroundBounds(Rect out) {
138         Rect bounds = getBounds();
139         Gravity.apply(Gravity.END, mBgWidth, mBgHeight, bounds, out, mLayoutDirection);
140     }
141 
getCollapsedForegroundBounds(Rect out)142     private void getCollapsedForegroundBounds(Rect out) {
143         Rect bounds = getBounds();
144         getBackgroundBounds(mBgRect);
145         if (mCollapseToDot) {
146             Gravity.apply(Gravity.CENTER, mDotSize, mDotSize, mBgRect, out);
147         } else {
148             out.set(bounds.left, mBgRect.top, bounds.right, mBgRect.bottom);
149         }
150     }
151 
getForegroundBounds(RectF out)152     private void getForegroundBounds(RectF out) {
153         Rect bounds = getBounds();
154         getCollapsedForegroundBounds(mTmpRect);
155         lerpRect(bounds, mTmpRect, mCollapseProgress, out);
156     }
157 
lerpRect(Rect start, Rect stop, float amount, RectF out)158     private void lerpRect(Rect start, Rect stop, float amount, RectF out) {
159         float left = MathUtils.lerp(start.left, stop.left, amount);
160         float top = MathUtils.lerp(start.top, stop.top, amount);
161         float right = MathUtils.lerp(start.right, stop.right, amount);
162         float bottom = MathUtils.lerp(start.bottom, stop.bottom, amount);
163         out.set(left, top, right, bottom);
164     }
165 
166     /**
167      * Clips the given canvas to this chip's foreground shape.
168      * @param canvas Canvas to clip.
169      */
clipToForeground(Canvas canvas)170     public void clipToForeground(Canvas canvas) {
171         getForegroundBounds(mTmpRectF);
172         float radius = MathUtils.lerp(
173                 mExpandedChipRadius,
174                 mCollapseToDot ? mCollapsedChipRadius : mBgRadius,
175                 mCollapseProgress);
176 
177         mPath.reset();
178         mPath.addRoundRect(mTmpRectF, radius, radius, Path.Direction.CW);
179         canvas.clipPath(mPath);
180     }
181 
182     @Override
onBoundsChange(@onNull Rect bounds)183     protected void onBoundsChange(@NonNull Rect bounds) {
184         super.onBoundsChange(bounds);
185         invalidateSelf();
186     }
187 
188     @Override
setAlpha(int alpha)189     public void setAlpha(int alpha) {
190         mChipPaint.setAlpha(alpha);
191         mBgPaint.setAlpha(alpha);
192     }
193 
194     /**
195      * Transitions to a full chip.
196      *
197      * @param animate Whether to animate the change to a full chip, or expand instantly.
198      */
expand(boolean animate)199     public void expand(boolean animate) {
200         if (DEBUG) Log.d(TAG, "expanding");
201         if (mIsExpanded) {
202             return;
203         }
204         mIsExpanded = true;
205         if (animate) {
206             mCollapse.cancel();
207             mExpand.start();
208         } else {
209             mCollapseProgress = 0f;
210             invalidateSelf();
211         }
212     }
213 
214     /**
215      * Starts the animation to a dot.
216      */
collapse()217     public void collapse() {
218         if (DEBUG) Log.d(TAG, "collapsing");
219         if (!mIsExpanded) {
220             return;
221         }
222         mIsExpanded = false;
223         mExpand.cancel();
224         mCollapse.start();
225     }
226 
227     @Override
setColorFilter(@ullable ColorFilter colorFilter)228     public void setColorFilter(@Nullable ColorFilter colorFilter) {
229         // no-op
230     }
231 
232     @Override
getOpacity()233     public int getOpacity() {
234         return PixelFormat.TRANSLUCENT;
235     }
236 }
237