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