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.wear.widget.drawer;
18 
19 import android.animation.Animator;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Paint;
25 import android.graphics.Paint.Style;
26 import android.graphics.RadialGradient;
27 import android.graphics.Shader;
28 import android.graphics.Shader.TileMode;
29 import android.os.Build;
30 import android.util.AttributeSet;
31 import android.view.View;
32 
33 import androidx.annotation.RequiresApi;
34 import androidx.annotation.RestrictTo;
35 import androidx.annotation.RestrictTo.Scope;
36 import androidx.viewpager.widget.PagerAdapter;
37 import androidx.viewpager.widget.ViewPager;
38 import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
39 import androidx.wear.R;
40 import androidx.wear.widget.SimpleAnimatorListener;
41 
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * A page indicator for {@link ViewPager} based on {@link
46  * androidx.wear.view.DotsPageIndicator} which identifies the current page in relation to
47  * all available pages. Pages are represented as dots. The current page can be highlighted with a
48  * different color or size dot.
49  *
50  * <p>The default behavior is to fade out the dots when the pager is idle (not settling or being
51  * dragged). This can be changed with {@link #setDotFadeWhenIdle(boolean)}.
52  *
53  * <p>Use {@link #setPager(ViewPager)} to connect this view to a pager instance.
54  *
55  * @hide
56  */
57 @RequiresApi(Build.VERSION_CODES.M)
58 @RestrictTo(Scope.LIBRARY)
59 public class PageIndicatorView extends View implements OnPageChangeListener {
60 
61     private static final String TAG = "Dots";
62     private final Paint mDotPaint;
63     private final Paint mDotPaintShadow;
64     private final Paint mDotPaintSelected;
65     private final Paint mDotPaintShadowSelected;
66     private int mDotSpacing;
67     private float mDotRadius;
68     private float mDotRadiusSelected;
69     private int mDotColor;
70     private int mDotColorSelected;
71     private boolean mDotFadeWhenIdle;
72     private int mDotFadeOutDelay;
73     private int mDotFadeOutDuration;
74     private int mDotFadeInDuration;
75     private float mDotShadowDx;
76     private float mDotShadowDy;
77     private float mDotShadowRadius;
78     private int mDotShadowColor;
79     private PagerAdapter mAdapter;
80     private int mNumberOfPositions;
81     private int mSelectedPosition;
82     private int mCurrentViewPagerState;
83     private boolean mVisible;
84 
PageIndicatorView(Context context)85     public PageIndicatorView(Context context) {
86         this(context, null);
87     }
88 
PageIndicatorView(Context context, AttributeSet attrs)89     public PageIndicatorView(Context context, AttributeSet attrs) {
90         this(context, attrs, 0);
91     }
92 
PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr)93     public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
94         super(context, attrs, defStyleAttr);
95 
96         final TypedArray a =
97                 getContext()
98                         .obtainStyledAttributes(
99                                 attrs, R.styleable.PageIndicatorView, defStyleAttr,
100                                 R.style.WsPageIndicatorViewStyle);
101 
102         mDotSpacing = a.getDimensionPixelOffset(
103                 R.styleable.PageIndicatorView_wsPageIndicatorDotSpacing, 0);
104         mDotRadius = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotRadius, 0);
105         mDotRadiusSelected =
106                 a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotRadiusSelected, 0);
107         mDotColor = a.getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotColor, 0);
108         mDotColorSelected = a
109                 .getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotColorSelected, 0);
110         mDotFadeOutDelay =
111                 a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeOutDelay, 0);
112         mDotFadeOutDuration =
113                 a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeOutDuration, 0);
114         mDotFadeInDuration =
115                 a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeInDuration, 0);
116         mDotFadeWhenIdle =
117                 a.getBoolean(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeWhenIdle, false);
118         mDotShadowDx = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowDx, 0);
119         mDotShadowDy = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowDy, 0);
120         mDotShadowRadius =
121                 a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowRadius, 0);
122         mDotShadowColor =
123                 a.getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowColor, 0);
124         a.recycle();
125 
126         mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
127         mDotPaint.setColor(mDotColor);
128         mDotPaint.setStyle(Style.FILL);
129 
130         mDotPaintSelected = new Paint(Paint.ANTI_ALIAS_FLAG);
131         mDotPaintSelected.setColor(mDotColorSelected);
132         mDotPaintSelected.setStyle(Style.FILL);
133         mDotPaintShadow = new Paint(Paint.ANTI_ALIAS_FLAG);
134         mDotPaintShadowSelected = new Paint(Paint.ANTI_ALIAS_FLAG);
135 
136         mCurrentViewPagerState = ViewPager.SCROLL_STATE_IDLE;
137         if (isInEditMode()) {
138             // When displayed in layout preview:
139             // Simulate 5 positions, currently on the 3rd position.
140             mNumberOfPositions = 5;
141             mSelectedPosition = 2;
142             mDotFadeWhenIdle = false;
143         }
144 
145         if (mDotFadeWhenIdle) {
146             mVisible = false;
147             animate().alpha(0f).setStartDelay(2000).setDuration(mDotFadeOutDuration).start();
148         } else {
149             animate().cancel();
150             setAlpha(1.0f);
151         }
152         updateShadows();
153     }
154 
updateShadows()155     private void updateShadows() {
156         updateDotPaint(
157                 mDotPaint, mDotPaintShadow, mDotRadius, mDotShadowRadius, mDotColor,
158                 mDotShadowColor);
159         updateDotPaint(
160                 mDotPaintSelected,
161                 mDotPaintShadowSelected,
162                 mDotRadiusSelected,
163                 mDotShadowRadius,
164                 mDotColorSelected,
165                 mDotShadowColor);
166     }
167 
updateDotPaint( Paint dotPaint, Paint shadowPaint, float baseRadius, float shadowRadius, int color, int shadowColor)168     private void updateDotPaint(
169             Paint dotPaint,
170             Paint shadowPaint,
171             float baseRadius,
172             float shadowRadius,
173             int color,
174             int shadowColor) {
175         float radius = baseRadius + shadowRadius;
176         float shadowStart = baseRadius / radius;
177         Shader gradient =
178                 new RadialGradient(
179                         0,
180                         0,
181                         radius,
182                         new int[]{shadowColor, shadowColor, Color.TRANSPARENT},
183                         new float[]{0f, shadowStart, 1f},
184                         TileMode.CLAMP);
185 
186         shadowPaint.setShader(gradient);
187         dotPaint.setColor(color);
188         dotPaint.setStyle(Style.FILL);
189     }
190 
191     /**
192      * Supplies the ViewPager instance, and attaches this views {@link OnPageChangeListener} to the
193      * pager.
194      *
195      * @param pager the pager for the page indicator
196      */
setPager(ViewPager pager)197     public void setPager(ViewPager pager) {
198         pager.addOnPageChangeListener(this);
199         setPagerAdapter(pager.getAdapter());
200         mAdapter = pager.getAdapter();
201         if (mAdapter != null && mAdapter.getCount() > 0) {
202             positionChanged(0);
203         }
204     }
205 
206     /**
207      * Gets the center-to-center distance between page dots.
208      *
209      * @return the distance between page dots
210      */
getDotSpacing()211     public float getDotSpacing() {
212         return mDotSpacing;
213     }
214 
215     /**
216      * Sets the center-to-center distance between page dots.
217      *
218      * @param spacing the distance between page dots
219      */
setDotSpacing(int spacing)220     public void setDotSpacing(int spacing) {
221         if (mDotSpacing != spacing) {
222             mDotSpacing = spacing;
223             requestLayout();
224         }
225     }
226 
227     /**
228      * Gets the radius of the page dots.
229      *
230      * @return the radius of the page dots
231      */
getDotRadius()232     public float getDotRadius() {
233         return mDotRadius;
234     }
235 
236     /**
237      * Sets the radius of the page dots.
238      *
239      * @param radius the radius of the page dots
240      */
setDotRadius(int radius)241     public void setDotRadius(int radius) {
242         if (mDotRadius != radius) {
243             mDotRadius = radius;
244             updateShadows();
245             invalidate();
246         }
247     }
248 
249     /**
250      * Gets the radius of the page dot for the selected page.
251      *
252      * @return the radius of the selected page dot
253      */
getDotRadiusSelected()254     public float getDotRadiusSelected() {
255         return mDotRadiusSelected;
256     }
257 
258     /**
259      * Sets the radius of the page dot for the selected page.
260      *
261      * @param radius the radius of the selected page dot
262      */
setDotRadiusSelected(int radius)263     public void setDotRadiusSelected(int radius) {
264         if (mDotRadiusSelected != radius) {
265             mDotRadiusSelected = radius;
266             updateShadows();
267             invalidate();
268         }
269     }
270 
271     /**
272      * Returns the color used for dots other than the selected page.
273      *
274      * @return color the color used for dots other than the selected page
275      */
getDotColor()276     public int getDotColor() {
277         return mDotColor;
278     }
279 
280     /**
281      * Sets the color used for dots other than the selected page.
282      *
283      * @param color the color used for dots other than the selected page
284      */
setDotColor(int color)285     public void setDotColor(int color) {
286         if (mDotColor != color) {
287             mDotColor = color;
288             invalidate();
289         }
290     }
291 
292     /**
293      * Returns the color of the dot for the selected page.
294      *
295      * @return the color used for the selected page dot
296      */
getDotColorSelected()297     public int getDotColorSelected() {
298         return mDotColorSelected;
299     }
300 
301     /**
302      * Sets the color of the dot for the selected page.
303      *
304      * @param color the color of the dot for the selected page
305      */
setDotColorSelected(int color)306     public void setDotColorSelected(int color) {
307         if (mDotColorSelected != color) {
308             mDotColorSelected = color;
309             invalidate();
310         }
311     }
312 
313     /**
314      * Indicates if the dots fade out when the pager is idle.
315      *
316      * @return whether the dots fade out when idle
317      */
getDotFadeWhenIdle()318     public boolean getDotFadeWhenIdle() {
319         return mDotFadeWhenIdle;
320     }
321 
322     /**
323      * Sets whether the dots fade out when the pager is idle.
324      *
325      * @param fade whether the dots fade out when idle
326      */
setDotFadeWhenIdle(boolean fade)327     public void setDotFadeWhenIdle(boolean fade) {
328         mDotFadeWhenIdle = fade;
329         if (!fade) {
330             fadeIn();
331         }
332     }
333 
334     /**
335      * Returns the duration of fade out animation, in milliseconds.
336      *
337      * @return the duration of the fade out animation, in milliseconds
338      */
getDotFadeOutDuration()339     public int getDotFadeOutDuration() {
340         return mDotFadeOutDuration;
341     }
342 
343     /**
344      * Sets the duration of the fade out animation.
345      *
346      * @param duration the duration of the fade out animation
347      */
setDotFadeOutDuration(int duration, TimeUnit unit)348     public void setDotFadeOutDuration(int duration, TimeUnit unit) {
349         mDotFadeOutDuration = (int) TimeUnit.MILLISECONDS.convert(duration, unit);
350     }
351 
352     /**
353      * Returns the duration of the fade in duration, in milliseconds.
354      *
355      * @return the duration of the fade in duration, in milliseconds
356      */
getDotFadeInDuration()357     public int getDotFadeInDuration() {
358         return mDotFadeInDuration;
359     }
360 
361     /**
362      * Sets the duration of the fade in animation.
363      *
364      * @param duration the duration of the fade in animation
365      */
setDotFadeInDuration(int duration, TimeUnit unit)366     public void setDotFadeInDuration(int duration, TimeUnit unit) {
367         mDotFadeInDuration = (int) TimeUnit.MILLISECONDS.convert(duration, unit);
368     }
369 
370     /**
371      * Sets the delay between the pager arriving at an idle state, and the fade out animation
372      * beginning, in milliseconds.
373      *
374      * @return the delay before the fade out animation begins, in milliseconds
375      */
getDotFadeOutDelay()376     public int getDotFadeOutDelay() {
377         return mDotFadeOutDelay;
378     }
379 
380     /**
381      * Sets the delay between the pager arriving at an idle state, and the fade out animation
382      * beginning, in milliseconds.
383      *
384      * @param delay the delay before the fade out animation begins, in milliseconds
385      */
setDotFadeOutDelay(int delay)386     public void setDotFadeOutDelay(int delay) {
387         mDotFadeOutDelay = delay;
388     }
389 
390     /**
391      * Sets the pixel radius of shadows drawn beneath the dots.
392      *
393      * @return the pixel radius of shadows rendered beneath the dots
394      */
getDotShadowRadius()395     public float getDotShadowRadius() {
396         return mDotShadowRadius;
397     }
398 
399     /**
400      * Sets the pixel radius of shadows drawn beneath the dots.
401      *
402      * @param radius the pixel radius of shadows rendered beneath the dots
403      */
setDotShadowRadius(float radius)404     public void setDotShadowRadius(float radius) {
405         if (mDotShadowRadius != radius) {
406             mDotShadowRadius = radius;
407             updateShadows();
408             invalidate();
409         }
410     }
411 
412     /**
413      * Returns the horizontal offset of shadows drawn beneath the dots.
414      *
415      * @return the horizontal offset of shadows drawn beneath the dots
416      */
getDotShadowDx()417     public float getDotShadowDx() {
418         return mDotShadowDx;
419     }
420 
421     /**
422      * Sets the horizontal offset of shadows drawn beneath the dots.
423      *
424      * @param dx the horizontal offset of shadows drawn beneath the dots
425      */
setDotShadowDx(float dx)426     public void setDotShadowDx(float dx) {
427         mDotShadowDx = dx;
428         invalidate();
429     }
430 
431     /**
432      * Returns the vertical offset of shadows drawn beneath the dots.
433      *
434      * @return the vertical offset of shadows drawn beneath the dots
435      */
getDotShadowDy()436     public float getDotShadowDy() {
437         return mDotShadowDy;
438     }
439 
440     /**
441      * Sets the vertical offset of shadows drawn beneath the dots.
442      *
443      * @param dy the vertical offset of shadows drawn beneath the dots
444      */
setDotShadowDy(float dy)445     public void setDotShadowDy(float dy) {
446         mDotShadowDy = dy;
447         invalidate();
448     }
449 
450     /**
451      * Returns the color of the shadows drawn beneath the dots.
452      *
453      * @return the color of the shadows drawn beneath the dots
454      */
getDotShadowColor()455     public int getDotShadowColor() {
456         return mDotShadowColor;
457     }
458 
459     /**
460      * Sets the color of the shadows drawn beneath the dots.
461      *
462      * @param color the color of the shadows drawn beneath the dots
463      */
setDotShadowColor(int color)464     public void setDotShadowColor(int color) {
465         mDotShadowColor = color;
466         updateShadows();
467         invalidate();
468     }
469 
positionChanged(int position)470     private void positionChanged(int position) {
471         mSelectedPosition = position;
472         invalidate();
473     }
474 
updateNumberOfPositions()475     private void updateNumberOfPositions() {
476         int count = mAdapter.getCount();
477         if (count != mNumberOfPositions) {
478             mNumberOfPositions = count;
479             requestLayout();
480         }
481     }
482 
fadeIn()483     private void fadeIn() {
484         mVisible = true;
485         animate().cancel();
486         animate().alpha(1f).setStartDelay(0).setDuration(mDotFadeInDuration).start();
487     }
488 
fadeOut(long delayMillis)489     private void fadeOut(long delayMillis) {
490         mVisible = false;
491         animate().cancel();
492         animate().alpha(0f).setStartDelay(delayMillis).setDuration(mDotFadeOutDuration).start();
493     }
494 
fadeInOut()495     private void fadeInOut() {
496         mVisible = true;
497         animate().cancel();
498         animate()
499                 .alpha(1f)
500                 .setStartDelay(0)
501                 .setDuration(mDotFadeInDuration)
502                 .setListener(
503                         new SimpleAnimatorListener() {
504                             @Override
505                             public void onAnimationComplete(Animator animator) {
506                                 mVisible = false;
507                                 animate()
508                                         .alpha(0f)
509                                         .setListener(null)
510                                         .setStartDelay(mDotFadeOutDelay)
511                                         .setDuration(mDotFadeOutDuration)
512                                         .start();
513                             }
514                         })
515                 .start();
516     }
517 
518     @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)519     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
520         if (mDotFadeWhenIdle) {
521             if (mCurrentViewPagerState == ViewPager.SCROLL_STATE_DRAGGING) {
522                 if (positionOffset != 0) {
523                     if (!mVisible) {
524                         fadeIn();
525                     }
526                 } else {
527                     if (mVisible) {
528                         fadeOut(0);
529                     }
530                 }
531             }
532         }
533     }
534 
535     @Override
onPageSelected(int position)536     public void onPageSelected(int position) {
537         if (position != mSelectedPosition) {
538             positionChanged(position);
539         }
540     }
541 
542     @Override
onPageScrollStateChanged(int state)543     public void onPageScrollStateChanged(int state) {
544         if (mCurrentViewPagerState != state) {
545             mCurrentViewPagerState = state;
546             if (mDotFadeWhenIdle) {
547                 if (state == ViewPager.SCROLL_STATE_IDLE) {
548                     if (mVisible) {
549                         fadeOut(mDotFadeOutDelay);
550                     } else {
551                         fadeInOut();
552                     }
553                 }
554             }
555         }
556     }
557 
558     /**
559      * Sets the {@link PagerAdapter}.
560      */
setPagerAdapter(PagerAdapter adapter)561     public void setPagerAdapter(PagerAdapter adapter) {
562         mAdapter = adapter;
563         if (mAdapter != null) {
564             updateNumberOfPositions();
565             if (mDotFadeWhenIdle) {
566                 fadeInOut();
567             }
568         }
569     }
570 
571     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)572     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
573         int totalWidth;
574         if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
575             totalWidth = MeasureSpec.getSize(widthMeasureSpec);
576         } else {
577             int contentWidth = mNumberOfPositions * mDotSpacing;
578             totalWidth = contentWidth + getPaddingLeft() + getPaddingRight();
579         }
580         int totalHeight;
581         if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
582             totalHeight = MeasureSpec.getSize(heightMeasureSpec);
583         } else {
584             float maxRadius =
585                     Math.max(mDotRadius + mDotShadowRadius, mDotRadiusSelected + mDotShadowRadius);
586             int contentHeight = (int) Math.ceil(maxRadius * 2);
587             contentHeight = (int) (contentHeight + mDotShadowDy);
588             totalHeight = contentHeight + getPaddingTop() + getPaddingBottom();
589         }
590         setMeasuredDimension(
591                 resolveSizeAndState(totalWidth, widthMeasureSpec, 0),
592                 resolveSizeAndState(totalHeight, heightMeasureSpec, 0));
593     }
594 
595     @Override
onDraw(Canvas canvas)596     protected void onDraw(Canvas canvas) {
597         super.onDraw(canvas);
598 
599         if (mNumberOfPositions > 1) {
600             float dotCenterLeft = getPaddingLeft() + (mDotSpacing / 2f);
601             float dotCenterTop = getHeight() / 2f;
602             canvas.save();
603             canvas.translate(dotCenterLeft, dotCenterTop);
604             for (int i = 0; i < mNumberOfPositions; i++) {
605                 if (i == mSelectedPosition) {
606                     float radius = mDotRadiusSelected + mDotShadowRadius;
607                     canvas.drawCircle(mDotShadowDx, mDotShadowDy, radius, mDotPaintShadowSelected);
608                     canvas.drawCircle(0, 0, mDotRadiusSelected, mDotPaintSelected);
609                 } else {
610                     float radius = mDotRadius + mDotShadowRadius;
611                     canvas.drawCircle(mDotShadowDx, mDotShadowDy, radius, mDotPaintShadow);
612                     canvas.drawCircle(0, 0, mDotRadius, mDotPaint);
613                 }
614                 canvas.translate(mDotSpacing, 0);
615             }
616             canvas.restore();
617         }
618     }
619 
620     /**
621      * Notifies the view that the data set has changed.
622      */
notifyDataSetChanged()623     public void notifyDataSetChanged() {
624         if (mAdapter != null && mAdapter.getCount() > 0) {
625             updateNumberOfPositions();
626         }
627     }
628 }
629