Android自定义轮播图指示器
batmanlf
8年前
<p>轮播图在项目中一般会使用VeiwPager来实现,同时还会关联轮播指示器。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1340681258f51eb8babebc9d055fadc3.gif"></p> <p style="text-align:center">轮播指示器效果gif</p> <p>改造ViewPager的OnPageChangedListener,添加自定义的ViewPager滚动监听器(直接将上一篇文章的代码贴过来了)</p> <pre> <code class="language-java">/** * ViewPager辅助类 */ public class ViewPagerHelper implements ViewPager.OnPageChangeListener { private double mLastPositionOffsetSum; // 上一次滑动总的偏移量 private OnPageScrollListener mOnPageScrollListener; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // 当前总的偏移量 float currentPositionOffsetSum = position + positionOffset; // 上次滑动的总偏移量大于此次滑动的总偏移量,页面从右向左进入(手指从右向左滑动) boolean rightToLeft = mLastPositionOffsetSum <= currentPositionOffsetSum; if (currentPositionOffsetSum == mLastPositionOffsetSum) return; int enterPosition; int leavePosition; float percent; if (rightToLeft) { // 从右向左滑 enterPosition = (positionOffset == 0.0f) ? position : position + 1; leavePosition = enterPosition - 1; percent = (positionOffset == 0.0f) ? 1.0f : positionOffset; } else { // 从左向右滑 enterPosition = position; leavePosition = position + 1; percent = 1 - positionOffset; } if (mOnPageScrollListener != null) { mOnPageScrollListener.onPageScroll(enterPosition, leavePosition, percent); } mLastPositionOffsetSum = currentPositionOffsetSum; } @Override public void onPageSelected(int position) { if (mOnPageScrollListener != null) { mOnPageScrollListener.onPageSelected(position); } } /** * @param state 当前滑动状态 * ViewPager.SCROLL_STATE_IDLE 页面处于闲置、稳定状态,即没被拖动也没惯性滑动 * ViewPager.SCROLL_STATE_DRAGGING 页面正在被用户拖动,即手指正在拖动状态 * Viewpager.SCROLL_STATE_SETTLING 页面处于即将到达最终状态的过程,即手指松开后惯性滑动状态 */ @Override public void onPageScrollStateChanged(int state) { if (mOnPageScrollListener != null) { mOnPageScrollListener.onPageScrollStateChanged(state); } } public void bindScrollListener(ViewPager viewPager, OnPageScrollListener onPageScrollListener) { mOnPageScrollListener = onPageScrollListener; viewPager.addOnPageChangeListener(this); } }</code></pre> <pre> <code class="language-java">/** * ViewPage的页面滚动监听器 */ public interface OnPageScrollListener { /** * 页面滚动时调用 * * @param enterPosition 进入页面的位置 * @param leavePosition 离开的页面的位置 * @param percent 滑动百分比 */ void onPageScroll(int enterPosition, int leavePosition, float percent); /** * 页面选中时调用 * * @param position 选中页面的位置 */ void onPageSelected(int position); /** * 页面滚动状态变化时调用 * * @param state 页面的滚动状态 */ void onPageScrollStateChanged(int state); }</code></pre> <p>根据两种指示器的效果分析,都是通过回调onPageScroll方法中不断变化的enterPositon、leavePosition和percent来实现。</p> <p><strong>NumberIndicator(数字指示器)</strong></p> <p>当ViewPager页面从右向左滑动时,指示器中对应页面的数字从下往上滚动,页面停止,数字停止在中间位置。</p> <p>当ViewPager页面从左向右滑动时,指示器中对应页面的数字从下往上滚动,页面停止,数字停止在中间位置。</p> <p>最终指示器效果可以采用绘制View的方式来实现(当然也可以采用组合控件的方式来实现)。ViewPager页面滑动时,不断重绘View,达到指示数字上下滑动的效果。</p> <pre> <code class="language-java">/** * 数字指示器 */ public class NumberIndicater extends View implements OnPageScrollListener { private Context mContext; private int mCircleColor; private int mCircleSize; private int mNumberColor; private int mNumberSize; private int mCount; private int mCurrent; private Paint mCirclePaint; private Paint mTextPaint; private float offset; // 页面偏移百分比 private boolean isUp; // 指示器数字是否向上滑动 public NumberIndicater(Context context) { this(context, null); } public NumberIndicater(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NumberIndicater(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); initPaint(); } /** * 初始化属性 * * @param context * @param attrs */ private void initAttrs(Context context, AttributeSet attrs) { mContext = context; mCircleSize = dp2px(48f); mCircleColor = 0xfffdd63b; mNumberSize = sp2px(14f); mNumberColor = 0xff353535; // 自定义属性 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NumberIndicater); mCircleColor = ta.getColor(R.styleable.NumberIndicater_circle_color, mCircleColor); mCircleSize = (int) ta.getDimension(R.styleable.NumberIndicater_circle_size, mCircleSize); mNumberColor = ta.getColor(R.styleable.NumberIndicater_number_color, mNumberColor); mNumberSize = (int) ta.getDimension(R.styleable.NumberIndicater_number_size, mNumberSize); ta.recycle(); } private void initPaint() { mCirclePaint = new Paint(); mCirclePaint.setAntiAlias(true); mCirclePaint.setColor(mCircleColor); mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE); mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); mTextPaint.setTextAlign(Paint.Align.LEFT); mTextPaint.setColor(mNumberColor); mTextPaint.setTextSize(mNumberSize); offset = 1; mCount = 3; mCurrent = 1; isUp = true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置测量后的尺寸 setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec)); } private int measure(int measureSpec) { int size = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.EXACTLY: size = specSize; break; case MeasureSpec.AT_MOST: size = mCircleSize; break; } return size; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制圆形底图 canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, mCircleSize / 2f, mCirclePaint); // 绘制分割线 drawSplit(canvas); // 绘制右边总数数字 drawTotleNumber(canvas); // 绘制左边指示数字 drawIndicatNumber(canvas); } private void drawSplit(Canvas canvas) { String text = "/"; float width = mTextPaint.measureText(text); float x = (getWidth() - width) / 2f; Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); float y = getHeight() / 2f + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2f; // x为绘制文本左边缘距离X轴的距离,y为绘制文本基线距离Y轴的位置 canvas.drawText(text, x, y, mTextPaint); } private void drawTotleNumber(Canvas canvas) { String text = String.valueOf(mCount); float x = getWidth() / 2f + mTextPaint.measureText("/") / 2f + 3; Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); float y = getHeight() / 2f + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2f; canvas.drawText(text, x, y, mTextPaint); } private void drawIndicatNumber(Canvas canvas) { mTextPaint.setTextSize(mNumberSize * 1.3f); String text = String.valueOf(mCurrent); Rect rect = new Rect(); // 获取文本的宽度 float width = mTextPaint.measureText(text); // 获取文本的高度 mTextPaint.getTextBounds(text, 0, text.length(), rect); float height = rect.height(); // 文本左边缘距离X轴的距离 float x = getWidth() / 2f - mTextPaint.measureText("/") / 2f - 3 - width; Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); // 文本基线位置距离Y轴的距离 float y = getHeight() / 2f + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2f; if (isUp) { // 指示数字向上滑动 y = offset * y + (1 - offset) * (getHeight() / 2f - mCircleSize / 2f + mCircleSize + height); } else { // 指示数字向下滑动 y = offset * y + (1 - offset) * (getHeight() / 2f - mCircleSize / 2f); } canvas.drawText(text, x, y, mTextPaint); mTextPaint.setTextSize(mNumberSize); } /** * 将指示器绑定到ViewPager * * @param viewPager view pager */ public void bindViewPager(ViewPager viewPager) { if (viewPager != null && viewPager.getAdapter() != null) { mCount = viewPager.getAdapter().getCount(); new ViewPagerHelper().bindScrollListener(viewPager, this); invalidate(); // 绑定ViewPager后指示器重绘,因为指示器的数字与初始的可能不同 } } @Override public void onPageScroll(int enterPosition, int leavePosition, float percent) { mCurrent = enterPosition + 1; offset = percent; isUp = enterPosition > leavePosition; postInvalidate(); // 滑动过程中不断重绘 } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { } private int dp2px(float dpValue) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, mContext.getResources().getDisplayMetrics()); } private int sp2px(float dpValue) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dpValue, mContext.getResources().getDisplayMetrics()); } }</code></pre> <p><strong>PointIndicator(圆点指示器)</strong></p> <p>根据ViewPager滑动的位置和百分比,动态绘制指示小圆点。通过Viewpger滑动过程中的enterPosition和leavePosition以及滑动百分比percent来计算出滑动小圆点的左边位置即可。</p> <pre> <code class="language-java">/** * 圆点指示器 */ public class PointIndicator extends View implements OnPageScrollListener { private Context mContext; private int mNormalColor; private int mSelectColor; private int mPointSize; private int mPointSpace; private Paint mNormalPaint; private Paint mSelectPaint; private int mCount; private int enterPosition; private int leavePosition; private float percent; public PointIndicator(Context context) { this(context, null); } public PointIndicator(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PointIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); initPaint(); } private void initPaint() { mNormalPaint = new Paint(); mNormalPaint.setColor(mNormalColor); mNormalPaint.setAntiAlias(true); mSelectPaint = new Paint(); mSelectPaint.setColor(mSelectColor); mSelectPaint.setAntiAlias(true); mCount = 4; } private void initAttrs(Context context, AttributeSet attrs) { mContext = context; mNormalColor = 0x66cccccc; mSelectColor = 0xfffdd63b; mPointSize = dp2px(3f); mPointSpace = dp2px(3f); // 自定义属性 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PointIndicator); mNormalColor = ta.getColor(R.styleable.PointIndicator_normal_color, mNormalColor); mSelectColor = ta.getColor(R.styleable.PointIndicator_select_color, mSelectColor); mPointSize = (int) ta.getDimension(R.styleable.PointIndicator_point_size, mPointSize); mPointSpace = (int) ta.getDimension(R.styleable.PointIndicator_point_space, mPointSpace); ta.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } private int measureWidth(int measureSpec) { int size = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.EXACTLY: size = specSize; break; case MeasureSpec.AT_MOST: size = mCount * mPointSize + (mCount - 1) * mPointSpace; break; } return size; } private int measureHeight(int measureSpec) { int size = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.EXACTLY: size = specSize; break; case MeasureSpec.AT_MOST: size = mPointSize; break; } return size; } @Override protected void onDraw(Canvas canvas) { // 绘制normalPoint drawNormalPoint(canvas); // 绘制selectPoint drawSelectPoint(canvas); } private void drawSelectPoint(Canvas canvas) { float cx; if (enterPosition > leavePosition) { cx = (leavePosition + 0.5f) * mPointSize + leavePosition * mPointSpace + (mPointSize + mPointSpace) * percent; } else { cx = (leavePosition + 0.5f) * mPointSize + leavePosition * mPointSpace - (mPointSize + mPointSpace) * percent; } float cy = getHeight() / 2; float radius = mPointSize / 2f; canvas.drawCircle(cx, cy, radius, mSelectPaint); } private void drawNormalPoint(Canvas canvas) { for (int i = 0; i < mCount; i++) { float cx = mPointSize / 2f + (mPointSize + mPointSpace) * i; float cy = getHeight() / 2; float radius = mPointSize / 2f; canvas.drawCircle(cx, cy, radius, mNormalPaint); } } public void bindViewPager(ViewPager viewPager) { if (viewPager != null) { if (viewPager.getAdapter() != null) { mCount = viewPager.getAdapter().getCount(); new ViewPagerHelper().bindScrollListener(viewPager, this); requestLayout(); // 绑定ViewPager后指示器重新布局,因为指示器的数量和宽度可能有变化 } } } @Override public void onPageScroll(int enterPosition, int leavePosition, float percent) { this.enterPosition = enterPosition; this.leavePosition = leavePosition; this.percent = percent; postInvalidate(); } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { } private int dp2px(float dpValue) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, mContext.getResources().getDisplayMetrics()); } }</code></pre> <p>涉及到的自定义属性:</p> <pre> <code class="language-java"><?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="NumberIndicater"> <attr name="circle_color" format="color"/> <attr name="circle_size" format="dimension"/> <attr name="number_size" format="dimension"/> <attr name="number_color" format="color"/> </declare-styleable> <declare-styleable name="PointIndicator"> <attr name="point_size" format="dimension"/> <attr name="point_space" format="dimension"/> <attr name="normal_color" format="color"/> <attr name="select_color" format="color"/> </declare-styleable> </resources></code></pre> <p>以上两种指示器都是通过自绘View的方式来实现。通过自定义的OnPageScrollListener还可以实现更多效果炫酷的指示器。</p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/60be684d897d</p> <p> </p>