精华内容
下载资源
问答
  • Android 自定义控件之圆形页面指示器CirclePageIndicator带划动效果前言感谢效果图目标流程自定义属性自定义默认属性自定义接口创建控件类继承View声明属性变量初始化属性信息测量布局绘制布局记录变化信息复写...

    前言

    在app首次打开的指导页面和app内一些左右翻动的列表里经常会需要的一个页面指示器,像一般手机桌面也会有一个翻页的页面指示器,这次我们就来定制一个。
    页面指示器

    感谢

    基本的逻辑来自JakeWharton/ViewPagerIndicator
    他里面包含很多的样式,是一个很好的学习资料
    在这里插入图片描述
    不过他是纯英文的,也不维护了。但是很多人都还有在用,我也经常用在项目上,但是只会用是没办法解决心里头的疑惑。很想把它的源码拆出来瞧瞧,学习一下思维也是很棒的,这对于我们学习自定义控件有着非常大帮助,毕竟已经维护这么久了,也有很多人在用,全面性还是足够的。下面我就挑了圆形指示器进行解析和翻译。学习自定义控件的朋友应该能从中收获不少。

    效果图

    下图为GIF动图,点击查看
    圆形页面指示器

    目标

    定下要实现的功能

    1. 可以修改圆形指示器的大小
    2. 可以修改圆形指示器当前页的颜色
    3. 可以修改圆形指示器所有页面的颜色
    4. 可以修改圆形指示器所有页面的边框大小
    5. 可以修改圆形指示器所有页面的边框颜色
    6. 可以修改圆形指示器是否居中显示
    7. 可以修改圆形指示器是否支持拖动跟随效果

    流程

    做了这么多次自定义控件了,流程都应该熟读于心,这样子对于我们去看别人的自定义控件代码也更加的清晰,不会觉得一脸茫然,知道哪一步再到哪一步是非常重要的。

    1. 自定义属性
    2. 自定义默认属性
    3. 自定义接口
    4. 创建控件类继承View
    5. 声明属性变量
    6. 初始化属性信息
    7. 测量布局
    8. 绘制布局
    9. 记录变化信息
    10. 复写自定义接口

    以上的黄色部分内容是自定义控件里最常见的,也是一般必定会有的,当我们查看别人的自定义控件时,一般也按照这个流程来整理。

    自定义属性

    文件路径 /res/values/attrs.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="CirclePageIndicator">
            <!-- Whether or not the indicators should be centered. -->
            <!-- 指示器是否居中显示 -->
            <attr name="centered" format="boolean" />
            <!-- Color of the filled circle that represents the current page. -->
            <!-- 当前页面指示器颜色 -->
            <attr name="selectColor" format="color"/>
            <!-- Color of the filled circles that represents pages. -->
            <!-- 全部页面指示器颜色 -->
            <attr name="pageColor" format="color"/>
            <!-- Radius of the circles. This is also the spacing between circles. -->
            <!-- 指示器半径 -->
            <attr name="radius" format="dimension"/>
            <!-- Whether or not the selected indicator snaps to the circles. -->
            <!-- 指示器是否锁定位置 -->
            <attr name="snap" format="boolean"/>
            <!-- Color of the open circles. -->
            <!-- 全部页面指示器空心圆边框颜色 -->
            <attr name="strokeColor" format="color"/>
            <!-- Width of the stroke used to draw the circles. -->
            <!-- 全部页面指示器空心圆边框宽度 -->
            <attr name="strokeWidth" format="dimension" />
            <!-- View background -->
            <!-- 页面指示器背景 -->
            <attr name="android:background"/>
        </declare-styleable>
    </resources>
    

    自定义默认属性

    文件路径 /res/values/defaults.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <!-- 默认资源集 -->
        <!-- 默认指示器是否位于中间 -->
        <bool name="default_circle_indicator_centered">true</bool>
        <!-- 默认指示器全部页面实心圆颜色 -->
        <color name="default_circle_indicator_page_color">#00000000</color>
        <!-- 默认指示器当前页面实心圆颜色 -->
        <color name="default_circle_indicator_select_color">#FFFFFFFF</color>
        <!-- 默认指示器全部页面空心圆边框颜色 -->
        <color name="default_circle_indicator_stroke_color">#FFDDDDDD</color>
        <!-- 默认指示器全部页面空心圆边框宽度 -->
        <dimen name="default_circle_indicator_stroke_width">1dp</dimen>
        <!-- 默认指示器圆形半径 -->
        <dimen name="default_circle_indicator_radius">4dp</dimen>
        <!-- 默认指示器是否锁定在位置上,设置为false的话,指示器就会跟着手指划动而移动 -->
        <bool name="default_circle_indicator_snap">false</bool>
    </resources>
    

    自定义接口

    新建文件 PageIndicator.class 继承页面改变监听器,这样我们除了自己自定义的接口外也能复写页面改变事件的接口

    /**
     * 接口
     * A PageIndicator is responsible to show an visual indicator on the total views
     * number and the current visible view.
     */
    public interface PageIndicator extends ViewPager.OnPageChangeListener {
        /**
         * Bind the indicator to a ViewPager.
         * 绑定指示器和viewpager
         * @param view
         */
        void setViewPager(ViewPager view);
    
        /**
         * Bind the indicator to a ViewPager.
         * 绑定指示器和viewpager
         * @param view
         * @param initialPosition
         */
        void setViewPager(ViewPager view, int initialPosition);
    
        /**
         * <p>Set the current page of both the ViewPager and indicator.</p>
         * <p/>
         * <p>This <strong>must</strong> be used if you need to set the page before
         * the views are drawn on screen (e.g., default start page).</p>
         * 跳到当前选项页面
         * @param item
         */
        void setCurrentItem(int item);
    
        /**
         * Set a page change listener which will receive forwarded events.
         * 设置监听器
         * @param listener
         */
        void setOnPageChangeListener(ViewPager.OnPageChangeListener listener);
    
        /**
         * Notify the indicator that the fragment list has changed.
         * 更新
         */
        void notifyDataSetChanged();
    }
    

    创建控件类继承View

    /**
     * Draws circles (one for each view). The current view position is filled and
     * others are only stroked.
     * 页面指示器
     */
    public class CirclePageIndicator extends View implements PageIndicator {
        public CirclePageIndicator(Context context) {
            super(context);
        }
    
        public CirclePageIndicator(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public void setViewPager(ViewPager view) {
    
        }
    
        @Override
        public void setViewPager(ViewPager view, int initialPosition) {
    
        }
    
        @Override
        public void setCurrentItem(int item) {
    
        }
    
        @Override
        public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
    
        }
    
        @Override
        public void notifyDataSetChanged() {
    
        }
    
        @Override
        public void onPageScrolled(int i, float v, int i1) {
    
        }
    
        @Override
        public void onPageSelected(int i) {
    
        }
    
        @Override
        public void onPageScrollStateChanged(int i) {
    
        }
    }
    

    声明属性变量

        /** 无效手指指示 */
        private static final int INVALID_POINTER = -1;
    
        /** 上下文 */
        private Context mContext;
    
        /** 圆形指示器半径 */
        private float mRadius;
        /** 所有页面圆形指示器实心圆画笔 */
        private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG);
        /** 所有页面圆形指示器边框空心圆画笔 */
        private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG);
        /** 当前页面圆形指示器实心圆画笔 */
        private final Paint mPaintSelect = new Paint(ANTI_ALIAS_FLAG);
        /** 页面管理控件 */
        private ViewPager mViewPager;
        /** 页面改变监听器 */
        private ViewPager.OnPageChangeListener mListener;
        /** 当前页面号码 */
        private int mCurrentPage;
        /** 保存页面号码 */
        private int mSnapPage;
        /** 页面划动时的偏移距离 */
        private float mPageOffset;
        /** 页面划动条状态 */
        private int mScrollState;
        /** 指示器是否居中显示 */
        private boolean mCentered;
        /** 指示器是否锁定在位置上,设置为false的话,指示器就会跟着手指划动而移动 */
        private boolean mSnap;
        /** 控件最小可触发响应的划动距离 */
        private int mTouchSlop;
        /** 手指按下位置 */
        private float mLastMotionX = -1;
        /** 响应的手指ID */
        private int mActivePointerId = INVALID_POINTER;
        /** 手指是否在拖动中 */
        private boolean mIsDragging;
    

    初始化属性信息

        public CirclePageIndicator(Context context) {
            super(context);
            mContext = context;
            init(null);
        }
    
        public CirclePageIndicator(Context context, AttributeSet attrs){
            super(context, attrs);
            mContext = context;
            init(attrs);
        }
    
        /**
         * 初始化
         * @param attrs
         */
        private void init(AttributeSet attrs) {
            // Load defaults from resources
            // 加载默认资源
            final Resources res = getResources();
            // 默认当前页面指示器实心圆颜色
            final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color);
            // 默认全部页面指示器实心圆颜色
            final int defaultFillColor = res.getColor(R.color.default_circle_indicator_select_color);
            // 默认全部页面指示器空心圆边框颜色
            final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);
            // 默认全部页面指示器空心圆边框宽度
            final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);
            // 默认指示器圆形半径
            final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius);
            // 默认指示器是否居中
            final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered);
            // 默认指示器是否锁定在位置上
            final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap);
            // Retrieve styles attributes
            // 读取配置信息
            TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator);
            mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered);
            mPaintPageFill.setStyle(Style.FILL);
            mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor));
            mPaintStroke.setStyle(Style.STROKE);
            mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor));
            mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth));
            mPaintSelect.setStyle(Style.FILL);
            mPaintSelect.setColor(a.getColor(R.styleable.CirclePageIndicator_selectColor, defaultFillColor));
            mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius);
            mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap);
            Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background);
            if (null != background) {
                setBackgroundDrawable(background);
            }
            // 释放内存,回收资源
            a.recycle();
            // 拿到控件的最小划动距离
            final ViewConfiguration configuration = ViewConfiguration.get(mContext);
            mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        }
    

    测量布局

        /**
         * 测量
         * @param widthMeasureSpec
         * @param heightMeasureSpec
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // 保存测量数据
            setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));
        }
    
        /**
         * Determines the width of this view
         *
         * @param measureSpec A measureSpec packed into an int
         * @return The width of the view, honoring constraints from measureSpec
         */
        private int measureLong(int measureSpec) {
            int result;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
                // We were told how big to be
                // 给定数据
                result = specSize;
            } else {
                // Calculate the width according the views count
                // 根据页面数量来计算宽度,最后加1为了保证后面绘制时数据计算成float也能有数据冗余
                final int count = mViewPager.getAdapter().getCount();
                result = (int) (getPaddingLeft() + getPaddingRight()
                        + (count * 2 * mRadius) + (count - 1) * mRadius + 1);
                //Respect AT_MOST value if that was what is called for by measureSpec
                if (specMode == MeasureSpec.AT_MOST) {
                    result = Math.min(result, specSize);
                }
            }
            return result;
        }
    
        /**
         * Determines the height of this view
         *
         * @param measureSpec A measureSpec packed into an int
         * @return The height of the view, honoring constraints from measureSpec
         */
        private int measureShort(int measureSpec) {
            int result;
            int specMode = MeasureSpec.getMode(measureSpec);
            int specSize = MeasureSpec.getSize(measureSpec);
    
            if (specMode == MeasureSpec.EXACTLY) {
                // We were told how big to be
                // 给定数据
                result = specSize;
            } else {
                // Measure the height
                result = (int) (2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
                //Respect AT_MOST value if that was what is called for by measureSpec
                if (specMode == MeasureSpec.AT_MOST) {
                    result = Math.min(result, specSize);
                }
            }
            return result;
        }
    

    绘制布局

        /**
         * 绘制
         * @param canvas
         */
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            // 绑定的页面管理控件不能为空
            if (mViewPager == null) {
                return;
            }
            // 绑定的页面数量也不能为0
            final int count = mViewPager.getAdapter().getCount();
            if (count == 0) {
                return;
            }
            // 如果当前页的页码大于全部页面数量,就要修改当前页为最后一页
            if (mCurrentPage >= count) {
                setCurrentItem(count - 1);
                return;
            }
            // 声明变量
            int longSize = getWidth();  // 整体长度
            int longPaddingBefore = getPaddingLeft();  // 距离前面的长度
            int longPaddingAfter = getPaddingRight();  // 距离后面的长度
            int shortPaddingBefore = getPaddingTop(); // 距离上面的长度
            // 一个指示器占据的位置,一个圆形再加上左边边界半个圆和右边边界半个圆
            final float fourRadius = mRadius * 4;
            // 短边偏移量,举例:横着排列,那就是顶部距离到指示器圆中心点
            final float shortOffset = shortPaddingBefore + mRadius;
            // 长边偏移量,举例:横着排列,那就是左边距离到指示器圆中心点
            float longOffset = longPaddingBefore + fourRadius / 2.0f;
            if (mCentered) {
                // 如果指示器居中显示,计算中间开始位置
                longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * fourRadius) / 2.0f);
            }
            // 圆心X轴
            float dX;
            // 圆心Y轴
            float dY;
            // 全部页面指示器实心圆半径
            float pageFillRadius = mRadius;
            if (mPaintStroke.getStrokeWidth() > 0) {
                // 全部页面指示器实心圆半径 为 设定圆半径减去空心圆宽度的一半
                pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;
            }
            // 循环绘制全部页面指示器
            for (int iLoop = 0; iLoop < count; iLoop++) {
                // 记录XY轴位置
                dX = longOffset + (iLoop * fourRadius);
                dY = shortOffset;
                // Only paint fill if not completely transparent
                if (mPaintPageFill.getAlpha() > 0) {
                    // 绘制全部页面指示器实心圆
                    canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);
                }
                // Only paint stroke if a stroke width was non-zero
                if (pageFillRadius != mRadius) {
                    // 绘制全部页面指示器空心圆外框
                    canvas.drawCircle(dX, dY, mRadius, mPaintStroke);
                }
            }
            // Draw the filled circle according to the current scroll
            // 绘制当前页面页面指示器
            float cx = (mSnap ? mSnapPage : mCurrentPage) * fourRadius;
            if (!mSnap) {
                // 如果不锁定位置,那么就要在划动时记录页面间偏移量,然后在这里加上
                cx += mPageOffset * fourRadius;
            }
            // 记录XY轴位置
            dX = longOffset + cx;
            dY = shortOffset;
            // 绘制当前页面实心圆
            canvas.drawCircle(dX, dY, mRadius, mPaintSelect);
        }
    

    记录变化信息

        /**
         * 拦截触控事件
         * @param ev
         * @return
         */
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (super.onTouchEvent(ev)) {
                return true;
            }
            // 绑定的页面管理控件不能为空并且页面数量不能为0
            if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) {
                return false;
            }
            // 拿到当前动作
            final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    // 手指按下时
                    // 拿到手指ID
                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                    // 将放下手指的位置记录下来
                    mLastMotionX = ev.getX();
                    break;
                case MotionEvent.ACTION_MOVE: {
                    // 手指在屏幕上移动时
                    // 根据手指按下时记录的手指ID去获得活动手指的Index
                    final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    // 拿到这个手指当前所在的X轴位置
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    // 计算移动的距离
                    final float deltaX = x - mLastMotionX;
                    // 如果上一次记录的没在移动中
                    if (!mIsDragging) {
                        // 那么这一次就要看移动距离的绝对值(Math.abs)有没有大过最小识别的移动距离
                        if (Math.abs(deltaX) > mTouchSlop) {
                            // 有的话,就记录为移动中
                            mIsDragging = true;
                        }
                    }
                    // 如果在移动中
                    if (mIsDragging) {
                        // 记录手指当前位置
                        mLastMotionX = x;
                        // 如果是虚假的移动
                        if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) {
                            // 使用fakeDragBy将页面偏移手指移动的距离
                            mViewPager.fakeDragBy(deltaX);
                        }
                    }
                    break;
                }
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    // 手指离开屏幕时
                    // 上一个状态不是移动中
                    if (!mIsDragging) {
                        final int count = mViewPager.getAdapter().getCount();
                        final int width = getWidth();
                        final float halfWidth = width / 2f;
                        final float sixthWidth = width / 6f;
    
                        if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {
                            // 当前页面不是第一页、上一个状态是划动中、最后手指的位置在屏幕的左边1/3的位置内,就当作要往后翻一页
                            if (action != MotionEvent.ACTION_CANCEL) {
                                mViewPager.setCurrentItem(mCurrentPage - 1);
                            }
                            return true;
                            // 当前页面不是最后一页、上一个状态是划动中、最后手指的位置在屏幕的右边1/3的位置内,就当作要往前翻一页
                        } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {
                            if (action != MotionEvent.ACTION_CANCEL) {
                                mViewPager.setCurrentItem(mCurrentPage + 1);
                            }
                            return true;
                        }
                    }
                    // 移动状态归否
                    mIsDragging = false;
                    // 手指头ID清空
                    mActivePointerId = INVALID_POINTER;
                    // 结束页面管理器的假动作
                    if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();
                    break;
                case MotionEventCompat.ACTION_POINTER_DOWN: {
                    final int index = MotionEventCompat.getActionIndex(ev);
                    mLastMotionX = MotionEventCompat.getX(ev, index);
                    mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                    break;
                }
                case MotionEventCompat.ACTION_POINTER_UP:
                    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    if (pointerId == mActivePointerId) {
                        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                        mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
                    }
                    mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
                    break;
            }
    
            return true;
        }
    
        /**
         * 复现
         * @param state
         */
        @Override
        public void onRestoreInstanceState(Parcelable state) {
            SavedState savedState = (SavedState) state;
            super.onRestoreInstanceState(savedState.getSuperState());
            mCurrentPage = savedState.currentPage;
            mSnapPage = savedState.currentPage;
            requestLayout();
        }
    
        /**
         * 保存
         * @return
         */
        @Override
        public Parcelable onSaveInstanceState() {
            Parcelable superState = super.onSaveInstanceState();
            SavedState savedState = new SavedState(superState);
            savedState.currentPage = mCurrentPage;
            return savedState;
        }
    
        /**
         * 保存状态类
         */
        static class SavedState extends BaseSavedState {
            int currentPage;
    
            public SavedState(Parcelable superState) {
                super(superState);
            }
    
            private SavedState(Parcel in) {
                super(in);
                currentPage = in.readInt();
            }
    
            @Override
            public void writeToParcel(Parcel dest, int flags) {
                super.writeToParcel(dest, flags);
                dest.writeInt(currentPage);
            }
    
            @SuppressWarnings("UnusedDeclaration")
            public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
                @Override
                public SavedState createFromParcel(Parcel in) {
                    return new SavedState(in);
                }
    
                @Override
                public SavedState[] newArray(int size) {
                    return new SavedState[size];
                }
            };
        }
    

    复写自定义接口

        /**
         * 设置页面管理器
         * @param view
         */
        @Override
        public void setViewPager(ViewPager view) {
            if (mViewPager == view) {
                return;
            }
            if (mViewPager != null) {
                mViewPager.setOnPageChangeListener(null);
            }
            if (view.getAdapter() == null) {
                throw new IllegalStateException("ViewPager does not have adapter instance.");
            }
            mViewPager = view;
            mViewPager.setOnPageChangeListener(this);
            invalidate();
        }
    
        /**
         * 设置页面管理器,同时定义了当前页面
         * @param view
         * @param initialPosition
         */
        @Override
        public void setViewPager(ViewPager view, int initialPosition) {
            setViewPager(view);
            setCurrentItem(initialPosition);
        }
    
        /**
         * 设置当前页面
         * @param item
         */
        @Override
        public void setCurrentItem(int item) {
            if (mViewPager == null) {
                throw new IllegalStateException("ViewPager has not been bound.");
            }
            mViewPager.setCurrentItem(item);
            mCurrentPage = item;
            invalidate();
        }
    
        /**
         * 设置页面改变监听器
         * @param listener
         */
        @Override
        public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
            mListener = listener;
        }
    
        /**
         * 请求重新加载
         */
        @Override
        public void notifyDataSetChanged() {
            invalidate();
        }
    
        /**
         * 页面滚动时
         * @param i  位置
         * @param v  偏移距离
         * @param i1 偏移像素距离
         */
        @Override
        public void onPageScrolled(int i, float v, int i1) {
            mCurrentPage = i;
            mPageOffset = v;
            invalidate();
    
            if (mListener != null) {
                mListener.onPageScrolled(i, v, i1);
            }
        }
    
        /**
         * 页面选择
         * @param i  位置
         */
        @Override
        public void onPageSelected(int i) {
            if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) {
                mCurrentPage = i;
                mSnapPage = i;
                invalidate();
            }
    
            if (mListener != null) {
                mListener.onPageSelected(i);
            }
        }
    
        /**
         * 页面划动状态改变
         * @param i  状态
         */
        @Override
        public void onPageScrollStateChanged(int i) {
            mScrollState = i;
    
            if (mListener != null) {
                mListener.onPageScrollStateChanged(i);
            }
        }
    

    使用

    在布局文件中添加:

    <com.dlong.rep.dlflipviewpage.indicator.CirclePageIndicator
                android:id="@+id/indicator"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="bottom"
                android:paddingBottom="10dp"
                app:pageColor="@color/colorAccent"
                app:radius="6dp"
                app:selectColor="@color/colorPrimary"
                app:strokeWidth="0dp" />
    

    在活动中使用

    private ViewPager view_pager;
    private CirclePageIndicator indicator;
    

    获得控件,指示器要和ViewPager一起使用

    private void initview() {
            view_pager = (ViewPager) findViewById(R.id.viewPager);
            indicator = (CirclePageIndicator) findViewById(R.id.indicator);
        }
    
    indicator.setViewPager(view_pager);
    

    下一篇介绍ViewPager+GridView的组合使用。

    展开全文
  • 根据慕课网同标题课程 写的一个demo,, 网址:http://www.imooc.com/learn/615,, 有兴趣可以看一下
  • Android自定义View之超简单圆形数字指示器

    MIUI8内置的天气App的一种效果
    images

    不过好像没有看到有其他的什么动画效果,也有可能是在4.7屏下这个Layout在其他Layout的下面,加载完数据有效果看不到?
    不纠结这个,心血来潮重新自定义写个就是了。

    效果图:
    images

    自定义View的流程不再这里过多叙述,直接上代码。

        //(灰)外圈画笔
        private Paint outPaint = new Paint();
        //(绿)内圈画笔
        private Paint inPaint = new Paint();
        //文字画笔
        private Paint middleTextPaint = new Paint();
        //外切矩形
        private RectF mRectF;
    
        //圆初始弧度
        private float startAngle = 135;
        //圆结束弧度
        private float sweepAngle = 270;
    
        int mCenter = 0;
        int mRadius = 0;
    
        private int indexValue;
        private String middleText = "N/A";
        private String bottomText = "";
        private int circleWidth;
        private int circleHeight;
        private float inSweepAngle;
    
        private float numberTextSize;
        private float middleTextSize;
        private float bottomTextSize;
    
        private int outCircleColor;
        private int inCircleColor;
        private int numberColor;
        private int middleTextColor;
        private int bottomTextColor;

    为了兼容Android LOLLIPOP+,需要重写View的构造函数,在构造函数中初始化画笔和自定义属性。

        public CircleIndexView(Context context)
        {
            this(context, null);
        }
    
        public CircleIndexView(Context context, AttributeSet attrs)
        {
            this(context, attrs, -1);
        }
    
        public CircleIndexView(Context context, AttributeSet attrs, int defStyleAttr)
        {
            super(context, attrs, defStyleAttr);
            initParams(context, attrs);
            initPaint();
        }
    
    
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        public CircleIndexView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
        {
            super(context, attrs, defStyleAttr, defStyleRes);
            initParams(context, attrs);
            initPaint();
        }
        /**
         * 初始化View参数
         *
         * @param context
         * @param attrs
         */
        private void initParams(Context context, AttributeSet attrs)
        {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.circleindexview);
            middleText = ta.getString(R.styleable.circleindexview_middleText);
            bottomText = ta.getString(R.styleable.circleindexview_bottomText);
            numberTextSize = ta.getDimension(R.styleable.circleindexview_numberTextSize,
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, context.getResources().getDisplayMetrics()));
            middleTextSize = ta.getDimension(R.styleable.circleindexview_middleTextSize,
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, context.getResources().getDisplayMetrics()));
            bottomTextSize = ta.getDimension(R.styleable.circleindexview_bottomTextSize,
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, context.getResources().getDisplayMetrics()));
            outCircleColor = ta.getColor(R.styleable.circleindexview_outCircleColor, Color.LTGRAY);
            inCircleColor = ta.getColor(R.styleable.circleindexview_inCircleColor, Color.GREEN);
            numberColor = ta.getColor(R.styleable.circleindexview_numberColor, Color.GREEN);
            middleTextColor = ta.getColor(R.styleable.circleindexview_middleTextColor, Color.LTGRAY);
            bottomTextColor = ta.getColor(R.styleable.circleindexview_bottomTextColor, Color.LTGRAY);
            ta.recycle();
        }

    在初始化参数的时候,对于sp/dp的值需要通过TypeValue进行转换。

    ta.getDimension(R.styleable.circleindexview_bottomTextSize,TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, context.getResources().getDisplayMetrics()));

    TypedValue.COMPLEX_UNIT_SP对应的是spTypedValue.COMPLEX_UNIT_DIP对应的是dp
    当然,除了getDimension()之外,还有getDimensionPixelOffset(),getDimensionPixelSize(),各自都有区别,感兴趣可以查查Api。

    测量View,在这里,为了让整个View比较好看,刻意设置了layout_width和layout_height无论是match_parent或者wrap_content,都是200dp的默认值。(measureWidth()和measureHeight()代码基本相同)

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
        {
            setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
        }
    
        private int measureWidth(int widthMeasureSpec)
        {
            int result = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
            int specMode = MeasureSpec.getMode(widthMeasureSpec);
            int specSize = MeasureSpec.getSize(widthMeasureSpec);
            switch (specMode)
            {
                case MeasureSpec.EXACTLY:
                    result = specSize;
                    break;
                case MeasureSpec.UNSPECIFIED:
                    break;
                case MeasureSpec.AT_MOST:
                    break;
            }
            setCircleWidth(result);
            return result;
        }

    最后,是将整个view绘制出来。代码有注释,就不多赘述。

    
        @Override
        protected void onDraw(Canvas canvas)
        {
            super.onDraw(canvas);
    
            mCenter = getCircleWidth() / 2;
            mRadius = getCircleWidth() / 2 - 50;
    
            mRectF = new RectF(mCenter - mRadius, mCenter - mRadius, mCenter
                    + mRadius, mCenter + mRadius);
    
            //外圆圈
            canvas.drawArc(mRectF, startAngle, sweepAngle, false, outPaint);
            //内圆圈
            canvas.drawArc(mRectF, startAngle, getInSweepAngle(), false, inPaint);
    
            //中心数字
            middleTextPaint.setColor(getNumberColor());
            middleTextPaint.setTextSize(getNumberTextSize());
            canvas.drawText(getIndexValue() + "", getCircleWidth() / 2, getCircleHeight() / 2, middleTextPaint);
    
            //中心文字(etc. Pm25)
            middleTextPaint.setColor(getMiddleTextColor());
            middleTextPaint.setTextSize(getMiddleTextSize());
            canvas.drawText(getMiddleText(), getCircleWidth() / 2, getCircleHeight() / 2 + 40, middleTextPaint);
    
            //底部文字(etc. 空气污染指数)
            middleTextPaint.setColor(getBottomTextColor());
            middleTextPaint.setTextSize(getBottomTextSize());
            canvas.drawText(getBottomText(), getCircleWidth() / 2, getCircleHeight() - 50, middleTextPaint);
    
        }

    暴露公共方法给用户更新数据,同时实现动画效果,这里用到了属性动画。

        public void updateIndex(int value)
        {
            float inSweepAngle = sweepAngle * value / 100;
            ValueAnimator angleAnim = ValueAnimator.ofFloat(0f, inSweepAngle);
            ValueAnimator valueAnim = ValueAnimator.ofInt(0, value);
            angleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
            {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator)
                {
                    float currentValue = (float) valueAnimator.getAnimatedValue();
                    setInSweepAngle(currentValue);
                    invalidate();
                }
            });
            valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
            {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator)
                {
                    int currentValue = (int) valueAnimator.getAnimatedValue();
                    setIndexValue(currentValue);
                    invalidate();
                }
            });
            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.setInterpolator(new DecelerateInterpolator());
            animatorSet.setStartDelay(150);
            animatorSet.playTogether(angleAnim, valueAnim);
            animatorSet.start();
        }

    项目开源,已上传到jcenter,具体使用见gaoyuyu/CircleIndexView

    欢迎各位star,fork,在使用过程中有任何BUG,欢迎大家留言,共同学习共同进步~!


    相关推荐链接:
    自定义View教程:安卓自定义View教程目录

    展开全文
  • 圆形指示器 public class CircleIndicatorView extends View implements ViewPager.OnPageChangeListener{ private static final String LETTER[] = new String[]{"A","B","C","D","E","F","G","H","I","G",...

    添加命名空间

    xmlns:qishui="http://schemas.android.com/apk/res-auto"

    使用控件布局

    <com.example.zhou.tabpageindicator.view.ViewPagerIndicator
        android:id="@+id/id_indicator"
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:background="@color/colorPrimary"
        android:orientation="horizontal"
        qishui:item_count="5">
    </com.example.zhou.tabpageindicator.view.ViewPagerIndicator>
    <android.support.v4.view.ViewPager
        android:id="@+id/id_vp"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
    </android.support.v4.view.ViewPager>

    逻辑设置

    final List<String> mDatas = Arrays.asList(UiUtils.getStringArray(R.array.tab_names));
    mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
        @Override
        public int getCount() {
            return mDatas.size();
        }
    
        @Override
        public Fragment getItem(int position) {
            return FragmentFactory.createFragment(position);
        }
    };
    //设置Tab上的标题
    mIndicator.setTabItemTitles(mDatas);
    mViewPager.setAdapter(mAdapter);
    //设置关联的ViewPager
    mIndicator.setViewPager(mViewPager, 0);

    其中mDatas获取到的是一串在Strings.xml字符集合

    <string-array name="tab_names">
        <item>首页</item>
        <item>活动</item>
        <item>辅练</item>
        <item>商城</item>
        <item>我的</item>
    </string-array>

    简单点可以直接定义出来,不必获取xml中的;其中FragmentFactory就是创建不同的fragment简单点new 一个定义好的即可。

    此文只为记录,向hongyang大佬致敬

    http://blog.csdn.net/lmj623565791/article/details/42160391

    相关资料下载

    http://pan.baidu.com/s/1slFbxF3

     

    圆形指示器

    public class CircleIndicatorView extends View implements ViewPager.OnPageChangeListener{
        private static final String LETTER[] = new String[]{"A","B","C","D","E","F","G","H","I","G","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"};
        // private int mSelectColor = Color.parseColor("#E38A7C");
        private int mSelectColor = Color.parseColor("#FFFFFF");
        private Paint mCirclePaint;
        private Paint mTextPaint;
        private int mCount; // indicator 的数量
        private int mRadius;//半径
        private int mStrokeWidth;//border
        private int mTextColor;// 小圆点中文字的颜色
        private int mDotNormalColor;// 小圆点默认颜色
        private int mSpace = 0;// 圆点之间的间距
        private List<Indicator> mIndicators;
        private int mSelectPosition = 0; // 选中的位置
        private FillMode mFillMode = FillMode.NONE;// 默认只有小圆点
        private ViewPager mViewPager;
        private OnIndicatorClickListener mOnIndicatorClickListener;
        /**
         * 是否允许点击Indicator切换ViewPager
         */
        private boolean mIsEnableClickSwitch = false;
        public CircleIndicatorView(Context context) {
            super(context);
            init();
        }
    
        public CircleIndicatorView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            getAttr(context,attrs);
            init();
        }
    
        public CircleIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            getAttr(context,attrs);
            init();
        }
    
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        public CircleIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            getAttr(context,attrs);
            init();
        }
    
        private void init(){
    
            mCirclePaint = new Paint();
            mCirclePaint.setDither(true);
            mCirclePaint.setAntiAlias(true);
            mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE);
    
            mTextPaint = new Paint();
            mTextPaint.setDither(true);
            mTextPaint.setAntiAlias(true);
            // 默认值
            mIndicators = new ArrayList<>();
    
            initValue();
    
        }
    
        private void initValue(){
            mCirclePaint.setColor(mDotNormalColor);
            mCirclePaint.setStrokeWidth(mStrokeWidth);
    
            mTextPaint.setColor(mTextColor);
            mTextPaint.setTextSize(mRadius);
        }
    
        /**
         * 获取自定义属性
         * @param context
         * @param attrs
         */
        private void getAttr(Context context,AttributeSet attrs){
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleIndicatorView);
            mRadius = (int) typedArray.getDimensionPixelSize(R.styleable.CircleIndicatorView_indicatorRadius,dpToPx(6));
            mStrokeWidth = (int) typedArray.getDimensionPixelSize(R.styleable.CircleIndicatorView_indicatorBorderWidth,dpToPx(2));
            mSpace = typedArray.getDimensionPixelSize(R.styleable.CircleIndicatorView_indicatorSpace,dpToPx(5));
            // color
            mTextColor = typedArray.getColor(R.styleable.CircleIndicatorView_indicatorTextColor,Color.BLACK);
            mSelectColor = typedArray.getColor(R.styleable.CircleIndicatorView_indicatorSelectColor,Color.WHITE);
            mDotNormalColor = typedArray.getColor(R.styleable.CircleIndicatorView_indicatorColor,Color.GRAY);
    
            mIsEnableClickSwitch = typedArray.getBoolean(R.styleable.CircleIndicatorView_enableIndicatorSwitch,false);
            int fillMode = typedArray.getInt(R.styleable.CircleIndicatorView_fill_mode,2);
            if(fillMode == 0){
                mFillMode = FillMode.LETTER;
            }else if(fillMode == 1){
                mFillMode = FillMode.NUMBER;
            }else{
                mFillMode = FillMode.NONE;
            }
            typedArray.recycle();
        }
    
        /**
         * 测量每个圆点的位置
         */
        private void measureIndicator(){
            mIndicators.clear();
            float cx = 0;
            for(int i=0;i<mCount;i++){
                Indicator indicator = new Indicator();
                if( i== 0){
                    cx = mRadius + mStrokeWidth;
                }else{
                    cx += (mRadius + mStrokeWidth) * 2 +mSpace;
                }
    
                indicator.cx = cx;
                indicator.cy = getMeasuredHeight() / 2;
    
                mIndicators.add(indicator);
            }
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            int width = (mRadius+mStrokeWidth)* 2 * mCount + mSpace *(mCount - 1);
            int height = mRadius * 2 + mSpace * 2;
    
            setMeasuredDimension(width,height);
    
            measureIndicator();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
    
            for(int i=0;i<mIndicators.size();i++){
    
                Indicator indicator = mIndicators.get(i);
                float x = indicator.cx;
    
                float y = indicator.cy;
    
                if(mSelectPosition == i){
                    mCirclePaint.setStyle(Paint.Style.FILL);
                    mCirclePaint.setColor(mSelectColor);
                }else{
                    mCirclePaint.setColor(mDotNormalColor);
                    if(mFillMode != FillMode.NONE){
                        mCirclePaint.setStyle(Paint.Style.STROKE);
                    }else{
                        mCirclePaint.setStyle(Paint.Style.FILL);
    
                    }
                }
                canvas.drawCircle(x,y, mRadius, mCirclePaint);
    
                // 绘制小圆点中的内容
                if(mFillMode != FillMode.NONE){
                    String text = "";
                    if(mFillMode == FillMode.LETTER){
                        if(i >= 0 && i<LETTER.length){
                            text = LETTER[i];
                        }
                    }else{
                        text = String.valueOf(i+1);
                    }
                    Rect bound = new Rect();
                    mTextPaint.getTextBounds(text,0,text.length(),bound);
                    int textWidth = bound.width();
                    int textHeight = bound.height();
    
                    float textStartX = x - textWidth / 2;
                    float textStartY = y + textHeight / 2;
                    canvas.drawText(text,textStartX,textStartY, mTextPaint);
                }
    
            }
    
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            float xPoint = 0;
            float yPoint = 0;
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    xPoint = event.getX();
                    yPoint = event.getY();
                    handleActionDown(xPoint,yPoint);
                    break;
    
            }
    
            return super.onTouchEvent(event);
        }
    
        private void handleActionDown(float xDis,float yDis){
            for(int i=0;i<mIndicators.size();i++){
                Indicator indicator = mIndicators.get(i);
                if(xDis < (indicator.cx + mRadius+mStrokeWidth)
                        && xDis >=(indicator.cx - (mRadius + mStrokeWidth))
                        && yDis >= (yDis - (indicator.cy+mStrokeWidth))
                        && yDis <(indicator.cy+mRadius+mStrokeWidth)){
                    // 找到了点击的Indicator
                    // 是否允许切换ViewPager
                    if(mIsEnableClickSwitch){
                        mViewPager.setCurrentItem(i,false);
                    }
    
                    // 回调
                    if(mOnIndicatorClickListener!=null){
                        mOnIndicatorClickListener.onSelected(i);
                    }
                    break;
                }
            }
        }
    
        public void setOnIndicatorClickListener(OnIndicatorClickListener onIndicatorClickListener) {
            mOnIndicatorClickListener = onIndicatorClickListener;
        }
    
        private void setCount(int count) {
            mCount = count;
            invalidate();
        }
    
        /**
         * 设置 border
         * @param borderWidth
         */
        public void setBorderWidth(int borderWidth){
            mStrokeWidth = borderWidth;
            initValue();
        }
    
        /**
         * 设置文字的颜色
         * @param textColor
         */
        public void setTextColor(int textColor) {
            mTextColor = textColor;
            initValue();
        }
    
        /**
         * 设置选中指示器的颜色
         * @param selectColor
         */
        public void setSelectColor(int selectColor) {
            mSelectColor = selectColor;
        }
    
        /**
         *  设置指示器默认颜色
         * @param dotNormalColor
         */
        public void setDotNormalColor(int dotNormalColor) {
            mDotNormalColor = dotNormalColor;
            initValue();
        }
    
        /**
         * 设置选中的位置
         * @param selectPosition
         */
        public void setSelectPosition(int selectPosition) {
            mSelectPosition = selectPosition;
        }
    
        /**
         * 设置Indicator 模式
         * @param fillMode
         */
        public void setFillMode(FillMode fillMode) {
            mFillMode = fillMode;
        }
    
        /**
         * 设置Indicator 半径
         * @param radius
         */
        public void setRadius(int radius) {
            mRadius = radius;
            initValue();
        }
    
        public void setSpace(int space) {
            mSpace = space;
        }
    
        public void setEnableClickSwitch(boolean enableClickSwitch){
            mIsEnableClickSwitch = enableClickSwitch;
        }
        /**
         *  与ViewPager 关联
         * @param viewPager
         */
        public void setUpWithViewPager(ViewPager viewPager){
            releaseViewPager();
            if(viewPager == null){
                return;
            }
            mViewPager = viewPager;
            mViewPager.addOnPageChangeListener(this);
            int count = mViewPager.getAdapter().getCount();
            setCount(count);
        }
    
        /**
         * 重置ViewPager
         */
        private void releaseViewPager(){
            if(mViewPager!=null){
                mViewPager.removeOnPageChangeListener(this);
                mViewPager = null;
            }
    
        }
    
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    
        }
    
        @Override
        public void onPageSelected(int position) {
            mSelectPosition = position;
            invalidate();
        }
    
        @Override
        public void onPageScrollStateChanged(int state) {
    
        }
    
        /**
         * Indicator 点击回调
         */
        public interface OnIndicatorClickListener{
            public void onSelected(int position);
        }
    
    
        public static class Indicator{
            public float cx; // 圆心x坐标
            public float cy; // 圆心y 坐标
        }
    
        public enum FillMode{
            LETTER,
            NUMBER,
            NONE
        }
    
    
        public  int dpToPx(int dp) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
        }
    
        public  int pxToDp(float px) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, px, Resources.getSystem().getDisplayMetrics());
        }
    
    }

    自定义属性

    <declare-styleable name="CircleIndicatorView">
        <attr name="indicatorRadius" format="dimension"/>
        <attr name="indicatorBorderWidth" format="dimension"/>
        <attr name="indicatorSpace" format="dimension"/>
        <attr name="indicatorTextColor" format="color"/>
        <attr name="indicatorColor" format="color"/>
        <attr name="indicatorSelectColor" format="color"/>
        <attr name="enableIndicatorSwitch" format="boolean"/>
        <attr name="fill_mode">
            <enum name="letter" value="0"/>
            <enum name="number" value="1"/>
            <enum name="none" value="2"/>
        </attr>
    </declare-styleable>

    xml设置

    <com.zhouwei.indicatorview.CircleIndicatorView
             android:id="@+id/indicator_view"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_alignParentBottom="true"
             android:layout_marginBottom="50dp"
             android:layout_centerHorizontal="true"
             app:indicatorSelectColor="#00A882"
             app:fill_mode="letter"
             app:indicatorBorderWidth="2dp"
             app:indicatorRadius="8dp"
             app:indicatorColor="@color/colorAccent"
             app:indicatorTextColor="@android:color/white"
             />

    .class设置

    // 初始化ViewPager 相关
            mViewPager = (ViewPager) findViewById(R.id.viewpager);
            mPagerAdapter = new ViewPagerAdapter();
            mViewPager.setAdapter(mPagerAdapter);
    
            mIndicatorView = (CircleIndicatorView) findViewById(R.id.indicator_view);
            // 关联ViewPager 
            mIndicatorView.setUpWithViewPager(mViewPager);

    相关设置可以不在xml中设置,在代码中同样可以设置

    // 在代码中设置相关属性
            
            CircleIndicatorView indicatorView = (CircleIndicatorView) findViewById(R.id.indicator_view3);
            // 设置半径
            indicatorView.setRadius(DisplayUtils.dpToPx(15));
            // 设置Border
            indicatorView.setBorderWidth(DisplayUtils.dpToPx(2));
    
            // 设置文字颜色
            indicatorView.setTextColor(Color.WHITE);
            // 设置选中颜色
            indicatorView.setSelectColor(Color.parseColor("#FEBB50"));
            //
            indicatorView.setDotNormalColor(Color.parseColor("#E38A7C"));
            // 设置指示器间距
            indicatorView.setSpace(DisplayUtils.dpToPx(10));
            // 设置模式
            indicatorView.setFillMode(CircleIndicatorView.FillMode.LETTER);
            
            // 设置点击Indicator可以切换ViewPager
            indicatorView.setEnableClickSwitch(true);
    
            // 最重要的一步:关联ViewPager
            indicatorView.setUpWithViewPager(mViewPager);

    自定义属性

    属性名属性意义取值
    indicatorRadius设置指示器圆点的半径单位为 dp 的值
    indicatorBorderWidth设置指示器的border单位为 dp 的值
    indicatorSpace设置指示器之间的距离单位为 dp 的值
    indicatorTextColor设置指示器中间的文字颜色颜色值,如:#FFFFFF
    indicatorColor设置指示器圆点的颜色颜色值
    indicatorSelectColor设置指示器选中的颜色颜色值
    fill_mode设置指示器的模式枚举值:有三种,分别是letter,number和none
    enableIndicatorSwitch设置是否点击Indicator切换ViewPager,默认为false布尔值

    源代码参见

    https://github.com/pinguo-zhouwei/CircleIndicatorView

     

     

    转载于:https://my.oschina.net/u/3015461/blog/916382

    展开全文
  • viewpager,indicator,自定义指示器,支持圆形指示器顶部展示,展示图片等功能 <com.example.indicator.PagerSlidingTabStrip android:id="@+id/id_pager_s_t_s" android:layout_width="match_parent" ...
  • Android——自定义ViewPager指示器

    千次阅读 2017-07-06 20:18:38
    很多ViewPager和下面的图形绑定一块来滚动,也就是指示器指示器可以是很多,正方形,长方形,三角形,圆形等。这里举一个三角形例子:效果图:未滑动 滑动中:这里滑动只要超过倒数第二个就自动向后滑动。上代码...

    很多ViewPager和下面的图形绑定一块来滚动,也就是指示器,指示器可以是很多,正

    方形,长方形,三角形,圆形等。

    这里举一个三角形例子:

    效果图:未滑动
    这里写图片描述

    滑动中:

    这里写图片描述

    这里滑动只要超过倒数第二个就自动向后滑动。

    上代码:

    自定义view:

    
    
    public class ViewPagerIndicator extends LinearLayout {
        private Paint paint;
        private Path path;
        private int triangleWidth;
        private int triangleHeight;
        private static final float RADIO_TRIANGLE_WIDTH = 1 / 6f;
        private static final int HIGHCOLOR = 0xffffffff;
        private static final int LOWERCOLOR = 0x88ffffff;
        private int initTranslationX;
        private int moveTranslationX;
        private int TabVisibleCount;
        private static final int COUNT_DEFAULT_TAB = 4;
        private List<String> titles = new ArrayList<String>();
    
        public ViewPagerIndicator(Context context, AttributeSet attrs) {
            super(context, attrs);
            // 获取可见Tab的数量
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerIndicator);
            TabVisibleCount = ta.getInt(R.styleable.ViewPagerIndicator_visible_tab_count, COUNT_DEFAULT_TAB);
            if (TabVisibleCount < 0) {
                TabVisibleCount = COUNT_DEFAULT_TAB;
            }
            ta.recycle();
    
            // 初始化画笔
            paint = new Paint();
            paint.setAntiAlias(true);
            paint.setColor(Color.WHITE);
            paint.setStyle(Style.FILL);
            paint.setPathEffect(new CornerPathEffect(5));
    
        }
    
        public ViewPagerIndicator(Context context) {
            super(context, null);
            // TODO Auto-generated constructor stub
        }
    
        /**
         * 绘制三角形
         */
    
        /**
         * 没有背景时直接调用的是dispatchDraw()方法,draw()方法里包含了dispatchDraw()方法的调用。
         * 因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法
         */
        @Override
        protected void dispatchDraw(Canvas canvas) {
            // TODO Auto-generated method stub
            super.dispatchDraw(canvas);
            /*
             * 如果有save()和restore(), 那么平移、缩放、旋转等操作只对save()和restore()作用域之间的代码有效。
             * 当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,
             * 那么之后在画布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,
             * 当操作之后取出之前保存过的状态,这样就不会对其他的元素进行影响
             */
            canvas.save();
            canvas.translate(initTranslationX + moveTranslationX, getHeight());
            canvas.drawPath(path, paint);
            canvas.restore();
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            // TODO Auto-generated method stub
            super.onSizeChanged(w, h, oldw, oldh);
            triangleWidth = (int) (w / TabVisibleCount * RADIO_TRIANGLE_WIDTH);
            initTranslationX = w / TabVisibleCount / 2 - triangleWidth / 2;
    
            initTrangle();
        }
    
        /**
         * 在xml加载完成以后调用此方法
         */
    
        @Override
        protected void onFinishInflate() {
            // TODO Auto-generated method stub
            super.onFinishInflate();
    
            int count = getChildCount();
            if (count == 0)
                return;
    
            for (int i = 0; i < count; i++) {
                View view = getChildAt(i);
                LinearLayout.LayoutParams lp = (LayoutParams) view.getLayoutParams();
                // 如果之前设有weight 都设置为0
                lp.weight = 0;
                lp.width = getScreenWidth() / TabVisibleCount;
                view.setLayoutParams(lp);
            }
            setItemClickEvent();
        }
    
        private int getScreenWidth() {
            // TODO Auto-generated method stub
            WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics outMetrics = new DisplayMetrics();
            wm.getDefaultDisplay().getMetrics(outMetrics);
            return outMetrics.widthPixels;
        }
    
        /**
         * 初始化三角形
         */
        private void initTrangle() {
            // TODO Auto-generated method stub
            path = new Path();
            triangleHeight = (int) (triangleWidth / 2 / 1.7);
            path.moveTo(0, 0);
            path.lineTo(triangleWidth, 0);
            path.lineTo(triangleWidth / 2, -triangleHeight);
            path.close();
        }
    
        /**
         * 指示器跟随手指移动
         * 
         * @param position
         *            当前页
         * @param offset(0~1)
         *            偏移量
         */
        public void scoll(int position, float offset) {
            // TODO Auto-generated method stub
            int tabWidth = getWidth() / TabVisibleCount;
            moveTranslationX = (int) (tabWidth * (position + offset));
    
            // 容器移动,当tab处于移动至最后一个时
    
            if (position >= (TabVisibleCount - 2) && offset > 0 && getChildCount() > TabVisibleCount) {
    
                if (TabVisibleCount != 1) {
    
                    scrollTo((position - (TabVisibleCount - 2)) * tabWidth + (int) (tabWidth * offset), 0);
                } else {
                    scrollTo(position * tabWidth + (int) (tabWidth * offset), 0);
                }
            }
    
            // 请求重绘三角形
            invalidate();
        }
    
        // 该方法需要在setTabitemTitles方法之前调用有效,因为用到了TabVisibleCount
        public void setVisibleTab(int count) {
            this.TabVisibleCount = count;
        }
    
        /**
         * 根据title动态创建TextView
         * 
         * @param title
         * @return
         */
        public void setTabitemTitles(List<String> titles) {
            if (titles != null && titles.size() > 0) {
                this.titles = titles;
            }
            for (String title : titles) {
                addView(getTextView(title));
            }
            setItemClickEvent();
        }
    
        private View getTextView(String title) {
            // TODO Auto-generated method stub
            TextView tv = new TextView(getContext());
            LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            lp.width = getScreenWidth() / TabVisibleCount;
            tv.setText(title);
            tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
            tv.setGravity(Gravity.CENTER);
            tv.setLayoutParams(lp);
            tv.setTextColor(LOWERCOLOR);
            return tv;
        }
    
        /**
         * 用户的setOnPageChangeListener回调方法供外部使用
         * 
         * @author lpl
         *
         */
        public interface PageChangeListener {
            public void onPageScrollStateChanged(int arg0);
    
            public void onPageScrolled(int arg0, float arg1, int arg2);
    
            public void onPageSelected(int arg0);
        }
    
        private PageChangeListener listener;
    
        public void setOnPageChangeListener(PageChangeListener listener) {
            this.listener = listener;
        }
    
        /**
         * 关联ViewPager
         * 
         * @param pager
         * @param pos
         *            默认选中页
         */
        private ViewPager pager;
    
        public void setViewPager(final ViewPager pager, int pos) {
            this.pager = pager;
            pager.setOnPageChangeListener(new OnPageChangeListener() {
    
                @Override
                public void onPageScrollStateChanged(int arg0) {
                    // TODO Auto-generated method stub
    
                }
    
                @Override
                public void onPageScrolled(int arg0, float arg1, int arg2) {
                    // 总偏移量:arg0(position)*tabWidth+arg1(positionoffset)*tabWidth
                    if (listener != null) {
                        listener.onPageScrolled(arg0, arg1, arg2);
                    }
                    scoll(arg0, arg1);
    
                }
    
                @Override
                public void onPageSelected(int arg0) {
                    // TODO Auto-generated method stub
                    if (listener != null) {
                        listener.onPageSelected(arg0);
                    }
                    HighTextView(arg0);
                }
            });
            pager.setCurrentItem(pos);
            HighTextView(pos);
        }
    
        public void resetColorTextView() {
            for (int i = 0; i < getChildCount(); i++) {
                TextView tv = (TextView) getChildAt(i);
                tv.setTextColor(LOWERCOLOR);
            }
        }
    
        /**
         * 设置高亮的TextView
         * 
         * @param position
         */
        public void HighTextView(int position) {
            resetColorTextView();
            TextView tv = (TextView) getChildAt(position);
            tv.setTextColor(HIGHCOLOR);
        }
    
        /**
         * 设置tab的点击事件
         */
        public void setItemClickEvent() {
            for (int i = 0; i < getChildCount(); i++) {
                final int j = i;
                TextView tv = (TextView) getChildAt(i);
                tv.setOnClickListener(new OnClickListener() {
    
                    @Override
                    public void onClick(View v) {
                        // TODO Auto-generated method stub
                        pager.setCurrentItem(j);
                    }
                });
            }
        }
    }
    

    在Activity中onCreate调用此控件:

    // setVisibleTab该方法需要在setTabitemTitles方法之前调用有效,
    //因为用到了TabVisibleCount
            // indicator.setVisibleTab(3);设置可见的title个数
    
            //titles是List容器存放TextView
            indicator.setTabitemTitles(titles);
    
            //默认0是第一个title为高亮选中
            indicator.setViewPager(viewpager, 0);
            viewpager.setAdapter(adapter);
    展开全文
  • 自定义Android圆形指示器
  • 本文实例为大家分享了Android自定义带圆点的半圆形进度条,供大家参考,具体内容如下 仅限用于半圆形,如须要带圆点的圆形进度条,圆点会出现错位现象,此代码仅供,带圆点的圆形进度条有空研究一下!图片效果在下方, ...
  • 指示器在APP应用中最为常见,例如新手引导页,滑动页,轮播图等等,在这些界面的指示器中圆形指示器最为常见。圆形指示器在网上一搜也很多,但是很多用着都不太方便,耦合性太强,一些设置往往不符合要求,使用起来...
  • 那么今天就通过自定义View来创建一个属于自己的指示器。 效果图: 首先,在res - values下新建一个attr文件, 可命名为:attr_indicator.xml 代码如下: <resources> <declare-styleable name=...
  • 1:轮播com.youth.banner 轮播图控件com.youth.banner,github地址:https://github.com/youth5201314/banner。 具体使用请看github,简单方便,咱们主要介绍如何自定义指示器。 2:自定义指示器 ...
  • 但是我们可以自定义类似的圆形的进度提示控件,主要使用iOS中的绘图机制来实现。这里我们要实现一个通过按钮点击然后圆形进度提示不断增加的效果。 (1)新建一个Cocoa Touch Class,注意要继承自UIView。这个是绘制...
  • ViewPager指示器实现代码如下,我主要是实现了圆形指示器。 首先在attrs文件中添加: <declare-styleable name="ViewPagerIndicator"> <attr name="unSelectCirclerColor" format="color"></...
  •  分享一个控件,可用于ViewPager页面指示器,使用起来非常简单方便。支持横向、纵向两种布局,可以xml设置也可以动态设置。支持三种模式Mode:  INSIDE: 滚动的小圆点被遮住,在整个圆点视图的背后滚
  • 其实我这个就是实现四个点的效果,通过滑动viewpager来选中当前的点进行变色类似这种效果图,颜色可以自己修改还有大小不多说直接上代码自定义属性xml&lt;?xml version="1.0" encoding="utf-8&...
  • 圆形指示器UI图 UI分析 内侧一个白色的虚线弧 外侧有两条弧,一个是灰色的实线弧,一个是白色的虚线弧,实线弧尾部有一个小圆点 中间是文字,大小不一致,颜色是白色 加载的时候需要一个加载动画,实线椭圆进度条...
  • Android 自定义View 颜色选取,可以实现水平、竖直选择颜色类似 SeekBar 的方式通过滑动选择颜色。 效果图 xml 属性 1.indicatorColor 指示点颜色 2.indicatorEnable 是否使用指示点 3.orientation 方向 ...
  • 制作一个圆形仪表盘,此表盘通用各种参数指示,如:温度,速度,压力,等进度相关的类别。 二、环境 qt5.7 MinGW windows8 三、正文 首先 演示一下效果图 表盘采用的是上半圆形表盘,有彩色的渐变条框,这个...
  • 上一次说了自定义圆形页面指示器 Android 自定义控件之圆形页面指示器CirclePageIndicator带划动效果 应该很少人耐心看完了。。。 这一次就把它打包成依赖库,直接添加依赖使用就好了。 不过为了方便我自己的项目...
  • 微信官方提供的swiper的指示点是小圆形,位置在swiper中间靠近底部位置。如 而产品的要求是需要将指示点方式在右下角,同时是条形。如 思路:不显示swiper自带的指示点,根据banner数据集合的长度显示指示点。...
  • 在element中提供的 只能是在指定位置上设置 所以通过属性无法设置其他 位置 所以我们只能去手动添加位置 效果图 <template>...div class="container-swiper">... v-if="$store.state.search....
  • //节点圆形半径 private float mNodeRadius; //节点默认填充颜色 private int mNodeColor; //移动节点填充颜色 private int mSelectedNodeColor; //当前选中节点的索引 private int mCurrentIndex = 0; //...
  • android 自定义带进度值的圆形进度条

    千次阅读 2017-03-25 23:05:24
    这个进度条非常实用,可以有进度的显示,可以自定义圆内部和边缘的颜色等,话不都说,先看效果再上车, 直接上代码: 调用实例 package android.example.com.roundprogressbar; import android.app.Activity; ...
  • 自定义的一个粘性的指示器。 先看看效果。效果赞不赞因人而异,想更酷一些的话,可以花时间实现一下,这里就是想给大家分享一下实现的思路。 1.继承View。获取、计算 一些必要的数据。如:padding,view大小,圆点...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,169
精华内容 2,067
关键字:

自定义圆形指示器