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 android.graphics.Canvas;
20 import android.graphics.Color;
21 import android.graphics.Paint;
22 import android.graphics.Rect;
23 import android.graphics.RectF;
24 import android.util.DisplayMetrics;
25 
26 /**
27  * Helper class for drawing round scroll bars on round Wear devices.
28  */
29 class RoundScrollbarRenderer {
30     // The range of the scrollbar position represented as an angle in degrees.
31     private static final float SCROLLBAR_ANGLE_RANGE = 28.8f;
32     private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90%
33     private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 3.1f; // 10%
34     private static final float THUMB_WIDTH_DP = 4f;
35     private static final float OUTER_PADDING_DP = 2f;
36     private static final int DEFAULT_THUMB_COLOR = 0xFFFFFFFF;
37     private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF;
38 
39     // Rate at which the scrollbar will resize itself when the size of the view changes
40     private static final float RESIZING_RATE = 0.8f;
41     // Threshold at which the scrollbar will stop resizing smoothly and jump to the correct size
42     private static final int RESIZING_THRESHOLD_PX = 20;
43 
44     private final Paint mThumbPaint = new Paint();
45     private final Paint mTrackPaint = new Paint();
46     private final RectF mRect = new RectF();
47     private final View mParent;
48     private final int mMaskThickness;
49 
50     private float mPreviousMaxScroll = 0;
51     private float mMaxScrollDiff = 0;
52     private float mPreviousCurrentScroll = 0;
53     private float mCurrentScrollDiff = 0;
54 
RoundScrollbarRenderer(View parent)55     public RoundScrollbarRenderer(View parent) {
56         // Paints for the round scrollbar.
57         // Set up the thumb paint
58         mThumbPaint.setAntiAlias(true);
59         mThumbPaint.setStrokeCap(Paint.Cap.ROUND);
60         mThumbPaint.setStyle(Paint.Style.STROKE);
61 
62         // Set up the track paint
63         mTrackPaint.setAntiAlias(true);
64         mTrackPaint.setStrokeCap(Paint.Cap.ROUND);
65         mTrackPaint.setStyle(Paint.Style.STROKE);
66 
67         mParent = parent;
68 
69         // Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same
70         // way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so
71         // that it doesn't get clipped.
72         mMaskThickness = parent.getContext().getResources().getDimensionPixelSize(
73                 com.android.internal.R.dimen.circular_display_mask_thickness);
74     }
75 
drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft)76     public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) {
77         if (alpha == 0) {
78             return;
79         }
80         // Get information about the current scroll state of the parent view.
81         float maxScroll = mParent.computeVerticalScrollRange();
82         float scrollExtent = mParent.computeVerticalScrollExtent();
83         float newScroll = mParent.computeVerticalScrollOffset();
84 
85         if (scrollExtent <= 0) {
86             if (!mParent.canScrollVertically(1) && !mParent.canScrollVertically(-1)) {
87                 return;
88             } else {
89                 scrollExtent = 0;
90             }
91         } else if (maxScroll <= scrollExtent) {
92             return;
93         }
94 
95         // Make changes to the VerticalScrollRange happen gradually
96         if (Math.abs(maxScroll - mPreviousMaxScroll) > RESIZING_THRESHOLD_PX
97                 && mPreviousMaxScroll != 0) {
98             mMaxScrollDiff += maxScroll - mPreviousMaxScroll;
99             mCurrentScrollDiff += newScroll - mPreviousCurrentScroll;
100         }
101 
102         mPreviousMaxScroll = maxScroll;
103         mPreviousCurrentScroll = newScroll;
104 
105         if (Math.abs(mMaxScrollDiff) > RESIZING_THRESHOLD_PX
106                 || Math.abs(mCurrentScrollDiff) > RESIZING_THRESHOLD_PX) {
107             mMaxScrollDiff *= RESIZING_RATE;
108             mCurrentScrollDiff *= RESIZING_RATE;
109 
110             maxScroll -= mMaxScrollDiff;
111             newScroll -= mCurrentScrollDiff;
112         } else {
113             mMaxScrollDiff = 0;
114             mCurrentScrollDiff = 0;
115         }
116 
117         float currentScroll = Math.max(0, newScroll);
118         float linearThumbLength = scrollExtent;
119         float thumbWidth = dpToPx(THUMB_WIDTH_DP);
120         mThumbPaint.setStrokeWidth(thumbWidth);
121         mTrackPaint.setStrokeWidth(thumbWidth);
122 
123         setThumbColor(applyAlpha(DEFAULT_THUMB_COLOR, alpha));
124         setTrackColor(applyAlpha(DEFAULT_TRACK_COLOR, alpha));
125 
126         // Normalize the sweep angle for the scroll bar.
127         float sweepAngle = (linearThumbLength / maxScroll) * SCROLLBAR_ANGLE_RANGE;
128         sweepAngle = clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE);
129         // Normalize the start angle so that it falls on the track.
130         float startAngle = (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle))
131                 / (maxScroll - linearThumbLength) - SCROLLBAR_ANGLE_RANGE / 2f;
132         startAngle = clamp(startAngle, -SCROLLBAR_ANGLE_RANGE / 2f,
133                 SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle);
134 
135         // Draw the track and the thumb.
136         float inset = thumbWidth / 2 + mMaskThickness;
137         mRect.set(
138                 bounds.left + inset,
139                 bounds.top + inset,
140                 bounds.right - inset,
141                 bounds.bottom - inset);
142 
143         if (drawToLeft) {
144             canvas.drawArc(mRect, 180 + SCROLLBAR_ANGLE_RANGE / 2f, -SCROLLBAR_ANGLE_RANGE, false,
145                     mTrackPaint);
146             canvas.drawArc(mRect, 180 - startAngle, -sweepAngle, false, mThumbPaint);
147         } else {
148             canvas.drawArc(mRect, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, false,
149                     mTrackPaint);
150             canvas.drawArc(mRect, startAngle, sweepAngle, false, mThumbPaint);
151         }
152     }
153 
getRoundVerticalScrollBarBounds(Rect bounds)154     void getRoundVerticalScrollBarBounds(Rect bounds) {
155         float padding = dpToPx(OUTER_PADDING_DP);
156         final int width = mParent.mRight - mParent.mLeft;
157         final int height = mParent.mBottom - mParent.mTop;
158         bounds.left = mParent.mScrollX + (int) padding;
159         bounds.top = mParent.mScrollY + (int) padding;
160         bounds.right = mParent.mScrollX + width - (int) padding;
161         bounds.bottom = mParent.mScrollY + height - (int) padding;
162     }
163 
clamp(float val, float min, float max)164     private static float clamp(float val, float min, float max) {
165         if (val < min) {
166             return min;
167         } else if (val > max) {
168             return max;
169         } else {
170             return val;
171         }
172     }
173 
applyAlpha(int color, float alpha)174     private static int applyAlpha(int color, float alpha) {
175         int alphaByte = (int) (Color.alpha(color) * alpha);
176         return Color.argb(alphaByte, Color.red(color), Color.green(color), Color.blue(color));
177     }
178 
setThumbColor(int thumbColor)179     private void setThumbColor(int thumbColor) {
180         if (mThumbPaint.getColor() != thumbColor) {
181             mThumbPaint.setColor(thumbColor);
182         }
183     }
184 
setTrackColor(int trackColor)185     private void setTrackColor(int trackColor) {
186         if (mTrackPaint.getColor() != trackColor) {
187             mTrackPaint.setColor(trackColor);
188         }
189     }
190 
dpToPx(float dp)191     private float dpToPx(float dp) {
192         return dp * ((float) mParent.getContext().getResources().getDisplayMetrics().densityDpi)
193                 / DisplayMetrics.DENSITY_DEFAULT;
194     }
195 }
196