美文网首页面试题自定义控件Android效果/自定义
从一个简洁的进度刻度绘制中了解自定义View的思路流程

从一个简洁的进度刻度绘制中了解自定义View的思路流程

作者: zhangke3016 | 来源:发表于2016-07-30 13:32 被阅读1934次
    circleloading

    先看效果(原谅我的渣像素),进度的刻度、宽度、颜色可以随意设定:

    circleloading circleloading
    【项目github地址: https://github.com/zhangke3016/CircleLoading]

    实现起来并不难,通过本文,我们可以学到:

    1、自定义属性的使用。
    2、shader的使用
    3、自定义View中对onmeasure的处理
    4、增深对PathMeasure工具类的了解
    5、最主要的是对自定义View有个比较清晰的思路认识

    一、原理介绍

    做这样一个进度效果,我们可以拆分如下步骤来实现:

    1、从外部圆环开始测量绘制;
    2、再加入刻度条效果;
    3、再加入刻度随进度增加而增加效果;
    4、增加自定义属性增加可定制性;
    5、控件使用方法介绍

    csdn地址:http://blog.csdn.net/zhangke3016/article/details/52035641

    OK,有了这个思路,那我们开始吧:

    1、测量绘制外部圆环
    首先我们要开始绘制外部的圆环,这步很简单,主要是使用canvas的drawArc()方法,

    /*     
    * @param oval 画弧线矩形区域
    * @param startAngle 开始的角度
     * @param sweepAngle 划过的角度
    * @param useCenter 如果为true 为实心圆弧
     * @param paint     画笔
       * /
     public void drawArc(RectF oval, float startAngle, float sweepAngle,boolean useCenter,Paint paint)
    

    这个相对简单,主要是确定开始角度,并不断增加绘制划过角度,圆弧就出现在界面中了,这里需要注意的是RectF oval的大小确定:
    在确定RectF oval之前,我们要先测量确定当前控件的宽高,根据当前控件的宽高来确定oval的合适大小。
    测量当前控件的大小一般我们在onmeasure()方法中处理,resolveMeasured()方法传递两个参数,第一各参数为widthMeasureSpec或者heightMeasureSpec,第二个参数为期望值也就是默认值。

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(resolveMeasured(widthMeasureSpec, nDesired), resolveMeasured(heightMeasureSpec, nDesired));
        }
    
    /**
         * 
         * @param measureSpec 
         * @param desired 
         * @return
         */
        private int resolveMeasured(int measureSpec, int desired)  
        {  
            int result = 0;  
            int specSize = MeasureSpec.getSize(measureSpec);  
            switch (MeasureSpec.getMode(measureSpec)) {  
                case MeasureSpec.UNSPECIFIED: // 
                    result = desired;  
                    break;  
                case MeasureSpec.AT_MOST:  //wrap-content
                    result = Math.min(specSize, desired);  
                    break;  
                case MeasureSpec.EXACTLY:  //match-content
                default:  
                    result = specSize;  
            }  
            return result;  
        } 
    

    在测量之后我们就可以来求oval的具体大小 ,我把这步操作放在了onSizeChanged方法中,求出最小一边的一半减去圆环的宽度,得到圆弧的半径,然后根据半径以控件中心为中心绘制我们所需要的矩形区域。

    radiu = (int) ((Math.min(getWidth(), getHeight()))/2-mPaint.getStrokeWidth());
    
                oval.left = getWidth()/2-radiu;
                oval.top = getHeight()/2-radiu;
                oval.right = getWidth()/2+radiu;
                oval.bottom = getHeight()/2+radiu;
    

    这样下来,基本上就可以绘制比较理想的弧线了,在这里也把绘制中心文字也说下吧,主要通过getTextBounds()方法获取文字区域的宽高,然后在drawText()方法中将坐标进行适当偏移以使文字居中显示。

    String strProgressText = "";
            if(mOnProgressListener !=null){//如果不为空  则为接口返回的值
                strProgressText = mOnProgressListener.OnProgress(mMax, mProgress);
            }else{
                strProgressText = mProgress+"/"+mMax;
            }
            mTextPaint.getTextBounds(strProgressText, 0, strProgressText.length(), bounds);
            canvas.drawText(strProgressText, oval.centerX()-bounds.width()/2, oval.centerY()+bounds.height()/2, mTextPaint);
    

    最后还有一个小点就是渐变色的绘制,用的SweepGradient,我们可以看下Shader的子类,shader类是很强大的,类似与圆形图片、渐变效果都可以用它来实现,这里就不过多展开了:

    Shader
    SweepGradient sweepGradient = new SweepGradient(getWidth()/2, getHeight()/2, colors, null);
      p.setShader(sweepGradient);
    

    到这里为止,我们的圆环已经绘制好了,包括中间的文字以及圆环的渐变效果都已经实现了,就是这样的:

    进度显示

    2、加入刻度效果

    接下来要加入刻度效果,实现思路是这样的,我先默认实现两个圆弧(注意这两个圆弧只是我们假定添加的,并不是真正加在控件中显示),然后获取相同角度,根据相对位置获取两个圆环上的点进行连线,将这两个点连起的刻度线封装成对象添加在集合中,最后在onDraw方法中遍历集合,进行绘制。

    oval2 = new RectF();//内环
                oval2.left = getWidth()/2-radiu/4f*3;
                oval2.top = getHeight()/2-radiu/4f*3;
                oval2.right = getWidth()/2+radiu/4f*3;
                oval2.bottom = getHeight()/2+radiu/4f*3;
    
                oval3 = new RectF();//外环
                oval3.left = getWidth()/2-radiu/8f*7;
                oval3.top = getHeight()/2-radiu/8f*7;
                oval3.right = getWidth()/2+radiu/8f*7;
                oval3.bottom = getHeight()/2+radiu/8f*7;
    
    //然后初始化数据
        /**
         * 初始化数据
         */
        private void initData() {
            mLinesList.clear();
    
            Path path = new Path();
            Path path1 = new Path();
            //从startAngle开始 绘制180角度
            path.addArc(oval2, mStartAngle, mGraduationSweepAngle);
            path1.addArc(oval3, mStartAngle, mGraduationSweepAngle);
    
            PathMeasure pm = new PathMeasure(path, false);
            float itemLength = pm.getLength()/(nGraduationCount-1);
    
            PathMeasure pm1 = new PathMeasure(path1, false);
    
            float[] pos = new float[2];
            float[] postemp = new float[2];
            for (int i = 0; i < nGraduationCount; i++) {
                pm.getPosTan(itemLength*i, pos , null );
         pm1.getPosTan(itemLength*i/pm.getLength()*pm1.getLength(), postemp , null);
                Line line = new Line();
                line.p1.x = pos[0];
                line.p1.y = pos[1];
                line.p2.x = postemp[0];
                line.p2.y = postemp[1];
                mLinesList.add(line);
            }
        }
    
    //ondraw方法:
    for (int i = 0; i < mLinesList.size(); i++) {
                    Line line = mLinesList.get(i);
                    canvas.drawLine(line.p1.x, line.p1.y, line.p2.x, line.p2.y, mRollPaint);
                }
    

    这里用到了PathMeasure这个辅助工具,这里简单讲一下:

    public PathMeasure()
    
    //path:需要测量的path  forceClosed:是否关闭path
    public PathMeasure(Path path, boolean forceClosed)
    
    //指定需要测量的path
    public void setPath(Path path, boolean forceClosed)
    
    //返回当前path的总长度。
    getLength()
    
    //返回值是boolean,如过path为空,则返回false 传入参数有三个: 
    //distance:传入距离起点的距离。 
    //pos[]:意思是position,分别对应点的x,y坐标 
    //tan[]:获取切线值,不常用。
    public boolean getPosTan(float distance, float pos[], float tan[])
    
    //返回一个处理好的matrix,但是这个matrix是以左上角作为旋转点,所以需要将这个点移动到中心点。 其中一个参数flags,指这个martrix需要什么信息。flags的值有如下两个 
    PathMeasure.POSITION_MATRIX_FLAG:位置信息 
    pathMeasure.TANGENT_MATRIX_FLAG:切边信息,方位角信息
    public boolean getMatrix(float distance, Matrix matrix, int flags)
    
    //这个方法返回boolean,如果截取的长度为0则返回false,否则为true。参数如下
    //startD:起始距离 
    //stopD:终点距离 
    //dst:接收截取的path 
    //startWithMoveTo:是否把截取的path,moveto到起始点。
    public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
    

    3、加入刻度随进度增加而增加效果,并增加进度变化回调方便操作

    加入刻度随进度增加而增加,我们可以这样想,首先我总的刻度数是一定的,判断划过的角度占圆周的百分比,随之就可以得到划过刻度数占总刻度数的百分比,进而就求出划过的刻度数了。

    for (int i = 0; i < Math.round(mSweepAngle*nGraduationCount/360f); i++) {
                if(i<mLinesList.size()){
                    Line line = mLinesList.get(i);
                    canvas.drawLine(line.p1.x, line.p1.y, line.p2.x, line.p2.y, mRollDrawPaint);
                }
            }
    

    将划过的刻度数用画笔再绘制一次,随进度增加的刻度效果就出现啦!

    /**
         * 设置进度监听
         * @param mOnProgressListener
         */
        public void setOnProgressListener(OnProgressListener mOnProgressListener) {
            this.mOnProgressListener = mOnProgressListener;
        }
        /**
         * 用于外部判断当前进度状态
         */
        interface OnProgressListener{
            /**
             * 返回中间部分文字内容
             * @param max
             * @param progress
             * @return  
             */
            String OnProgress(int max,int progress);
        }
    

    设置回调监听,这样在每次进度变化的时候,可以随意变化中间部分文字显示的内容。

    4、增加自定义属性增加可定制性

    attrs.xml:

           <declare-styleable name="LoadingStyle">
            <attr name="textSize" format="dimension|reference"/><!-- 字体大小 -->
            <attr name="textColor" format="color|reference"/><!-- 字体颜色 -->
            <attr name="strokeWidth" format="dimension|reference"/><!-- 圆环大小 -->
            <attr name="isShowGraduationBackground" format="boolean"/><!-- 是否显示背景刻度 -->
            <attr name="isShowOutRoll" format="boolean"/><!-- 是否显示外部进度框 -->
            <attr name="startAngle" format="integer|reference"/><!-- 开始的角度 -->
            <attr name="max" format="integer|reference"/><!-- 最大值 -->
            <attr name="progress" format="integer|reference"/><!-- 默认进度值 -->
            <attr name="graduationBackgroundColor" format="color|reference"/><!-- 刻度的背景颜色 -->
            <attr name="graduationWidth" format="dimension|reference"/><!-- 刻度的宽度 -->
            <attr name="graduationCount" format="integer|reference"/><!-- 刻度的个数 -->
        </declare-styleable>
    

    layout文件:

     xmlns:app="http://schemas.android.com/apk/res-auto"<!--设置命名空间 -->
    
      <com.mrzk.circleloadinglibrary.CircleLoadingView
            android:id="@+id/lv_loading"
            android:layout_width="250dp"
            android:layout_height="250dp"
            android:layout_centerInParent="true" 
            app:textSize="35sp"
            app:textColor="#f60"
            app:strokeWidth="10dp"
            app:isShowGraduationBackground="true"
            app:startAngle="0"
            app:max="300"
            app:progress="100"
            app:graduationBackgroundColor="#ccc"
            app:graduationWidth="5dp"
            app:graduationCount="10"
            app:isShowOutRoll="false"
            />
    

    获取自定义属性值:

    TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.LoadingStyle);
            mTextSize = (int) typedArray.getDimension(R.styleable.LoadingStyle_textSize, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics()));
            mStrokeWidth = (int) typedArray.getDimension(R.styleable.LoadingStyle_strokeWidth, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()));
            mGraduationWidth = (int) typedArray.getDimension(R.styleable.LoadingStyle_graduationWidth, mStrokeWidth/2);
            mTextColor = (int) typedArray.getColor(R.styleable.LoadingStyle_textColor, Color.BLACK);
            mGraduationBackgroundColor = (int) typedArray.getColor(R.styleable.LoadingStyle_graduationBackgroundColor, Color.BLACK);
            mStartAngle = (int) typedArray.getInt(R.styleable.LoadingStyle_startAngle, 180);
            mMax = (int) typedArray.getInt(R.styleable.LoadingStyle_max, 0);
            mProgress = (int) typedArray.getInt(R.styleable.LoadingStyle_progress, 0);
            nGraduationCount = (int) typedArray.getInt(R.styleable.LoadingStyle_graduationCount, 35);
            isShowGraduationBackground =  typedArray.getBoolean(R.styleable.LoadingStyle_isShowGraduationBackground, true);
            isShowOutRoll =  typedArray.getBoolean(R.styleable.LoadingStyle_isShowOutRoll, true);
            typedArray.recycle();
    

    5、使用方法

    int[] colors = {0xFFE5BD7D, 0xFFFAAA64,
                        0xFFFFFFFF, 0xFF6AE2FD,
                        0xFF8CD0E5, 0xFFA3CBCB,
                        0xFFBDC7B3, 0xFFD1C299, 0xFFE5BD7D};
            lv_loading.setTextColor(Color.BLACK);//设置中心文字颜色
            lv_loading.setMax(500);//设置最大进度
            lv_loading.setShowGraduationBackgroundEnable(true);//是否显示刻度背景
            lv_loading.setGraduationBackgroundColor(Color.GRAY);//刻度的背景颜色
            lv_loading.setStartAngle(180);//设置开始旋转角度
            lv_loading.setGraduationCount(10);//设置刻度数
            lv_loading.setGraduationWidth(5);//设置刻度的宽度
            lv_loading.setOutColors(colors);//设置外部圆环颜色
            lv_loading.setInnerGraduationColors(colors);//设置内部刻度进度颜色
            lv_loading.setTextSize(35);//设置内部文字字体大小
            lv_loading.setShowOutRollEnable(false);//设置是否显示外部进度框
            lv_loading.setOnProgressListener(new OnProgressListener() {
                @Override
                public String OnProgress(int max, int progress) {
                    
                    return progress*100f/max+"%";
                }
            });
    

    相关文章

      网友评论

      • 076d5adddeb3:楼主,问一下,我的as导入不了 我的是1.2版本
        zhangke3016:@SoundofThunder 需要修改下build
      • d45390a4c552:android studio导入项目的时候 Error:Could not find property 'publishedGroupId' on project ':circleloadinglibrary'.
        zhangke3016:@alexxxx 谢谢提醒~刚修改了下配置,可以再次下载下,推荐引用依赖:compile 'com.mrzk.loadingview:circleloadinglibrary:1.1.0'。
      • b8677546b4cb:外环圆弧头部怎么弄成圆形的?
        zhangke3016: @公子小哥 额~那个用画笔的setStrokeCap方法设置
        b8677546b4cb:@zhangke3016 不是,我说的是圆弧线条头部怎么弄成圆角的 = =。
        zhangke3016:@公子小哥 canvas里有drawarc,drawcircle相关方法的~
      • 在下喵星人:你的app目录里面的java代码好像没有提交
        zhangke3016:@墨明棋妙麟 那个确实没有提交,你可以引用library依照说明就可以实现自己想要的效果啦~
      • 珂珂苏:努力奋斗的青春,闪闪发亮的日子
      • 珂珂苏:棒棒哒~~加油!

      本文标题:从一个简洁的进度刻度绘制中了解自定义View的思路流程

      本文链接:https://www.haomeiwen.com/subject/qcradttx.html