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