1 /*
2  * Copyright (C) 2017 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 androidx.leanback.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 
21 import android.content.Context;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Paint;
25 import android.graphics.Rect;
26 import android.graphics.RectF;
27 import android.os.Bundle;
28 import android.util.AttributeSet;
29 import android.view.View;
30 
31 import androidx.annotation.RestrictTo;
32 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
33 import androidx.leanback.R;
34 
35 import org.jspecify.annotations.NonNull;
36 
37 /**
38  * Replacement of SeekBar, has two bar heights and two thumb size when focused/not_focused.
39  * The widget does not deal with KeyEvent, it's client's responsibility to set a key listener.
40  */
41 @RestrictTo(LIBRARY_GROUP_PREFIX)
42 public final class SeekBar extends View {
43 
44     /**
45      */
46     @RestrictTo(LIBRARY_GROUP_PREFIX)
47     public abstract static class AccessibilitySeekListener {
48         /**
49          * Called to perform AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
50          */
onAccessibilitySeekForward()51         public abstract boolean onAccessibilitySeekForward();
52         /**
53          * Called to perform AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
54          */
onAccessibilitySeekBackward()55         public abstract boolean onAccessibilitySeekBackward();
56     }
57 
58     private final RectF mProgressRect = new RectF();
59     private final RectF mSecondProgressRect = new RectF();
60     private final RectF mBackgroundRect = new RectF();
61     private final Paint mSecondProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
62     private final Paint mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
63     private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
64     private final Paint mKnobPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
65 
66     private int mProgress;
67     private int mSecondProgress;
68     private int mMax;
69     private int mKnobx;
70 
71     private int mActiveRadius;
72     private int mBarHeight;
73     private int mActiveBarHeight;
74 
75     private AccessibilitySeekListener mAccessibilitySeekListener;
76 
SeekBar(Context context, AttributeSet attrs)77     public SeekBar(Context context, AttributeSet attrs) {
78         super(context, attrs);
79         setWillNotDraw(false);
80         mBackgroundPaint.setColor(Color.GRAY);
81         mSecondProgressPaint.setColor(Color.LTGRAY);
82         mProgressPaint.setColor(Color.RED);
83         mKnobPaint.setColor(Color.WHITE);
84         mBarHeight = context.getResources().getDimensionPixelSize(
85                 R.dimen.lb_playback_transport_progressbar_bar_height);
86         mActiveBarHeight = context.getResources().getDimensionPixelSize(
87                 R.dimen.lb_playback_transport_progressbar_active_bar_height);
88         mActiveRadius = context.getResources().getDimensionPixelSize(
89                 R.dimen.lb_playback_transport_progressbar_active_radius);
90     }
91 
92     /**
93      * Set radius in pixels for thumb when SeekBar is focused.
94      */
setActiveRadius(int radius)95     public void setActiveRadius(int radius) {
96         mActiveRadius = radius;
97         calculate();
98     }
99 
100     /**
101      * Set horizontal bar height in pixels when SeekBar is not focused.
102      */
setBarHeight(int barHeight)103     public void setBarHeight(int barHeight) {
104         mBarHeight = barHeight;
105         calculate();
106     }
107 
108     /**
109      * Set horizontal bar height in pixels when SeekBar is focused.
110      */
setActiveBarHeight(int activeBarHeight)111     public void setActiveBarHeight(int activeBarHeight) {
112         mActiveBarHeight = activeBarHeight;
113         calculate();
114     }
115 
116     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)117     protected void onFocusChanged(boolean gainFocus,
118             int direction, Rect previouslyFocusedRect) {
119         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
120         calculate();
121     }
122 
123     @Override
onSizeChanged(final int w, final int h, final int oldw, final int oldh)124     protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
125         super.onSizeChanged(w, h, oldw, oldh);
126         calculate();
127     }
128 
129     @Override
onDraw(@onNull Canvas canvas)130     protected void onDraw(@NonNull Canvas canvas) {
131         super.onDraw(canvas);
132         final int radius = isFocused() ? mActiveRadius : mBarHeight / 2;
133         canvas.drawRoundRect(mBackgroundRect, radius, radius, mBackgroundPaint);
134         if (mSecondProgressRect.right > mSecondProgressRect.left) {
135             canvas.drawRoundRect(mSecondProgressRect, radius, radius, mSecondProgressPaint);
136         }
137         canvas.drawRoundRect(mProgressRect, radius, radius, mProgressPaint);
138         canvas.drawCircle(mKnobx, getHeight() / 2, radius, mKnobPaint);
139     }
140 
141     /**
142      * Set progress within 0 and {@link #getMax()}
143      */
setProgress(int progress)144     public void setProgress(int progress) {
145         if (progress > mMax) {
146             progress = mMax;
147         } else if (progress < 0) {
148             progress = 0;
149         }
150         mProgress = progress;
151         calculate();
152     }
153 
154     /**
155      * Set secondary progress within 0 and {@link #getMax()}
156      */
setSecondaryProgress(int progress)157     public void setSecondaryProgress(int progress) {
158         if (progress > mMax) {
159             progress = mMax;
160         } else if (progress < 0) {
161             progress = 0;
162         }
163         mSecondProgress = progress;
164         calculate();
165     }
166 
167     /**
168      * Get progress within 0 and {@link #getMax()}
169      */
getProgress()170     public int getProgress() {
171         return mProgress;
172     }
173 
174     /**
175      * Get secondary progress within 0 and {@link #getMax()}
176      */
getSecondProgress()177     public int getSecondProgress() {
178         return mSecondProgress;
179     }
180 
181     /**
182      * Get max value.
183      */
getMax()184     public int getMax() {
185         return mMax;
186     }
187 
188     /**
189      * Set max value.
190      */
setMax(int max)191     public void setMax(int max) {
192         this.mMax = max;
193         calculate();
194     }
195 
196     /**
197      * Set color for progress.
198      */
setProgressColor(int color)199     public void setProgressColor(int color) {
200         mProgressPaint.setColor(color);
201     }
202 
203     /**
204      * Set color for second progress which is usually for buffering indication.
205      */
setSecondaryProgressColor(int color)206     public void setSecondaryProgressColor(int color) {
207         mSecondProgressPaint.setColor(color);
208     }
209 
210     /**
211      * Set color for second progress which is usually for buffering indication.
212      */
getSecondaryProgressColor()213     public int getSecondaryProgressColor() {
214         return mSecondProgressPaint.getColor();
215     }
216 
calculate()217     private void calculate() {
218         final int barHeight = isFocused() ? mActiveBarHeight : mBarHeight;
219 
220         final int width = getWidth();
221         final int height = getHeight();
222         final int verticalPadding = (height - barHeight) / 2;
223 
224         mBackgroundRect.set(mBarHeight / 2, verticalPadding,
225                 width - mBarHeight / 2, height - verticalPadding);
226 
227         final int radius = isFocused() ? mActiveRadius : mBarHeight / 2;
228         final int progressWidth = width - radius * 2;
229         final float progressPixels = mProgress / (float) mMax * progressWidth;
230         mProgressRect.set(mBarHeight / 2, verticalPadding, mBarHeight / 2 + progressPixels,
231                 height - verticalPadding);
232 
233         final float secondProgressPixels = mSecondProgress / (float) mMax * progressWidth;
234         mSecondProgressRect.set(mProgressRect.right, verticalPadding,
235                 mBarHeight / 2 + secondProgressPixels, height - verticalPadding);
236 
237         mKnobx = radius + (int) progressPixels;
238         invalidate();
239     }
240 
241     @Override
getAccessibilityClassName()242     public CharSequence getAccessibilityClassName() {
243         return android.widget.SeekBar.class.getName();
244     }
245 
setAccessibilitySeekListener(AccessibilitySeekListener listener)246     public void setAccessibilitySeekListener(AccessibilitySeekListener listener) {
247         mAccessibilitySeekListener = listener;
248     }
249 
250     @Override
performAccessibilityAction(int action, Bundle arguments)251     public boolean performAccessibilityAction(int action, Bundle arguments) {
252         if (mAccessibilitySeekListener != null) {
253             switch (action) {
254                 case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
255                     return mAccessibilitySeekListener.onAccessibilitySeekForward();
256                 case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
257                     return mAccessibilitySeekListener.onAccessibilitySeekBackward();
258             }
259         }
260         return super.performAccessibilityAction(action, arguments);
261     }
262 
263 }
264