Java tutorial
/* * Copyright (c) 2017 Gowtham Parimelazhagan * * 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 com.gm.common.ui.widget.pageindicator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.os.Build; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.text.TextUtilsCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.Pair; import android.view.View; import com.gm.common.R; import com.gm.common.ui.widget.pageindicator.animation.AbsAnimation; import com.gm.common.ui.widget.pageindicator.animation.AnimationType; import com.gm.common.ui.widget.pageindicator.animation.ColorAnimation; import com.gm.common.ui.widget.pageindicator.animation.FillAnimation; import com.gm.common.ui.widget.pageindicator.animation.ScaleAnimation; import com.gm.common.ui.widget.pageindicator.animation.ValueAnimation; import com.gm.common.util.DensityUtils; import com.gm.common.util.IdUtils; public class PageIndicatorView extends View implements ViewPager.OnPageChangeListener { private static final int DEFAULT_CIRCLES_COUNT = 3; private static final int COUNT_NOT_SET = -1; private static final int DEFAULT_RADIUS_DP = 6; private static final int DEFAULT_PADDING_DP = 8; private int radiusPx; private int paddingPx; private int strokePx; private int count; private boolean isCountSet; //Color private int unselectedColor; private int selectedColor; private int frameColor; private int frameColorReverse; // Orientation private Orientation orientation = Orientation.HORIZONTAL; //Scale private int frameRadiusPx; private int frameRadiusReversePx; private float scaleFactor; //Fill private int frameStrokePx; private int frameStrokeReversePx; //Worm private int frameFrom; private int frameTo; //Slide & Drop private int frameSlideFrom; private int frameY; //Thin Worm private int frameHeight; private boolean autoVisibility; private int selectedPosition; private int selectingPosition; private int lastSelectedPosition; private boolean isFrameValuesSet; private boolean interactiveAnimation; private long animationDuration; private DataSetObserver setObserver; private boolean dynamicCount; private Paint fillPaint = new Paint(); private Paint strokePaint = new Paint(); private RectF rect = new RectF(); private AnimationType animationType = AnimationType.NONE; private ValueAnimation animation; private ViewPager viewPager; private int viewPagerId; private RtlMode rtlMode = RtlMode.Off; public PageIndicatorView(Context context) { super(context); init(null); } public PageIndicatorView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public PageIndicatorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(attrs); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); findViewPager(); } @Override protected void onDetachedFromWindow() { unRegisterSetObserver(); super.onDetachedFromWindow(); } @Override public Parcelable onSaveInstanceState() { PositionSavedState positionSavedState = new PositionSavedState(super.onSaveInstanceState()); positionSavedState.setSelectedPosition(selectedPosition); positionSavedState.setSelectingPosition(selectingPosition); positionSavedState.setLastSelectedPosition(lastSelectedPosition); return positionSavedState; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof PositionSavedState) { PositionSavedState positionSavedState = (PositionSavedState) state; this.selectedPosition = positionSavedState.getSelectedPosition(); this.selectingPosition = positionSavedState.getSelectingPosition(); this.lastSelectedPosition = positionSavedState.getLastSelectedPosition(); super.onRestoreInstanceState(positionSavedState.getSuperState()); } else { super.onRestoreInstanceState(state); } } @SuppressWarnings("UnnecessaryLocalVariable") @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int circleDiameterPx = radiusPx * 2; int desiredWidth = 0; int desiredHeight = 0; if (orientation == Orientation.HORIZONTAL) { desiredHeight = circleDiameterPx + strokePx; } else { desiredWidth = circleDiameterPx + strokePx; } if (count != 0) { int diameterSum = circleDiameterPx * count; int strokeSum = (strokePx * 2) * count; int paddingSum = paddingPx * (count - 1); if (orientation == Orientation.HORIZONTAL) { desiredWidth = diameterSum + strokeSum + paddingSum; } else { desiredHeight = diameterSum + strokeSum + paddingSum; } } int width; int height; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); } else { width = desiredWidth; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desiredHeight, heightSize); } else { height = desiredHeight; } if (animationType == AnimationType.DROP) { if (orientation == Orientation.HORIZONTAL) { height *= 2; } else { width *= 2; } } if (width < 0) { width = 0; } if (height < 0) { height = 0; } setMeasuredDimension(width, height); } private boolean isViewMeasured() { return getMeasuredHeight() != 0 || getMeasuredWidth() != 0; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); setupFrameValues(); } @Override protected void onDraw(Canvas canvas) { drawIndicatorView(canvas); } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (isViewMeasured() && interactiveAnimation && animationType != AnimationType.NONE) { onPageScroll(position, positionOffset); } } @Override public void onPageSelected(int position) { if (viewPager != null && viewPager.getAdapter() != null) { int pageCount = viewPager.getAdapter().getCount(); if (pageCount < count) { return; } } if (isViewMeasured() && (!interactiveAnimation || animationType == AnimationType.NONE)) { if (isRtl()) { position = (count - 1) - position; } setSelection(position); } } @Override public void onPageScrollStateChanged(int state) { /*empty*/} /** * Set static number of circle indicators to be displayed. * * @param count total count of indicators. */ public void setCount(int count) { if (this.count != count) { this.count = count; this.isCountSet = true; resetFrameValues(); updateVisibility(); requestLayout(); } } /** * Return number of circle indicators */ public int getCount() { return count; } /** * Dynamic count will automatically update number of circle indicators * if {@link ViewPager} page count updated on run-time. If new count will be bigger than current count, * selected circle will stay as it is, otherwise it will be set to last one. * Note: works if {@link ViewPager} set and already have it's adapter. See {@link #setViewPager(ViewPager)}. * * @param dynamicCount boolean value to add/remove indicators dynamically. */ public void setDynamicCount(boolean dynamicCount) { this.dynamicCount = dynamicCount; if (dynamicCount) { registerSetObserver(); } else { unRegisterSetObserver(); } } /** * Set radius in dp of each circle indicator. Default value is {@link PageIndicatorView#DEFAULT_RADIUS_DP}. * Note: make sure you set circle Radius, not a Diameter. * * @param radiusDp radius of circle in dp. */ public void setRadius(int radiusDp) { if (radiusDp < 0) { radiusDp = 0; } radiusPx = DensityUtils.dpToPx(radiusDp); invalidate(); } /** * Set radius in px of each circle indicator. Default value is {@link PageIndicatorView#DEFAULT_RADIUS_DP}. * Note: make sure you set circle Radius, not a Diameter. * * @param radiusPx radius of circle in px. */ public void setRadius(float radiusPx) { if (radiusPx < 0) { radiusPx = 0; } this.radiusPx = (int) radiusPx; invalidate(); } /** * Return radius of each circle indicators in px. If custom radius is not set, return * default value {@link PageIndicatorView#DEFAULT_RADIUS_DP}. */ public int getRadius() { return radiusPx; } /** * Set padding in dp between each circle indicator. Default value is {@link PageIndicatorView#DEFAULT_PADDING_DP}. * * @param paddingDp padding between circles in dp. */ public void setPadding(int paddingDp) { if (paddingDp < 0) { paddingDp = 0; } paddingPx = DensityUtils.dpToPx(paddingDp); invalidate(); } /** * Set padding in px between each circle indicator. Default value is {@link PageIndicatorView#DEFAULT_PADDING_DP}. * * @param paddingPx padding between circles in px. */ public void setPadding(float paddingPx) { if (paddingPx < 0) { paddingPx = 0; } this.paddingPx = (int) paddingPx; invalidate(); } /** * Return padding in px between each circle indicator. If custom padding is not set, * return default value {@link PageIndicatorView#DEFAULT_PADDING_DP}. */ public int getPadding() { return paddingPx; } /** * Set scale factor used in {@link AnimationType#SCALE} animation. * Defines size of unselected indicator circles in comparing to selected one. * Minimum and maximum values are {@link ScaleAnimation#MAX_SCALE_FACTOR} and {@link ScaleAnimation#MIN_SCALE_FACTOR}. * See also {@link ScaleAnimation#DEFAULT_SCALE_FACTOR}. * * @param factor float value in range between 0 and 1. */ public void setScaleFactor(float factor) { if (factor > ScaleAnimation.MAX_SCALE_FACTOR) { factor = ScaleAnimation.MAX_SCALE_FACTOR; } else if (factor < ScaleAnimation.MIN_SCALE_FACTOR) { factor = ScaleAnimation.MIN_SCALE_FACTOR; } scaleFactor = factor; } /** * Returns scale factor values used in {@link AnimationType#SCALE} animation. * Defines size of unselected indicator circles in comparing to selected one. * Minimum and maximum values are {@link ScaleAnimation#MAX_SCALE_FACTOR} and {@link ScaleAnimation#MIN_SCALE_FACTOR}. * See also {@link ScaleAnimation#DEFAULT_SCALE_FACTOR}. * * @return float value that indicate scale factor. */ public float getScaleFactor() { return scaleFactor; } /** * Set stroke width in px to draw while {@link AnimationType#FILL} is selected. * Default value is {@link FillAnimation#DEFAULT_STROKE_DP} * * @param strokePx stroke width in px. */ public void setStrokeWidth(float strokePx) { if (strokePx < 0) { strokePx = 0; } else if (strokePx > radiusPx) { strokePx = radiusPx; } this.strokePx = (int) strokePx; invalidate(); } /** * Set stroke width in dp to draw while {@link AnimationType#FILL} is selected. * Default value is {@link FillAnimation#DEFAULT_STROKE_DP} * * @param strokeDp stroke width in dp. */ public void setStrokeWidth(int strokeDp) { int strokePx = DensityUtils.dpToPx(strokeDp); if (strokePx < 0) { strokePx = 0; } else if (strokePx > radiusPx) { strokePx = radiusPx; } this.strokePx = strokePx; invalidate(); } /** * Return stroke width in px. If custom stroke width is not set and {@link AnimationType#FILL} is selected. */ public int getStrokeWidth() { return strokePx; } /** * Set color of unselected state to each circle indicator. Default color {@link ColorAnimation#DEFAULT_UNSELECTED_COLOR}. * * @param color color of each unselected circle. */ public void setUnselectedColor(int color) { unselectedColor = color; invalidate(); } /** * Return color of unselected state of each circle indicator. If custom unselected color * is not set, return default color {@link ColorAnimation#DEFAULT_UNSELECTED_COLOR}. */ public int getUnselectedColor() { return unselectedColor; } /** * Set color of selected state to circle indicator. Default color is white {@link ColorAnimation#DEFAULT_SELECTED_COLOR}. * * @param color color selected circle. */ public void setSelectedColor(int color) { selectedColor = color; invalidate(); } /** * Automatically hide (View.INVISIBLE) PageIndicatorView while indicator count is <= 1. * Default is true. * * @param autoVisibility auto hide indicators. */ public void setAutoVisibility(boolean autoVisibility) { if (!autoVisibility) { setVisibility(VISIBLE); } this.autoVisibility = autoVisibility; updateVisibility(); } /** * Set orientation for indicator, one of HORIZONTAL or VERTICAL. * Default is HORIZONTAL. * * @param orientation an orientation to display page indicators.. */ public void setOrientation(@Nullable Orientation orientation) { if (orientation != null) { this.orientation = orientation; requestLayout(); } } /** * Return color of selected circle indicator. If custom unselected color. * is not set, return default color {@link ColorAnimation#DEFAULT_SELECTED_COLOR}. */ public int getSelectedColor() { return selectedColor; } /** * Set animation duration time in millisecond. Default animation duration time is {@link AbsAnimation#DEFAULT_ANIMATION_TIME}. * (Won't affect on anything unless {@link #setAnimationType(AnimationType type)} is specified * and {@link #setInteractiveAnimation(boolean isInteractive)} is false). * * @param duration animation duration time. */ public void setAnimationDuration(long duration) { animationDuration = duration; } /** * Return animation duration time in milliseconds. If custom duration is not set, * return default duration time {@link AbsAnimation#DEFAULT_ANIMATION_TIME}. */ public long getAnimationDuration() { return animationDuration; } /** * Set animation type to perform while selecting new circle indicator. * Default animation type is {@link AnimationType#NONE}. * * @param type type of animation, one of {@link AnimationType} */ public void setAnimationType(@Nullable AnimationType type) { if (type != null) { animationType = type; } else { animationType = AnimationType.NONE; } } /** * Set boolean value to perform interactive animation while selecting new indicator. * * @param isInteractive value of animation to be interactive or not. */ public void setInteractiveAnimation(boolean isInteractive) { interactiveAnimation = isInteractive; } /** * Set progress value in range [0 - 1] to specify state of animation while selecting new circle indicator. * (Won't affect on anything unless {@link #setInteractiveAnimation(boolean isInteractive)} is false). * * @param selectingPosition selecting position with specific progress value. * @param progress float value of progress. */ public void setProgress(int selectingPosition, float progress) { if (interactiveAnimation) { if (count <= 0 || selectingPosition < 0) { selectingPosition = 0; } else if (selectingPosition > count - 1) { selectingPosition = count - 1; } if (progress < 0) { progress = 0; } else if (progress > 1) { progress = 1; } this.selectingPosition = selectingPosition; setAnimationProgress(progress); } } /** * Set specific circle indicator position to be selected. If position < or > total count, * accordingly first or last circle indicator will be selected. * * @param position position of indicator to select. */ public void setSelection(int position) { if (position < 0) { position = 0; } else if (position > count - 1) { position = count - 1; } lastSelectedPosition = selectedPosition; selectedPosition = position; switch (animationType) { case NONE: invalidate(); break; case COLOR: startColorAnimation(); break; case SCALE: startScaleAnimation(); break; case WORM: startWormAnimation(); break; case FILL: startFillAnimation(); break; case SLIDE: startSlideAnimation(); break; case THIN_WORM: startThinWormAnimation(); break; case DROP: startDropAnimation(); break; case SWAP: startSwapAnimation(); break; } } /** * Return position of currently selected circle indicator. */ public int getSelection() { return selectedPosition; } /** * Set {@link ViewPager} to add {@link ViewPager.OnPageChangeListener} and automatically * handle selecting new indicators (and interactive animation effect if it is enabled). * * @param pager instance of {@link ViewPager} to work with */ public void setViewPager(@Nullable ViewPager pager) { releaseViewPager(); if (pager == null) { return; } viewPager = pager; viewPager.addOnPageChangeListener(this); setDynamicCount(dynamicCount); int count = getViewPagerCount(); if (isRtl()) { int selected = viewPager.getCurrentItem(); this.selectedPosition = (count - 1) - selected; } setCount(count); } /** * Release {@link ViewPager} and stop handling events of {@link ViewPager.OnPageChangeListener}. */ public void releaseViewPager() { if (viewPager != null) { viewPager.removeOnPageChangeListener(this); viewPager = null; } } /** * Specify to display PageIndicatorView with Right to left layout or not. * One of {@link RtlMode}: Off (Left to right), On (Right to left) * or Auto (handle this mode automatically based on users language preferences). * Default is Off. * * @param mode instance of {@link RtlMode} */ public void setRtlMode(@Nullable RtlMode mode) { if (mode == null) { rtlMode = RtlMode.Off; } else { rtlMode = mode; } } private void onPageScroll(int position, float positionOffset) { Pair<Integer, Float> progressPair = getProgress(position, positionOffset); int selectingPosition = progressPair.first; float selectingProgress = progressPair.second; if (selectingProgress == 1) { lastSelectedPosition = selectedPosition; selectedPosition = selectingPosition; } setProgress(selectingPosition, selectingProgress); } private void drawIndicatorView(@NonNull Canvas canvas) { for (int i = 0; i < count; i++) { int x = getXCoordinate(i); int y = getYCoordinate(i); drawCircle(canvas, i, x, y); } } private void drawCircle(@NonNull Canvas canvas, int position, int x, int y) { boolean selectedItem = !interactiveAnimation && (position == selectedPosition || position == lastSelectedPosition); boolean selectingItem = interactiveAnimation && (position == selectingPosition || position == selectedPosition); boolean isSelectedItem = selectedItem | selectingItem; if (isSelectedItem) drawWithAnimationEffect(canvas, position, x, y); else drawWithNoEffect(canvas, position, x, y); } private void drawWithAnimationEffect(@NonNull Canvas canvas, int position, int x, int y) { switch (animationType) { case NONE: drawWithNoEffect(canvas, position, x, y); break; case COLOR: drawWithColorAnimation(canvas, position, x, y); break; case SCALE: drawWithScaleAnimation(canvas, position, x, y); break; case SLIDE: drawWithSlideAnimation(canvas, position, x, y); break; case WORM: drawWithWormAnimation(canvas, x, y); break; case FILL: drawWithFillAnimation(canvas, position, x, y); break; case THIN_WORM: drawWithThinWormAnimation(canvas, x, y); break; case DROP: drawWithDropAnimation(canvas, x, y); break; case SWAP: if (orientation == Orientation.HORIZONTAL) drawWithSwapAnimation(canvas, position, x, y); else drawWithSwapAnimationVertically(canvas, position, x, y); break; } } private void drawWithNoEffect(@NonNull Canvas canvas, int position, int x, int y) { float radius = radiusPx; if (animationType == AnimationType.SCALE) { radius *= scaleFactor; } int color = unselectedColor; if (position == selectedPosition) { color = selectedColor; } Paint paint; if (animationType == AnimationType.FILL) { paint = strokePaint; paint.setStrokeWidth(strokePx); } else { paint = fillPaint; } paint.setColor(color); canvas.drawCircle(x, y, radius, paint); } private void drawWithColorAnimation(@NonNull Canvas canvas, int position, int x, int y) { int color = unselectedColor; if (interactiveAnimation) { if (position == selectingPosition) { color = frameColor; } else if (position == selectedPosition) { color = frameColorReverse; } } else { if (position == selectedPosition) { color = frameColor; } else if (position == lastSelectedPosition) { color = frameColorReverse; } } fillPaint.setColor(color); canvas.drawCircle(x, y, radiusPx, fillPaint); } private void drawWithScaleAnimation(@NonNull Canvas canvas, int position, int x, int y) { int color = unselectedColor; int radius = radiusPx; if (interactiveAnimation) { if (position == selectingPosition) { radius = frameRadiusPx; color = frameColor; } else if (position == selectedPosition) { radius = frameRadiusReversePx; color = frameColorReverse; } } else { if (position == selectedPosition) { radius = frameRadiusPx; color = frameColor; } else if (position == lastSelectedPosition) { radius = frameRadiusReversePx; color = frameColorReverse; } } fillPaint.setColor(color); canvas.drawCircle(x, y, radius, fillPaint); } // TODO private void drawWithSlideAnimation(@NonNull Canvas canvas, int position, int x, int y) { fillPaint.setColor(unselectedColor); canvas.drawCircle(x, y, radiusPx, fillPaint); int from = orientation == Orientation.HORIZONTAL ? frameSlideFrom : x; int to = orientation == Orientation.HORIZONTAL ? y : frameSlideFrom; if (interactiveAnimation && (position == selectingPosition || position == selectedPosition)) { fillPaint.setColor(selectedColor); canvas.drawCircle(from, to, radiusPx, fillPaint); } else if (!interactiveAnimation && (position == selectedPosition || position == lastSelectedPosition)) { fillPaint.setColor(selectedColor); canvas.drawCircle(from, to, radiusPx, fillPaint); } } private void drawWithWormAnimation(@NonNull Canvas canvas, int x, int y) { int radius = radiusPx; if (orientation == Orientation.HORIZONTAL) { rect.left = frameFrom; rect.right = frameTo; rect.top = y - radius; rect.bottom = y + radius; } else { rect.left = x - radiusPx; rect.right = x + radiusPx; rect.top = frameFrom; rect.bottom = frameTo; } fillPaint.setColor(unselectedColor); canvas.drawCircle(x, y, radius, fillPaint); fillPaint.setColor(selectedColor); canvas.drawRoundRect(rect, radiusPx, radiusPx, fillPaint); } private void drawWithFillAnimation(@NonNull Canvas canvas, int position, int x, int y) { int color = unselectedColor; float radius = radiusPx; int stroke = strokePx; if (interactiveAnimation) { if (position == selectingPosition) { color = frameColor; radius = frameRadiusPx; stroke = frameStrokePx; } else if (position == selectedPosition) { color = frameColorReverse; radius = frameRadiusReversePx; stroke = frameStrokeReversePx; } } else { if (position == selectedPosition) { color = frameColor; radius = frameRadiusPx; stroke = frameStrokePx; } else if (position == lastSelectedPosition) { color = frameColorReverse; radius = frameRadiusReversePx; stroke = frameStrokeReversePx; } } strokePaint.setColor(color); strokePaint.setStrokeWidth(strokePx); canvas.drawCircle(x, y, radiusPx, strokePaint); strokePaint.setStrokeWidth(stroke); canvas.drawCircle(x, y, radius, strokePaint); } private void drawWithThinWormAnimation(@NonNull Canvas canvas, int x, int y) { int radius = radiusPx; if (orientation == Orientation.HORIZONTAL) { rect.left = frameFrom; rect.right = frameTo; rect.top = y - (frameHeight / 2); rect.bottom = y + (frameHeight / 2); } else { rect.left = x - (frameHeight / 2); rect.right = x + (frameHeight / 2); rect.top = frameFrom; rect.bottom = frameTo; } fillPaint.setColor(unselectedColor); canvas.drawCircle(x, y, radius, fillPaint); fillPaint.setColor(selectedColor); canvas.drawRoundRect(rect, radiusPx, radiusPx, fillPaint); } private void drawWithDropAnimation(@NonNull Canvas canvas, int x, int y) { fillPaint.setColor(unselectedColor); canvas.drawCircle(x, y, radiusPx, fillPaint); fillPaint.setColor(selectedColor); canvas.drawCircle(frameSlideFrom, frameY, frameRadiusPx, fillPaint); } private void drawWithSwapAnimation(@NonNull Canvas canvas, int position, int x, int y) { fillPaint.setColor(unselectedColor); if (position == selectedPosition) { fillPaint.setColor(selectedColor); canvas.drawCircle(frameSlideFrom, y, radiusPx, fillPaint); } else if (interactiveAnimation && position == selectingPosition) { canvas.drawCircle(x - (frameSlideFrom - getXCoordinate(selectedPosition)), y, radiusPx, fillPaint); } else if (!interactiveAnimation) { canvas.drawCircle(x - (frameSlideFrom - getXCoordinate(selectedPosition)), y, radiusPx, fillPaint); } else { canvas.drawCircle(x, y, radiusPx, fillPaint); } } private void drawWithSwapAnimationVertically(@NonNull Canvas canvas, int position, int x, int y) { fillPaint.setColor(unselectedColor); if (position == selectedPosition) { fillPaint.setColor(selectedColor); canvas.drawCircle(x, frameSlideFrom, radiusPx, fillPaint); } else if (interactiveAnimation && position == selectingPosition) { canvas.drawCircle(x, y - (frameSlideFrom - getYCoordinate(selectedPosition)), radiusPx, fillPaint); } else if (!interactiveAnimation) { canvas.drawCircle(x, y - (frameSlideFrom - getYCoordinate(selectedPosition)), radiusPx, fillPaint); } else { canvas.drawCircle(x, y, radiusPx, fillPaint); } } private void init(@Nullable AttributeSet attrs) { setupId(); initAttributes(attrs); initAnimation(); updateVisibility(); fillPaint.setStyle(Paint.Style.FILL); fillPaint.setAntiAlias(true); strokePaint.setStyle(Paint.Style.STROKE); strokePaint.setAntiAlias(true); strokePaint.setStrokeWidth(strokePx); } private void setupId() { if (getId() == NO_ID) { setId(IdUtils.generateViewId()); } } private void initAttributes(@Nullable AttributeSet attrs) { TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PageIndicatorView, 0, 0); initCountAttribute(typedArray); initColorAttribute(typedArray); initAnimationAttribute(typedArray); initSizeAttribute(typedArray); } private void initCountAttribute(@NonNull TypedArray typedArray) { autoVisibility = typedArray.getBoolean(R.styleable.PageIndicatorView_piv_autoVisibility, true); dynamicCount = typedArray.getBoolean(R.styleable.PageIndicatorView_piv_dynamicCount, false); count = typedArray.getInt(R.styleable.PageIndicatorView_piv_count, COUNT_NOT_SET); if (!isCountSet && count == COUNT_NOT_SET) { isCountSet = true; count = DEFAULT_CIRCLES_COUNT; } int position = typedArray.getInt(R.styleable.PageIndicatorView_piv_select, 0); if (position < 0) { position = 0; } else if (count > 0 && position > count - 1) { position = count - 1; } selectedPosition = position; selectingPosition = position; viewPagerId = typedArray.getResourceId(R.styleable.PageIndicatorView_piv_viewPager, 0); } private void initColorAttribute(@NonNull TypedArray typedArray) { unselectedColor = typedArray.getColor(R.styleable.PageIndicatorView_piv_unselectedColor, Color.parseColor(ColorAnimation.DEFAULT_UNSELECTED_COLOR)); selectedColor = typedArray.getColor(R.styleable.PageIndicatorView_piv_selectedColor, Color.parseColor(ColorAnimation.DEFAULT_SELECTED_COLOR)); } private void initAnimationAttribute(@NonNull TypedArray typedArray) { animationDuration = typedArray.getInt(R.styleable.PageIndicatorView_piv_animationDuration, AbsAnimation.DEFAULT_ANIMATION_TIME); interactiveAnimation = typedArray.getBoolean(R.styleable.PageIndicatorView_piv_interactiveAnimation, false); int animIndex = typedArray.getInt(R.styleable.PageIndicatorView_piv_animationType, AnimationType.NONE.ordinal()); animationType = getAnimationType(animIndex); int rtlIndex = typedArray.getInt(R.styleable.PageIndicatorView_piv_rtl_mode, RtlMode.Off.ordinal()); rtlMode = getRtlMode(rtlIndex); } private void initSizeAttribute(@NonNull TypedArray typedArray) { int orientationIndex = typedArray.getInt(R.styleable.PageIndicatorView_piv_orientation, Orientation.HORIZONTAL.ordinal()); if (orientationIndex == 0) { orientation = Orientation.HORIZONTAL; } else { orientation = Orientation.VERTICAL; } radiusPx = (int) typedArray.getDimension(R.styleable.PageIndicatorView_piv_radius, DensityUtils.dpToPx(DEFAULT_RADIUS_DP)); paddingPx = (int) typedArray.getDimension(R.styleable.PageIndicatorView_piv_padding, DensityUtils.dpToPx(DEFAULT_PADDING_DP)); scaleFactor = typedArray.getFloat(R.styleable.PageIndicatorView_piv_scaleFactor, ScaleAnimation.DEFAULT_SCALE_FACTOR); if (scaleFactor < ScaleAnimation.MIN_SCALE_FACTOR) { scaleFactor = ScaleAnimation.MIN_SCALE_FACTOR; } else if (scaleFactor > ScaleAnimation.MAX_SCALE_FACTOR) { scaleFactor = ScaleAnimation.MAX_SCALE_FACTOR; } strokePx = (int) typedArray.getDimension(R.styleable.PageIndicatorView_piv_strokeWidth, DensityUtils.dpToPx(FillAnimation.DEFAULT_STROKE_DP)); if (strokePx > radiusPx) { strokePx = radiusPx; } if (animationType != AnimationType.FILL) { strokePx = 0; } } private void initAnimation() { animation = new ValueAnimation(new ValueAnimation.UpdateListener() { @Override public void onColorAnimationUpdated(int color, int colorReverse) { frameColor = color; frameColorReverse = colorReverse; invalidate(); } @Override public void onScaleAnimationUpdated(int color, int colorReverse, int radius, int radiusReverse) { frameColor = color; frameColorReverse = colorReverse; frameRadiusPx = radius; frameRadiusReversePx = radiusReverse; invalidate(); } @Override public void onSlideAnimationUpdated(int value) { frameSlideFrom = value; invalidate(); } @Override public void onWormAnimationUpdated(int leftX, int rightX) { // hot poin frameFrom = leftX; frameTo = rightX; invalidate(); } @Override public void onThinWormAnimationUpdated(int leftX, int rightX, int height) { frameFrom = leftX; frameTo = rightX; frameHeight = height; invalidate(); } @Override public void onFillAnimationUpdated(int color, int colorReverse, int radius, int radiusReverse, int stroke, int strokeReverse) { frameColor = color; frameColorReverse = colorReverse; frameRadiusPx = radius; frameRadiusReversePx = radiusReverse; frameStrokePx = stroke; frameStrokeReversePx = strokeReverse; invalidate(); } @Override public void onDropAnimationUpdated(int x, int y, int selectedRadius) { frameSlideFrom = (orientation == Orientation.HORIZONTAL) ? x : y; frameY = (orientation == Orientation.HORIZONTAL) ? y : x; frameRadiusPx = selectedRadius; invalidate(); } @Override public void onSwapAnimationUpdated(int xCoordinate) { frameSlideFrom = xCoordinate; invalidate(); } }); } private AnimationType getAnimationType(int index) { switch (index) { case 0: return AnimationType.NONE; case 1: return AnimationType.COLOR; case 2: return AnimationType.SCALE; case 3: return AnimationType.WORM; case 4: return AnimationType.SLIDE; case 5: return AnimationType.FILL; case 6: return AnimationType.THIN_WORM; case 7: return AnimationType.DROP; case 8: return AnimationType.SWAP; case 9: return AnimationType.DRAG_WORM; } return AnimationType.NONE; } private RtlMode getRtlMode(int index) { switch (index) { case 0: return RtlMode.On; case 1: return RtlMode.Off; case 2: return RtlMode.Auto; } return RtlMode.Auto; } private void resetFrameValues() { isFrameValuesSet = false; setupFrameValues(); } private void setupFrameValues() { if (!isViewMeasured() || isFrameValuesSet) { return; } //color frameColor = selectedColor; frameColorReverse = unselectedColor; //scale frameRadiusPx = radiusPx; frameRadiusReversePx = radiusPx; //worm int xCoordinate = getXCoordinate(selectedPosition); if (xCoordinate - radiusPx >= 0) { frameFrom = xCoordinate - radiusPx; frameTo = xCoordinate + radiusPx; } else { frameFrom = xCoordinate; frameTo = xCoordinate + (radiusPx * 2); } //slide & drop frameSlideFrom = xCoordinate; frameY = getYCoordinate(selectedPosition); //fill frameStrokePx = radiusPx; frameStrokeReversePx = radiusPx / 2; if (animationType == AnimationType.FILL) { frameRadiusPx = radiusPx / 2; frameRadiusReversePx = radiusPx; } //thin worm frameHeight = radiusPx * 2; isFrameValuesSet = true; } private void startColorAnimation() { animation.color().end(); animation.color().with(unselectedColor, selectedColor).duration(animationDuration).start(); } private void startScaleAnimation() { animation.scale().end(); animation.scale().with(unselectedColor, selectedColor, radiusPx, scaleFactor).duration(animationDuration) .start(); } private void startSlideAnimation() { int fromX = getCoordinate(lastSelectedPosition); int toX = getCoordinate(selectedPosition); animation.slide().end(); animation.slide().with(fromX, toX).duration(animationDuration).start(); } private void startWormAnimation() { int from = getCoordinate(lastSelectedPosition); int to = getCoordinate(selectedPosition); boolean isRightSide = selectedPosition > lastSelectedPosition; animation.worm().end(); animation.worm().duration(animationDuration).with(from, to, radiusPx, isRightSide).start(); } private void startFillAnimation() { animation.fill().end(); animation.fill().with(unselectedColor, selectedColor, radiusPx, strokePx).duration(animationDuration) .start(); } private void startThinWormAnimation() { int from = getCoordinate(lastSelectedPosition); int to = getCoordinate(selectedPosition); boolean isRightSide = selectedPosition > lastSelectedPosition; animation.thinWorm().end(); animation.thinWorm().duration(animationDuration).with(from, to, radiusPx, isRightSide).start(); } private void startDropAnimation() { int from = getCoordinate(lastSelectedPosition); int to = getCoordinate(selectedPosition); int center = (orientation == Orientation.HORIZONTAL) ? getYCoordinate(selectedPosition) : getXCoordinate(selectedPosition); animation.drop().end(); animation.drop().duration(animationDuration).with(from, to, center, radiusPx).start(); } private void startSwapAnimation() { int from = getCoordinate(lastSelectedPosition); int to = getCoordinate(selectedPosition); animation.swap().end(); animation.swap().with(from, to).duration(animationDuration).start(); } @Nullable private AbsAnimation setAnimationProgress(float progress) { switch (animationType) { case COLOR: return animation.color().with(unselectedColor, selectedColor).progress(progress); case SCALE: return animation.scale().with(unselectedColor, selectedColor, radiusPx, scaleFactor).progress(progress); case FILL: return animation.fill().with(unselectedColor, selectedColor, radiusPx, strokePx).progress(progress); case DRAG_WORM: case THIN_WORM: case WORM: case SLIDE: case DROP: case SWAP: int from = orientation == Orientation.HORIZONTAL ? getXCoordinate(selectedPosition) : getYCoordinate(selectedPosition); int to = orientation == Orientation.HORIZONTAL ? getXCoordinate(selectingPosition) : getYCoordinate(selectingPosition); if (animationType == AnimationType.SLIDE) { return animation.slide().with(from, to).progress(progress); } else if (animationType == AnimationType.SWAP) { return animation.swap().with(from, to).progress(progress); } else if (animationType == AnimationType.WORM || animationType == AnimationType.THIN_WORM || animationType == AnimationType.DRAG_WORM) { boolean isRightSide = selectingPosition > selectedPosition; if (animationType == AnimationType.WORM) { return animation.worm().with(from, to, radiusPx, isRightSide).progress(progress); } else if (animationType == AnimationType.THIN_WORM) { return animation.thinWorm().with(from, to, radiusPx, isRightSide).progress(progress); } } else { int center = (orientation == Orientation.HORIZONTAL) ? getYCoordinate(selectedPosition) : getXCoordinate(selectedPosition); return animation.drop().with(from, to, center, radiusPx).progress(progress); } } return null; } private void registerSetObserver() { if (setObserver == null && viewPager != null && viewPager.getAdapter() != null) { setObserver = new DataSetObserver() { @Override public void onChanged() { if (viewPager != null && viewPager.getAdapter() != null) { int newCount = viewPager.getAdapter().getCount(); int currItem = viewPager.getCurrentItem(); selectedPosition = currItem; selectingPosition = currItem; lastSelectedPosition = currItem; endAnimation(); setCount(newCount); setProgress(selectingPosition, 1.0f); } } }; try { viewPager.getAdapter().registerDataSetObserver(setObserver); } catch (IllegalStateException e) { e.printStackTrace(); } } } private void updateVisibility() { if (!autoVisibility) { return; } if (count > 1 && getVisibility() != VISIBLE) { setVisibility(VISIBLE); } else if (count <= 1 && getVisibility() != INVISIBLE) { setVisibility(View.INVISIBLE); } } private void endAnimation() { AbsAnimation anim = null; switch (animationType) { case COLOR: anim = animation.color(); break; case SLIDE: anim = animation.slide(); break; case SCALE: anim = animation.scale(); break; case WORM: anim = animation.worm(); break; case THIN_WORM: anim = animation.thinWorm(); break; case FILL: anim = animation.fill(); break; case DROP: anim = animation.drop(); break; case SWAP: anim = animation.swap(); break; } if (anim != null) { anim.end(); } } private void unRegisterSetObserver() { if (setObserver != null && viewPager != null && viewPager.getAdapter() != null) { try { viewPager.getAdapter().unregisterDataSetObserver(setObserver); setObserver = null; } catch (IllegalStateException e) { e.printStackTrace(); } } } private int getViewPagerCount() { if (viewPager != null && viewPager.getAdapter() != null) { return viewPager.getAdapter().getCount(); } else { return count; } } private void findViewPager() { if (viewPagerId == 0) { return; } Context context = getContext(); if (context instanceof Activity) { Activity activity = (Activity) getContext(); View view = activity.findViewById(viewPagerId); if (view != null && view instanceof ViewPager) { setViewPager((ViewPager) view); } } } @SuppressWarnings("UnnecessaryLocalVariable") private int getXCoordinate(int position) { if (orientation == Orientation.HORIZONTAL) { int x = 0; for (int i = 0; i < count; i++) { x += radiusPx + strokePx; if (position == i) { return x; } x += radiusPx + paddingPx; } return x; } else { int x = getWidth() / 2; if (animationType == AnimationType.DROP) { x += radiusPx + strokePx; } return x; } } private int getYCoordinate(int position) { if (orientation == Orientation.HORIZONTAL) { int y = getHeight() / 2; if (animationType == AnimationType.DROP) { y += radiusPx; } return y; } else { int y = 0; for (int i = 0; i < count; i++) { y += radiusPx + strokePx; if (position == i) return y; y += radiusPx + paddingPx; } return y; } } private int getCoordinate(int position) { return orientation == Orientation.HORIZONTAL ? getXCoordinate(position) : getYCoordinate(position); } private Pair<Integer, Float> getProgress(int position, float positionOffset) { if (isRtl()) { position = (count - 1) - position; if (position < 0) { position = 0; } } boolean isRightOverScrolled = position > selectedPosition; boolean isLeftOverScrolled; if (isRtl()) { isLeftOverScrolled = position - 1 < selectedPosition; } else { isLeftOverScrolled = position + 1 < selectedPosition; } if (isRightOverScrolled || isLeftOverScrolled) { selectedPosition = position; } boolean isSlideToRightSide = selectedPosition == position && positionOffset != 0; int selectingPosition; float selectingProgress; if (isSlideToRightSide) { selectingPosition = isRtl() ? position - 1 : position + 1; selectingProgress = positionOffset; } else { selectingPosition = position; selectingProgress = 1 - positionOffset; } if (selectingProgress > 1) { selectingProgress = 1; } else if (selectingProgress < 0) { selectingProgress = 0; } return new Pair<>(selectingPosition, selectingProgress); } private boolean isRtl() { switch (rtlMode) { case On: return true; case Off: return false; case Auto: return TextUtilsCompat.getLayoutDirectionFromLocale( getContext().getResources().getConfiguration().locale) == ViewCompat.LAYOUT_DIRECTION_RTL; } return false; } }