• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 android.view;
18 
19 import static android.util.MathUtils.acos;
20 
21 import static java.lang.Math.sin;
22 
23 import android.content.res.Resources;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.graphics.RectF;
29 import android.os.SystemProperties;
30 import android.util.DisplayMetrics;
31 import android.view.flags.Flags;
32 
33 /**
34  * Helper class for drawing round scroll bars on round Wear devices.
35  *
36  * @hide
37  */
38 public class RoundScrollbarRenderer {
39     /** @hide */
40     public static final String BLUECHIP_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled";
41 
42     // The range of the scrollbar position represented as an angle in degrees.
43     private static final float SCROLLBAR_ANGLE_RANGE = 28.8f;
44     private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 0.7f * SCROLLBAR_ANGLE_RANGE;
45     private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 0.3f * SCROLLBAR_ANGLE_RANGE;
46     private static final float GAP_BETWEEN_TRACK_AND_THUMB_DP = 3f;
47     private static final float OUTER_PADDING_DP = 2f;
48     private static final int DEFAULT_THUMB_COLOR = 0xFFC6C6C7;
49     private static final int DEFAULT_TRACK_COLOR = 0xFF2F3131;
50 
51     // Rate at which the scrollbar will resize itself when the size of the view changes
52     private static final float RESIZING_RATE = 0.8f;
53     // Threshold at which the scrollbar will stop resizing smoothly and jump to the correct size
54     private static final int RESIZING_THRESHOLD_PX = 20;
55 
56     private final Paint mThumbPaint = new Paint();
57     private final Paint mTrackPaint = new Paint();
58     private final RectF mRect = new RectF();
59     private final View mParent;
60     private final float mInset;
61     private final float mGapBetweenThumbAndTrackPx;
62     private final boolean mUseRefactoredRoundScrollbar;
63 
64     private float mPreviousMaxScroll = 0;
65     private float mMaxScrollDiff = 0;
66     private float mPreviousCurrentScroll = 0;
67     private float mCurrentScrollDiff = 0;
68     private float mThumbStrokeWidthAsDegrees = 0;
69     private float mGapBetweenTrackAndThumbAsDegrees = 0;
70     private boolean mDrawToLeft;
71 
RoundScrollbarRenderer(View parent)72     public RoundScrollbarRenderer(View parent) {
73         // Paints for the round scrollbar.
74         // Set up the thumb paint
75         mThumbPaint.setAntiAlias(true);
76         mThumbPaint.setStrokeCap(Paint.Cap.ROUND);
77         mThumbPaint.setStyle(Paint.Style.STROKE);
78 
79         // Set up the track paint
80         mTrackPaint.setAntiAlias(true);
81         mTrackPaint.setStrokeCap(Paint.Cap.ROUND);
82         mTrackPaint.setStyle(Paint.Style.STROKE);
83 
84         mParent = parent;
85 
86         Resources resources = parent.getContext().getResources();
87         // Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same
88         // way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so
89         // that it doesn't get clipped.
90         int maskThickness =
91                 resources.getDimensionPixelSize(
92                         com.android.internal.R.dimen.circular_display_mask_thickness);
93 
94         float thumbWidth =
95                 resources.getDimensionPixelSize(com.android.internal.R.dimen.round_scrollbar_width);
96         mGapBetweenThumbAndTrackPx = dpToPx(GAP_BETWEEN_TRACK_AND_THUMB_DP);
97         mThumbPaint.setStrokeWidth(thumbWidth);
98         mTrackPaint.setStrokeWidth(thumbWidth);
99         mInset = thumbWidth / 2 + maskThickness;
100 
101         mUseRefactoredRoundScrollbar =
102                 Flags.useRefactoredRoundScrollbar()
103                         && SystemProperties.getBoolean(BLUECHIP_ENABLED_SYSPROP, false);
104     }
105 
computeScrollExtent(float scrollExtent, float maxScroll)106     private float computeScrollExtent(float scrollExtent, float maxScroll) {
107         if (scrollExtent <= 0) {
108             if (!mParent.canScrollVertically(1) && !mParent.canScrollVertically(-1)) {
109                 return -1f;
110             } else {
111                 return 0f;
112             }
113         } else if (maxScroll <= scrollExtent) {
114             return -1f;
115         }
116         return scrollExtent;
117     }
118 
resizeGradually(float maxScroll, float newScroll)119     private void resizeGradually(float maxScroll, float newScroll) {
120         // Make changes to the VerticalScrollRange happen gradually
121         if (Math.abs(maxScroll - mPreviousMaxScroll) > RESIZING_THRESHOLD_PX
122                 && mPreviousMaxScroll != 0) {
123             mMaxScrollDiff += maxScroll - mPreviousMaxScroll;
124             mCurrentScrollDiff += newScroll - mPreviousCurrentScroll;
125         }
126 
127         mPreviousMaxScroll = maxScroll;
128         mPreviousCurrentScroll = newScroll;
129 
130         if (Math.abs(mMaxScrollDiff) > RESIZING_THRESHOLD_PX
131                 || Math.abs(mCurrentScrollDiff) > RESIZING_THRESHOLD_PX) {
132             mMaxScrollDiff *= RESIZING_RATE;
133             mCurrentScrollDiff *= RESIZING_RATE;
134         } else {
135             mMaxScrollDiff = 0;
136             mCurrentScrollDiff = 0;
137         }
138     }
139 
drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft)140     public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) {
141         if (alpha == 0) {
142             return;
143         }
144         // Get information about the current scroll state of the parent view.
145         float maxScroll = mParent.computeVerticalScrollRange();
146         float scrollExtent = mParent.computeVerticalScrollExtent();
147         float newScroll = mParent.computeVerticalScrollOffset();
148 
149         scrollExtent = computeScrollExtent(scrollExtent, maxScroll);
150         if (scrollExtent < 0f) {
151             return;
152         }
153 
154         // Make changes to the VerticalScrollRange happen gradually
155         resizeGradually(maxScroll, newScroll);
156         maxScroll -= mMaxScrollDiff;
157         newScroll -= mCurrentScrollDiff;
158 
159         applyThumbColor(alpha);
160 
161         float sweepAngle = computeSweepAngle(scrollExtent, maxScroll);
162         float startAngle =
163                 computeStartAngle(Math.max(0, newScroll), sweepAngle, maxScroll, scrollExtent);
164 
165         updateBounds(bounds);
166 
167         mDrawToLeft = drawToLeft;
168         drawRoundScrollbars(canvas, startAngle, sweepAngle, alpha);
169     }
170 
drawRoundScrollbars( Canvas canvas, float startAngle, float sweepAngle, float alpha)171     private void drawRoundScrollbars(
172             Canvas canvas, float startAngle, float sweepAngle, float alpha) {
173         if (mUseRefactoredRoundScrollbar) {
174             draw(canvas, startAngle, sweepAngle, alpha);
175         } else {
176             applyTrackColor(alpha);
177             drawArc(canvas, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, mTrackPaint);
178             drawArc(canvas, startAngle, sweepAngle, mThumbPaint);
179         }
180     }
181 
updateBounds(Rect bounds)182     private void updateBounds(Rect bounds) {
183         mRect.set(
184                 bounds.left + mInset,
185                 bounds.top + mInset,
186                 bounds.right - mInset,
187                 bounds.bottom - mInset);
188         mThumbStrokeWidthAsDegrees =
189                 getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f);
190         mGapBetweenTrackAndThumbAsDegrees =
191                 getVertexAngle((mRect.right - mRect.left) / 2f, mGapBetweenThumbAndTrackPx);
192     }
193 
computeSweepAngle(float scrollExtent, float maxScroll)194     private float computeSweepAngle(float scrollExtent, float maxScroll) {
195         // Normalize the sweep angle for the scroll bar.
196         float sweepAngle = (scrollExtent / maxScroll) * SCROLLBAR_ANGLE_RANGE;
197         return clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE);
198     }
199 
computeStartAngle( float currentScroll, float sweepAngle, float maxScroll, float scrollExtent)200     private float computeStartAngle(
201             float currentScroll, float sweepAngle, float maxScroll, float scrollExtent) {
202         // Normalize the start angle so that it falls on the track.
203         float startAngle =
204                 (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle)) / (maxScroll - scrollExtent)
205                         - SCROLLBAR_ANGLE_RANGE / 2f;
206         return clamp(
207                 startAngle, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle);
208     }
209 
getRoundVerticalScrollBarBounds(Rect bounds)210     void getRoundVerticalScrollBarBounds(Rect bounds) {
211         float padding = dpToPx(OUTER_PADDING_DP);
212         final int width = mParent.mRight - mParent.mLeft;
213         final int height = mParent.mBottom - mParent.mTop;
214         bounds.left = mParent.mScrollX + (int) padding;
215         bounds.top = mParent.mScrollY + (int) padding;
216         bounds.right = mParent.mScrollX + width - (int) padding;
217         bounds.bottom = mParent.mScrollY + height - (int) padding;
218     }
219 
clamp(float val, float min, float max)220     private static float clamp(float val, float min, float max) {
221         if (val < min) {
222             return min;
223         } else {
224             return Math.min(val, max);
225         }
226     }
227 
applyAlpha(int color, float alpha)228     private static int applyAlpha(int color, float alpha) {
229         int alphaByte = (int) (Color.alpha(color) * alpha);
230         return Color.argb(alphaByte, Color.red(color), Color.green(color), Color.blue(color));
231     }
232 
applyThumbColor(float alpha)233     private void applyThumbColor(float alpha) {
234         int color = applyAlpha(DEFAULT_THUMB_COLOR, alpha);
235         if (mThumbPaint.getColor() != color) {
236             mThumbPaint.setColor(color);
237         }
238     }
239 
applyTrackColor(float alpha)240     private void applyTrackColor(float alpha) {
241         int color = applyAlpha(DEFAULT_TRACK_COLOR, alpha);
242         if (mTrackPaint.getColor() != color) {
243             mTrackPaint.setColor(color);
244         }
245     }
246 
dpToPx(float dp)247     private float dpToPx(float dp) {
248         return dp * ((float) mParent.getContext().getResources().getDisplayMetrics().densityDpi)
249                 / DisplayMetrics.DENSITY_DEFAULT;
250     }
251 
getVertexAngle(float edge, float base)252     private static float getVertexAngle(float edge, float base) {
253         float edgeSquare = edge * edge * 2;
254         float baseSquare = base * base;
255         float gapInRadians = acos(((edgeSquare - baseSquare) / edgeSquare));
256         return (float) Math.toDegrees(gapInRadians);
257     }
258 
getKiteEdge(float knownEdge, float angleBetweenKnownEdgesInDegrees)259     private static float getKiteEdge(float knownEdge, float angleBetweenKnownEdgesInDegrees) {
260         return (float) (2 * knownEdge * sin(Math.toRadians(angleBetweenKnownEdgesInDegrees / 2)));
261     }
262 
draw(Canvas canvas, float thumbStartAngle, float thumbSweepAngle, float alpha)263     private void draw(Canvas canvas, float thumbStartAngle, float thumbSweepAngle, float alpha) {
264         // Draws the top arc
265         drawTrack(
266                 canvas,
267                 // The highest point of the top track on a vertical scale. Here the thumb width is
268                 // reduced to account for the arc formed by ROUND stroke style
269                 -SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees,
270                 // The lowest point of the top track on a vertical scale. It's reduced by
271                 // (a) angular distance for the arc formed by ROUND stroke style
272                 // (b) gap between thumb and top track
273                 thumbStartAngle - mThumbStrokeWidthAsDegrees - mGapBetweenTrackAndThumbAsDegrees,
274                 alpha);
275         // Draws the thumb
276         drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint);
277         // Draws the bottom arc
278         drawTrack(
279                 canvas,
280                 // The highest point of the bottom track on a vertical scale. Following added to it
281                 // (a) angular distance for the arc formed by ROUND stroke style
282                 // (b) gap between thumb and top track
283                 (thumbStartAngle + thumbSweepAngle)
284                         + mThumbStrokeWidthAsDegrees
285                         + mGapBetweenTrackAndThumbAsDegrees,
286                 // The lowest point of the top track on a vertical scale. Here the thumb width is
287                 // added to account for the arc formed by ROUND stroke style
288                 SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees,
289                 alpha);
290     }
291 
drawTrack(Canvas canvas, float beginAngle, float endAngle, float alpha)292     private void drawTrack(Canvas canvas, float beginAngle, float endAngle, float alpha) {
293         // Angular distance between end and begin
294         float angleBetweenEndAndBegin = endAngle - beginAngle;
295         // The sweep angle for the track is the angular distance between end and begin less the
296         // thumb width twice to account for top and bottom arc formed by the ROUND stroke style
297         float sweepAngle = angleBetweenEndAndBegin - 2 * mThumbStrokeWidthAsDegrees;
298 
299         float startAngle = -1f;
300         float strokeWidth = -1f;
301         if (sweepAngle > 0f) {
302             // The angle is greater than 0 which means a normal arc should be drawn with stroke
303             // width same as the thumb. The ROUND stroke style will cover the top/bottom arc of the
304             // track
305             startAngle = beginAngle + mThumbStrokeWidthAsDegrees;
306             strokeWidth = mThumbPaint.getStrokeWidth();
307         } else if (Math.abs(sweepAngle) < 2 * mThumbStrokeWidthAsDegrees) {
308             // The sweep angle is less than 0 but is still relevant in creating a circle for the
309             // top/bottom track. The start angle is adjusted to account for being the mid point of
310             // begin / end angle.
311             startAngle = beginAngle + angleBetweenEndAndBegin / 2;
312             // The radius of this circle forms a kite with the radius of the arc drawn for the rect
313             // with the given angular difference between the arc radius which is used to compute the
314             // new stroke width.
315             strokeWidth = getKiteEdge(((mRect.right - mRect.left) / 2), angleBetweenEndAndBegin);
316             // The opacity is decreased proportionally, if the stroke width of the track is 50% or
317             // less that that of the thumb
318             alpha = alpha * Math.min(1f, 2 * strokeWidth / mThumbPaint.getStrokeWidth());
319             // As we desire a circle to be drawn, the sweep angle is set to a minimal value
320             sweepAngle = Float.MIN_NORMAL;
321         } else {
322             return;
323         }
324 
325         applyTrackColor(alpha);
326         mTrackPaint.setStrokeWidth(strokeWidth);
327         drawArc(canvas, startAngle, sweepAngle, mTrackPaint);
328     }
329 
drawArc(Canvas canvas, float startAngle, float sweepAngle, Paint paint)330     private void drawArc(Canvas canvas, float startAngle, float sweepAngle, Paint paint) {
331         if (mDrawToLeft) {
332             canvas.drawArc(mRect, /* startAngle= */ 180 - startAngle, -sweepAngle, false, paint);
333         } else {
334             canvas.drawArc(mRect, startAngle, sweepAngle, /* useCenter= */ false, paint);
335         }
336     }
337 }
338