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