/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.widget.drawer;
import android.animation.Animator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import androidx.wear.R;
import androidx.wear.widget.SimpleAnimatorListener;
import java.util.concurrent.TimeUnit;
/**
* A page indicator for {@link ViewPager} based on {@link
* androidx.wear.view.DotsPageIndicator} which identifies the current page in relation to
* all available pages. Pages are represented as dots. The current page can be highlighted with a
* different color or size dot.
*
* <p>The default behavior is to fade out the dots when the pager is idle (not settling or being
* dragged). This can be changed with {@link #setDotFadeWhenIdle(boolean)}.
*
* <p>Use {@link #setPager(ViewPager)} to connect this view to a pager instance.
*
* @hide
*/
@RestrictTo(Scope.LIBRARY)
public class PageIndicatorView extends View implements OnPageChangeListener {
private static final String TAG = "Dots";
private final Paint mDotPaint;
private final Paint mDotPaintShadow;
private final Paint mDotPaintSelected;
private final Paint mDotPaintShadowSelected;
private int mDotSpacing;
private float mDotRadius;
private float mDotRadiusSelected;
private int mDotColor;
private int mDotColorSelected;
private boolean mDotFadeWhenIdle;
int mDotFadeOutDelay;
int mDotFadeOutDuration;
private int mDotFadeInDuration;
private float mDotShadowDx;
private float mDotShadowDy;
private float mDotShadowRadius;
private int mDotShadowColor;
private PagerAdapter mAdapter;
private int mNumberOfPositions;
private int mSelectedPosition;
private int mCurrentViewPagerState;
boolean mVisible;
public PageIndicatorView(Context context) {
this(context, null);
}
public PageIndicatorView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a =
getContext()
.obtainStyledAttributes(
attrs, R.styleable.PageIndicatorView, defStyleAttr,
R.style.WsPageIndicatorViewStyle);
mDotSpacing = a.getDimensionPixelOffset(
R.styleable.PageIndicatorView_wsPageIndicatorDotSpacing, 0);
mDotRadius = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotRadius, 0);
mDotRadiusSelected =
a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotRadiusSelected, 0);
mDotColor = a.getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotColor, 0);
mDotColorSelected = a
.getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotColorSelected, 0);
mDotFadeOutDelay =
a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeOutDelay, 0);
mDotFadeOutDuration =
a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeOutDuration, 0);
mDotFadeInDuration =
a.getInt(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeInDuration, 0);
mDotFadeWhenIdle =
a.getBoolean(R.styleable.PageIndicatorView_wsPageIndicatorDotFadeWhenIdle, false);
mDotShadowDx = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowDx, 0);
mDotShadowDy = a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowDy, 0);
mDotShadowRadius =
a.getDimension(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowRadius, 0);
mDotShadowColor =
a.getColor(R.styleable.PageIndicatorView_wsPageIndicatorDotShadowColor, 0);
a.recycle();
mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDotPaint.setColor(mDotColor);
mDotPaint.setStyle(Style.FILL);
mDotPaintSelected = new Paint(Paint.ANTI_ALIAS_FLAG);
mDotPaintSelected.setColor(mDotColorSelected);
mDotPaintSelected.setStyle(Style.FILL);
mDotPaintShadow = new Paint(Paint.ANTI_ALIAS_FLAG);
mDotPaintShadowSelected = new Paint(Paint.ANTI_ALIAS_FLAG);
mCurrentViewPagerState = ViewPager.SCROLL_STATE_IDLE;
if (isInEditMode()) {
// When displayed in layout preview:
// Simulate 5 positions, currently on the 3rd position.
mNumberOfPositions = 5;
mSelectedPosition = 2;
mDotFadeWhenIdle = false;
}
if (mDotFadeWhenIdle) {
mVisible = false;
animate().alpha(0f).setStartDelay(2000).setDuration(mDotFadeOutDuration).start();
} else {
animate().cancel();
setAlpha(1.0f);
}
updateShadows();
}
private void updateShadows() {
updateDotPaint(
mDotPaint, mDotPaintShadow, mDotRadius, mDotShadowRadius, mDotColor,
mDotShadowColor);
updateDotPaint(
mDotPaintSelected,
mDotPaintShadowSelected,
mDotRadiusSelected,
mDotShadowRadius,
mDotColorSelected,
mDotShadowColor);
}
private void updateDotPaint(
Paint dotPaint,
Paint shadowPaint,
float baseRadius,
float shadowRadius,
int color,
int shadowColor) {
float radius = baseRadius + shadowRadius;
float shadowStart = baseRadius / radius;
Shader gradient =
new RadialGradient(
0,
0,
radius,
new int[]{shadowColor, shadowColor, Color.TRANSPARENT},
new float[]{0f, shadowStart, 1f},
TileMode.CLAMP);
shadowPaint.setShader(gradient);
dotPaint.setColor(color);
dotPaint.setStyle(Style.FILL);
}
/**
* Supplies the ViewPager instance, and attaches this views {@link OnPageChangeListener} to the
* pager.
*
* @param pager the pager for the page indicator
*/
public void setPager(ViewPager pager) {
pager.addOnPageChangeListener(this);
setPagerAdapter(pager.getAdapter());
mAdapter = pager.getAdapter();
if (mAdapter != null && mAdapter.getCount() > 0) {
positionChanged(0);
}
}
/**
* Gets the center-to-center distance between page dots.
*
* @return the distance between page dots
*/
public float getDotSpacing() {
return mDotSpacing;
}
/**
* Sets the center-to-center distance between page dots.
*
* @param spacing the distance between page dots
*/
public void setDotSpacing(int spacing) {
if (mDotSpacing != spacing) {
mDotSpacing = spacing;
requestLayout();
}
}
/**
* Gets the radius of the page dots.
*
* @return the radius of the page dots
*/
public float getDotRadius() {
return mDotRadius;
}
/**
* Sets the radius of the page dots.
*
* @param radius the radius of the page dots
*/
public void setDotRadius(int radius) {
if (mDotRadius != radius) {
mDotRadius = radius;
updateShadows();
invalidate();
}
}
/**
* Gets the radius of the page dot for the selected page.
*
* @return the radius of the selected page dot
*/
public float getDotRadiusSelected() {
return mDotRadiusSelected;
}
/**
* Sets the radius of the page dot for the selected page.
*
* @param radius the radius of the selected page dot
*/
public void setDotRadiusSelected(int radius) {
if (mDotRadiusSelected != radius) {
mDotRadiusSelected = radius;
updateShadows();
invalidate();
}
}
/**
* Returns the color used for dots other than the selected page.
*
* @return color the color used for dots other than the selected page
*/
public int getDotColor() {
return mDotColor;
}
/**
* Sets the color used for dots other than the selected page.
*
* @param color the color used for dots other than the selected page
*/
public void setDotColor(int color) {
if (mDotColor != color) {
mDotColor = color;
invalidate();
}
}
/**
* Returns the color of the dot for the selected page.
*
* @return the color used for the selected page dot
*/
public int getDotColorSelected() {
return mDotColorSelected;
}
/**
* Sets the color of the dot for the selected page.
*
* @param color the color of the dot for the selected page
*/
public void setDotColorSelected(int color) {
if (mDotColorSelected != color) {
mDotColorSelected = color;
invalidate();
}
}
/**
* Indicates if the dots fade out when the pager is idle.
*
* @return whether the dots fade out when idle
*/
public boolean getDotFadeWhenIdle() {
return mDotFadeWhenIdle;
}
/**
* Sets whether the dots fade out when the pager is idle.
*
* @param fade whether the dots fade out when idle
*/
public void setDotFadeWhenIdle(boolean fade) {
mDotFadeWhenIdle = fade;
if (!fade) {
fadeIn();
}
}
/**
* Returns the duration of fade out animation, in milliseconds.
*
* @return the duration of the fade out animation, in milliseconds
*/
public int getDotFadeOutDuration() {
return mDotFadeOutDuration;
}
/**
* Sets the duration of the fade out animation.
*
* @param duration the duration of the fade out animation
*/
public void setDotFadeOutDuration(int duration, TimeUnit unit) {
mDotFadeOutDuration = (int) TimeUnit.MILLISECONDS.convert(duration, unit);
}
/**
* Returns the duration of the fade in duration, in milliseconds.
*
* @return the duration of the fade in duration, in milliseconds
*/
public int getDotFadeInDuration() {
return mDotFadeInDuration;
}
/**
* Sets the duration of the fade in animation.
*
* @param duration the duration of the fade in animation
*/
public void setDotFadeInDuration(int duration, TimeUnit unit) {
mDotFadeInDuration = (int) TimeUnit.MILLISECONDS.convert(duration, unit);
}
/**
* Sets the delay between the pager arriving at an idle state, and the fade out animation
* beginning, in milliseconds.
*
* @return the delay before the fade out animation begins, in milliseconds
*/
public int getDotFadeOutDelay() {
return mDotFadeOutDelay;
}
/**
* Sets the delay between the pager arriving at an idle state, and the fade out animation
* beginning, in milliseconds.
*
* @param delay the delay before the fade out animation begins, in milliseconds
*/
public void setDotFadeOutDelay(int delay) {
mDotFadeOutDelay = delay;
}
/**
* Sets the pixel radius of shadows drawn beneath the dots.
*
* @return the pixel radius of shadows rendered beneath the dots
*/
public float getDotShadowRadius() {
return mDotShadowRadius;
}
/**
* Sets the pixel radius of shadows drawn beneath the dots.
*
* @param radius the pixel radius of shadows rendered beneath the dots
*/
public void setDotShadowRadius(float radius) {
if (mDotShadowRadius != radius) {
mDotShadowRadius = radius;
updateShadows();
invalidate();
}
}
/**
* Returns the horizontal offset of shadows drawn beneath the dots.
*
* @return the horizontal offset of shadows drawn beneath the dots
*/
public float getDotShadowDx() {
return mDotShadowDx;
}
/**
* Sets the horizontal offset of shadows drawn beneath the dots.
*
* @param dx the horizontal offset of shadows drawn beneath the dots
*/
public void setDotShadowDx(float dx) {
mDotShadowDx = dx;
invalidate();
}
/**
* Returns the vertical offset of shadows drawn beneath the dots.
*
* @return the vertical offset of shadows drawn beneath the dots
*/
public float getDotShadowDy() {
return mDotShadowDy;
}
/**
* Sets the vertical offset of shadows drawn beneath the dots.
*
* @param dy the vertical offset of shadows drawn beneath the dots
*/
public void setDotShadowDy(float dy) {
mDotShadowDy = dy;
invalidate();
}
/**
* Returns the color of the shadows drawn beneath the dots.
*
* @return the color of the shadows drawn beneath the dots
*/
public int getDotShadowColor() {
return mDotShadowColor;
}
/**
* Sets the color of the shadows drawn beneath the dots.
*
* @param color the color of the shadows drawn beneath the dots
*/
public void setDotShadowColor(int color) {
mDotShadowColor = color;
updateShadows();
invalidate();
}
private void positionChanged(int position) {
mSelectedPosition = position;
invalidate();
}
private void updateNumberOfPositions() {
int count = mAdapter.getCount();
if (count != mNumberOfPositions) {
mNumberOfPositions = count;
requestLayout();
}
}
private void fadeIn() {
mVisible = true;
animate().cancel();
animate().alpha(1f).setStartDelay(0).setDuration(mDotFadeInDuration).start();
}
private void fadeOut(long delayMillis) {
mVisible = false;
animate().cancel();
animate().alpha(0f).setStartDelay(delayMillis).setDuration(mDotFadeOutDuration).start();
}
private void fadeInOut() {
mVisible = true;
animate().cancel();
animate()
.alpha(1f)
.setStartDelay(0)
.setDuration(mDotFadeInDuration)
.setListener(
new SimpleAnimatorListener() {
@Override
public void onAnimationComplete(Animator animator) {
mVisible = false;
animate()
.alpha(0f)
.setListener(null)
.setStartDelay(mDotFadeOutDelay)
.setDuration(mDotFadeOutDuration)
.start();
}
})
.start();
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (mDotFadeWhenIdle) {
if (mCurrentViewPagerState == ViewPager.SCROLL_STATE_DRAGGING) {
if (positionOffset != 0) {
if (!mVisible) {
fadeIn();
}
} else {
if (mVisible) {
fadeOut(0);
}
}
}
}
}
@Override
public void onPageSelected(int position) {
if (position != mSelectedPosition) {
positionChanged(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (mCurrentViewPagerState != state) {
mCurrentViewPagerState = state;
if (mDotFadeWhenIdle) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
if (mVisible) {
fadeOut(mDotFadeOutDelay);
} else {
fadeInOut();
}
}
}
}
}
/**
* Sets the {@link PagerAdapter}.
*/
public void setPagerAdapter(PagerAdapter adapter) {
mAdapter = adapter;
if (mAdapter != null) {
updateNumberOfPositions();
if (mDotFadeWhenIdle) {
fadeInOut();
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int totalWidth;
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
totalWidth = MeasureSpec.getSize(widthMeasureSpec);
} else {
int contentWidth = mNumberOfPositions * mDotSpacing;
totalWidth = contentWidth + getPaddingLeft() + getPaddingRight();
}
int totalHeight;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
totalHeight = MeasureSpec.getSize(heightMeasureSpec);
} else {
float maxRadius =
Math.max(mDotRadius + mDotShadowRadius, mDotRadiusSelected + mDotShadowRadius);
int contentHeight = (int) Math.ceil(maxRadius * 2);
contentHeight = (int) (contentHeight + mDotShadowDy);
totalHeight = contentHeight + getPaddingTop() + getPaddingBottom();
}
setMeasuredDimension(
resolveSizeAndState(totalWidth, widthMeasureSpec, 0),
resolveSizeAndState(totalHeight, heightMeasureSpec, 0));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mNumberOfPositions > 1) {
float dotCenterLeft = getPaddingLeft() + (mDotSpacing / 2f);
float dotCenterTop = getHeight() / 2f;
canvas.save();
canvas.translate(dotCenterLeft, dotCenterTop);
for (int i = 0; i < mNumberOfPositions; i++) {
if (i == mSelectedPosition) {
float radius = mDotRadiusSelected + mDotShadowRadius;
canvas.drawCircle(mDotShadowDx, mDotShadowDy, radius, mDotPaintShadowSelected);
canvas.drawCircle(0, 0, mDotRadiusSelected, mDotPaintSelected);
} else {
float radius = mDotRadius + mDotShadowRadius;
canvas.drawCircle(mDotShadowDx, mDotShadowDy, radius, mDotPaintShadow);
canvas.drawCircle(0, 0, mDotRadius, mDotPaint);
}
canvas.translate(mDotSpacing, 0);
}
canvas.restore();
}
}
/**
* Notifies the view that the data set has changed.
*/
public void notifyDataSetChanged() {
if (mAdapter != null && mAdapter.getCount() > 0) {
updateNumberOfPositions();
}
}
}