博客地址:
先上效果图。

在工作中难免遇到自定义 View
的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累。自定义View实战.

图片 1

一、最简单的自定义View,什么都不显示,但是有View的特性

com.cctvjiatao.customview.MainActivity

packagecom.cctvjiatao.customview;

importandroid.app.Activity;

importandroid.os.Bundle;

publicclassMainActivityextendsActivity {

@Override

protectedvoidonCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

}

com.cctvjiatao.customview.v1.CustomView1

packagecom.cctvjiatao.customview.v1;

importandroid.content.Context;

importandroid.util.AttributeSet;

importandroid.view.View;

/**

* @作者: 麦典威

* @包名:com.cctvjiatao.customview.v1

* @文件名:CustomView1.java

* @功能: 最简单的自定义View,什么都不显示,但是有View的特性

*/

publicclassCustomView1extendsView {

publicCustomView1(Context context) {

super(context);

}

publicCustomView1(Context context, AttributeSet attrs) {

super(context, attrs);

}

}

activity_main.xml

xmlns:tools=””

android:id=”@+id/container”

android:layout_width=”match_parent”

android:layout_height=”match_parent”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:background=”#00ff00″>

AndroidManifest.xml

package=”com.cctvjiatao.customview”

android:versionCode=”1″

android:versionName=”1.0″>

android:minSdkVersion=”14″

android:targetSdkVersion=”21″/>

android:allowBackup=”true”

android:icon=”@drawable/ic_launcher”

android:label=”@string/app_name”

android:theme=”@style/AppTheme”>

android:name=”.MainActivity”

android:label=”@string/app_name”>

效果图暂略。

都说”给我一个Path,我还你一个世界“
一直都被贝塞尔曲线面纱挡住没有勇敢的去揭开,看到别人玩的贝塞尔曲线
玩的那么嗨,现在是时候揭开那层面纱了
效果:
本案是模仿了:贝塞尔曲线实战–BackgroundView
并在该基础上润下色 核心原理都是一样的(可能有些雷同 望原创谅解)。
demo

图片 2

这个版本主要的任务就是完成环信客服系统的集成,上一篇文章
仿IOS下载View
也是这个版本开发需求中的一小部分,那今天介绍一下另一个小需求
客服好评客服好评
的功能在于用户对客服服务态度和质量的评价,也是作为考核客服服务的标准。相关代码已上传
EvaluationCardView

问题

  • 如何线性颜色变化?

  Shader mShader = new LinearGradient(pointX - raduis, pointY, pointX + raduis, pointY,
                new int[]{0xFF445EED, 0xFF072AE9, 0xFF0625CE}, null, Shader.TileMode.CLAMP);

二、自定义View,显示一行文字。注意:drawText的坐标是 “左下” 坐标。

com.cctvjiatao.customview.MainActivity 同一

AndroidManifest.xml 同一

activity_main.xml

xmlns:tools=””

android:id=”@+id/container”

android:layout_width=”match_parent”

android:layout_height=”match_parent”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:background=”#00ff00″

android:visibility=”gone”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”>

com.cctvjiatao.customview.v2.CustomView1

packagecom.cctvjiatao.customview.v2;

importandroid.content.Context;

importandroid.graphics.Canvas;

importandroid.graphics.Paint;

importandroid.util.AttributeSet;

importandroid.view.View;

/**

* @作者: 麦典威

* @包名:com.cctvjiatao.customview.v2

* @文件名:CustomView2.java

* @功能: 自定义View,显示一行文字。注意:drawText的坐标是 “左下” 坐标。

*/

publicclassCustomView1extendsView {

publicCustomView1(Context context, AttributeSet attrs) {

super(context, attrs);

}

publicCustomView1(Context context) {

super(context);

}

@Override

protectedvoidonDraw(Canvas canvas) {

Paint paint =newPaint();

// canvas.drawText(“This is a canvas”, 0, 0, paint);//这样写不会显示文字,因为文字的左下坐标是(0,0)

canvas.drawText(“This is a canvas,坐标为左下(0,50)”,0,50, paint);// (字符,左坐标,下坐标,画笔)

paint.setTextSize(30);//设置画笔大小,即字体大小

canvas.drawText(“This is a canvas,坐标为左下(0,30)”,0,30, paint);// (字符,左坐标,下坐标,画笔)

}

}

效果图暂略。

图片 3

是不是很酷炫.看起来觉得很难? 不难 , 其实实现起来很容易。思路:

看一下预览效果:

完整的自定义View

public class SpeedControlView extends View implements Runnable {
    //画笔
    private Paint mPaint, textPaint, speedAreaPaint;
    private Context mContext;
    //屏幕宽高
    private int screenWidth, screenHeight;
    //仪表盘圆的半径
    private float raduis, sRaduis;
    //圆心
    private int pointX, pointY;
    //文字的偏移量
    private float textScale;
    //速度指针变化的位置
    private float linePointerX, linePointerY;
    //速度
    private int speed;
    //速度范围的2个扇形外切矩形
    private RectF speedRectF, speedRectFInner;
    //速度控制模式  1 加速  2 减速  3 手刹
    private int type;

    // 速度文字 绘制的XY坐标
    private int baseX, baseY;

    //屏幕密度
    private float mDensityDpi;

    //设置速度控制模式
    public void setType(int type) {
        this.type = type;
    }

    //开始重绘
    private boolean start = true;

    public void setStart(boolean start) {
        this.start = start;
    }

    // 设置速度 并重绘视图
    public void setSpeed(int speed) {
        this.speed = speed;
        if (speed > 0) {
            postInvalidate();
        }
    }


    public SpeedControlView(Context context) {
        this(context, null);
    }

    public SpeedControlView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SpeedControlView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;

        //获取屏幕宽高
//        screenWidth = ((Activity) context).getWindowManager().getDefaultDisplay().getWidth();
//        screenHeight = ((Activity) context).getWindowManager().getDefaultDisplay().getHeight();

        //获取屏幕宽高 和 屏幕密度dpi
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        screenWidth = displayMetrics.widthPixels;
        screenHeight = displayMetrics.heightPixels;
        mDensityDpi = displayMetrics.densityDpi / 320;  //320为我的测试机dpi密度,以次绘制视图
        //关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        //设置抗锯齿
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        //设置画笔样式
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(5 * mDensityDpi);

        //初始化  圆心左边 和 半径
        raduis = screenWidth / 3;
        pointX = pointY = screenWidth / 2;
//        pointY = screenHeight / 4;

        //设置抗锯齿
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setAntiAlias(true);
        //设置画笔颜色
        textPaint.setColor(Color.WHITE);
        // 获取字体并设置画笔字体
        Typeface typeface = Typeface.createFromAsset(mContext.getAssets(), "kt.ttf");
        textPaint.setTypeface(typeface);
        //设置抗锯齿
        speedAreaPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        speedAreaPaint.setAntiAlias(true);
        //设置画笔样式
        speedAreaPaint.setStyle(Paint.Style.FILL);
        // 设置速度范围扇形的渐变颜色
        Shader mShader = new LinearGradient(pointX - raduis, pointY, pointX + raduis, pointY,
                new int[]{0xFF445EED, 0xFF072AE9, 0xFF0625CE}, null, Shader.TileMode.CLAMP);
        speedAreaPaint.setShader(mShader);
        // 初始化速度范围的2个扇形外切矩形
        speedRectF = new RectF(pointX - raduis + 10 * mDensityDpi, pointY - raduis + 10 * mDensityDpi,
                pointX + raduis - 10 * mDensityDpi, pointY + raduis - 10 * mDensityDpi);
        speedRectFInner = new RectF(pointX - raduis / 2, pointY - raduis / 2,
                pointX + raduis / 2, pointY + raduis / 2);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.YELLOW);

        //绘制外层圆,两个外圈,两个内圈
        drawCicle(canvas);

        //绘制速度范围扇形区域
        speedAreaPaint.setColor(0x7E3F51B5);
        drawSpeedArea(canvas);

        //变换画笔颜色 绘制刻度
        mPaint.setColor(0xBF3F6AB5);
        drawScale(canvas);

        //变换画笔颜色 绘制速度标识文字
        textPaint.setTextSize(25 * mDensityDpi);
        mPaint.setColor(Color.WHITE);
        sRaduis = raduis - 50 * mDensityDpi;
        textScale = Math.abs(textPaint.descent() + textPaint.ascent()) / 2;
        Log.e("textScale", textScale + "");

        drawText(canvas);

        //绘制中间文字内容
        drawCenter(canvas);
    }

    /**
     * 绘制外层圆
     */
    private void drawCicle(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(0xFF343434);
        canvas.drawCircle(pointX, pointY, raduis, mPaint);

        //外圈2个圆
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(0xBF3F6AB5);
        mPaint.setStrokeWidth(4 * mDensityDpi);
        canvas.drawCircle(pointX, pointY, raduis, mPaint);
        mPaint.setStrokeWidth(3 * mDensityDpi);
        canvas.drawCircle(pointX, pointY, raduis - 10 * mDensityDpi, mPaint);

        //内圈2个圆
        mPaint.setStrokeWidth(5 * mDensityDpi);
        mPaint.setColor(0xE73F51B5);
        canvas.drawCircle(pointX, pointY, raduis / 2, mPaint);
        mPaint.setColor(0x7E3F51B5);
        canvas.drawCircle(pointX, pointY, raduis / 2 + 5 * mDensityDpi, mPaint);
        mPaint.setStrokeWidth(3 * mDensityDpi);

    }

    /**
     * 绘制速度区域扇形
     */
    private void drawSpeedArea(Canvas canvas) {
        int degree;
        if (speed < 210) {
            degree = speed * 36 / 30;
        } else {
            degree = 210 * 36 / 30;
        }

        canvas.drawArc(speedRectF, 144, degree, true, speedAreaPaint);

        // TODO: 2016/5/12
        //不显示中间的内圈的扇形区域
        mPaint.setColor(0xFF343434);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawArc(speedRectFInner, 144, degree, true, mPaint);
        mPaint.setStyle(Paint.Style.STROKE);
    }


    /**
     * 绘制刻度
     */
    private void drawScale(Canvas canvas) {
        for (int i = 0; i < 60; i++) {
            if (i % 6 == 0) {
                canvas.drawLine(pointX - raduis + 10 * mDensityDpi, pointY, pointX - raduis + 50 * mDensityDpi, pointY, mPaint);
            } else {
                canvas.drawLine(pointX - raduis + 10 * mDensityDpi, pointY, pointX - raduis + 30 * mDensityDpi, pointY, mPaint);
            }
            canvas.rotate(6, pointX, pointY);
        }
    }

    /**
     * 绘制速度标识文字
     */
    private void drawText(Canvas canvas) {
//        canvas.save();
//        canvas.rotate(-36, pointX, pointY);
        for (int i = 0; i < 8; i++) {
            int value = 30 * i;
            String TEXT = String.valueOf(value);
            switch (value) {
                case 0:
                    // 计算Baseline绘制的起点X轴坐标
                    baseX = (int) (pointX - sRaduis * Math.cos(Math.PI / 5) + textPaint.measureText(TEXT) / 2 + textScale / 2);
                    // 计算Baseline绘制的Y坐标
                    baseY = (int) (pointY + sRaduis * Math.sin(Math.PI / 5) + textScale / 2);
                    break;
                case 30:
                    baseX = (int) (pointX - raduis + 50 * mDensityDpi + textPaint.measureText(TEXT) / 2);
                    baseY = (int) (pointY + textScale);
                    break;
                case 60:
                    baseX = (int) (pointX - sRaduis * Math.cos(Math.PI / 5) + textScale);
                    baseY = (int) (pointY - sRaduis * Math.sin(Math.PI / 5) + textScale * 2);
                    break;
                case 90:
                    baseX = (int) (pointX - sRaduis * Math.cos(2 * Math.PI / 5) - textScale / 2);
                    baseY = (int) (pointY - sRaduis * Math.sin(2 * Math.PI / 5) + 2 * textScale);
                    break;
                case 120:
                    baseX = (int) (pointX + sRaduis * Math.sin(Math.PI / 10) - textPaint.measureText(TEXT) / 2);
                    baseY = (int) (pointY - sRaduis * Math.cos(Math.PI / 10) + 2 * textScale);
                    break;
                case 150:
                    baseX = (int) (pointX + sRaduis * Math.cos(Math.PI / 5) - textPaint.measureText(TEXT) - textScale / 2);
                    baseY = (int) (pointY - sRaduis * Math.sin(Math.PI / 5) + textScale * 2);
                    break;
                case 180:
                    baseX = (int) (pointX + sRaduis - textPaint.measureText(TEXT) - textScale / 2);
                    baseY = (int) (pointY + textScale);
                    break;
                case 210:
                    baseX = (int) (pointX + sRaduis * Math.cos(Math.PI / 5) - textPaint.measureText(TEXT) - textScale / 2);
                    baseY = (int) (pointY + sRaduis * Math.sin(Math.PI / 5) - textScale / 2);
                    break;
            }
//            baseX = (int) (pointX - raduis + 50 * mDensityDpi + textPaint.measureText(TEXT) / 2);
//            baseY = (int) (pointY + textScale);
            canvas.drawText(TEXT, baseX, baseY, textPaint);
//            canvas.rotate(36, pointX, pointY);
        }
//        canvas.restore();
    }

    /**
     * 绘制中间文字内容
     */
    private void drawCenter(Canvas canvas) {
        //速度
        textPaint.setTextSize(60 * mDensityDpi);
        float tw = textPaint.measureText(String.valueOf(speed));
        baseX = (int) (pointX - tw / 2);
        baseY = (int) (pointY + Math.abs(textPaint.descent() + textPaint.ascent()) / 4);
        canvas.drawText(String.valueOf(speed), baseX, baseY, textPaint);

        //单位
        textPaint.setTextSize(20 * mDensityDpi);
        tw = textPaint.measureText("km/h");
        baseX = (int) (pointX - tw / 2);
        baseY = (int) (pointY + raduis / 4 + Math.abs(textPaint.descent() + textPaint.ascent()) / 4);
        canvas.drawText("km/h", baseX, baseY, textPaint);
    }


    @Override
    public void run() {
        int speedChange;
        while (start) {
            switch (type) {
                case 1://油门
                    speedChange = 3;
                    break;
                case 2://刹车
                    speedChange = -5;
                    break;
                case 3://手刹
                    speed = 0;
                default:
                    speedChange = -1;
                    break;
            }
            speed += speedChange;
            if (speed < 1) {
                speed = 0;
            }
            try {
                Thread.sleep(50);
                setSpeed(speed);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}

调用:

@Override
protected void onResume() {
    super.onResume();
    if (speedControlView != null) {
        speedControlView.setSpeed(0);
        speedControlView.setStart(true);
    }
    new Thread(speedControlView).start();
}

@Override
protected void onStop() {
    super.onStop();
    if (speedControlView != null) {
        speedControlView.setSpeed(0);
        speedControlView.setStart(false);
    }
}

三、自定义View,画线、矩形、圆角矩形、圆形

com.cctvjiatao.customview.MainActivity 同一

AndroidManifest.xml 同一

activity_main.xml

xmlns:tools=””

android:id=”@+id/container”

android:layout_width=”match_parent”

android:layout_height=”match_parent”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:background=”#00ff00″

android:visibility=”gone”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:visibility=”gone”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”>

com.cctvjiatao.customview.v2.CustomView2

packagecom.cctvjiatao.customview.v2;

importandroid.content.Context;

importandroid.graphics.Canvas;

importandroid.graphics.Paint;

importandroid.graphics.Rect;

importandroid.graphics.RectF;

importandroid.graphics.Paint.Style;

importandroid.util.AttributeSet;

importandroid.view.View;

/**

* @作者: 麦典威

* @包名:com.cctvjiatao.customview.v2

* @文件名:CustomView2.java

* @功能: 自定义View,画线、矩形、圆角矩形、圆形

*/

publicclassCustomView2extendsView {

publicCustomView2(Context context, AttributeSet attrs) {

super(context, attrs);

}

publicCustomView2(Context context) {

super(context);

}

@Override

protectedvoidonDraw(Canvas canvas) {

Paint paint =newPaint();

paint.setTextSize(10);

paint.setColor(0xffff0000);

// 画直线

canvas.drawLine(0,10,200,10, paint);

// 画斜线

canvas.drawLine(0,10,200,60, paint);

// 画矩形(Rect)

Rect rect =newRect(0,80,100,160);

canvas.drawRect(rect, paint);

// 画矩形(RectF)

RectF rectf =newRectF(150,80,250,160);

canvas.drawRect(rectf, paint);

// 画矩形(坐标)

canvas.drawRect(300,80,400,160, paint);

// 画圆角矩形(RectF)

RectF rectrf =newRectF(10,180,110,250);

canvas.drawRoundRect(rectrf,10,10, paint);

// 画圆角矩形(RectF)

canvas.drawRoundRect(120,180,220,250,10,10, paint);

// 画圆形

canvas.drawCircle(100,350,50, paint);

paint.setStyle(Style.STROKE);

canvas.drawCircle(210,350,50, paint);

paint.setStyle(Style.FILL_AND_STROKE);

canvas.drawCircle(320,350,50, paint);

paint.setStyle(Style.FILL);

canvas.drawCircle(430,350,50, paint);

}

}

效果图暂略。

实现细节

  1. 山 叶子 太阳 云 全部用Path Draw出来 没有使用一张图片
  2. 动画采用 PathMeasure+ObjectAnimator实现

1.绘制一个实心的圆做仪表盘背景。

图片 4预览

参考资料

  • 自定义View实战(一)
    汽车速度仪表盘

四、自定义View,画图像

com.cctvjiatao.customview.MainActivity 同一

AndroidManifest.xml 同一

activity_main.xml

xmlns:tools=””

android:id=”@+id/container”

android:layout_width=”match_parent”

android:layout_height=”match_parent”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:background=”#00ff00″

android:visibility=”gone”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:visibility=”gone”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”

android:visibility=”gone”>

android:layout_width=”match_parent”

android:layout_height=”match_parent”>

com.cctvjiatao.customview.v2.CustomView3

packagecom.cctvjiatao.customview.v2;

importcom.cctvjiatao.customview.R;

importandroid.content.Context;

importandroid.graphics.Bitmap;

importandroid.graphics.BitmapFactory;

importandroid.graphics.Canvas;

importandroid.graphics.Paint;

importandroid.util.AttributeSet;

importandroid.view.View;

/**

* @作者: 麦典威

* @包名:com.cctvjiatao.customview.v2

* @文件名:CustomView3.java

* @功能: 自定义View,画图像

*/

publicclassCustomView3extendsView {

privateBitmap bitmap;

publicCustomView3(Context context, AttributeSet attrs) {

super(context, attrs);

bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);

}

publicCustomView3(Context context) {

super(context);

bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);

}

@Override

protectedvoidonDraw(Canvas canvas) {

Paint paint =newPaint();

canvas.drawBitmap(bitmap,0,35, paint);

}

}

效果图暂略

贝塞尔曲线

  1. 知识点(我反正是没怎么看懂):

贝塞尔曲线高级教程
贝塞尔曲线

  1. 工具使用:
  • 对贝塞尔曲线那些数学公式是一脸懵圈的状态 天然性的抵触
    包括很多人也是
    所以就有人想出其他办法来轻松实现贝塞尔曲线

设计师教你怎么用最偷懒的方式画贝塞尔曲线
贝塞尔曲线在线绘制工具

  • 使用AI绘制

教你如何使用钢笔工具

以上面的云朵为例子:

先用钢笔画出云朵的锥型

图片 5

润角(或者做角度调整)

图片 6

打开标尺和网格 做坐标记录

图片 7

估坐标值 并记录

图片 8

上图需要记录2组坐标值:所有锚点坐标值 所有手柄坐标值 因为在Path
api中需要这些数据 若对Path api不太熟悉的可以去了解这篇文章:

Android自定义View之Path解析

//二次贝塞尔曲线(x1,y1)代表控制点 也就是上图的手柄坐标(x2,y2)代表结束点 也就是锚点坐标点
public void quadTo(float x1, float y1, float x2, float y2)
//三次贝塞尔曲线 (x1,y1)(x2,y2)代表控制点 也就是上图的手柄坐标(x3,y3)表结束点 也就是锚点坐标点
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

通过上面记录的坐标值进行 绘制

.......
private void init() {
        .......
        for(int i=0;i<points.length;i++){
            points[i]=new PointF();
        }
        for(int i=0;i<controls.length;i++){
            controls[i]=new PointF();
        }
        points[0].set(18.5f,47.5f);
        points[1].set(39.5f,43.5f);
        points[2].set(67.5f,42.5f);
        ......
        controls[0].set(27,35);
        controls[1].set(34,34);
        controls[2].set(48,27);
        controls[3].set(59,27);
        ........
}
 @Override
    protected void onDraw(Canvas canvas) {
        Path path=new Path();
        path.moveTo(points[0].x,points[0].y*mPercentY);
        path.lineTo(points[1].x,points[1].y*mPercentY);
path.quadTo(controls[0].x*mPercentX,controls[0].y*mPercentY,points[2].x,points[2].y*mPercentY);
path.quadTo(controls[1].x*mPercentX,controls[1].y*mPercentY,points[3].x,points[3].y*mPercentY);
        path.lineTo(points[4].x,points[4].y*mPercentY);
        path.lineTo(points[0].x,points[0].y*mPercentY);
        canvas.drawPath(path,paint);
        }

叶子漂移动画是 PathMeasure+ObjectAnimator实现的

  • 首先叶子的漂移轨迹也是贝塞尔曲线 可以通过AI制作好路线(当然
    你想怎么飘就怎么飘)

图片 9

  • 记录好 所有锚点坐标值 所有手柄坐标值 使用Path绘制贝塞尔曲线

        Path path = new Path();
        path.moveTo(mData[0].x, mData[0].y * mScaleH);
        path.quadTo(mCtrl[0].x * mScaleW, mCtrl[0].y * mScaleH, mData[1].x * mScaleW, mData[1].y * mScaleH);
        path.cubicTo(mCtrl[1].x * mScaleW, mCtrl[1].y * mScaleH, mCtrl[2].x * mScaleW, mCtrl[2].y * mScaleH,
                mData[2].x * mScaleW, mData[2].y * mScaleH);
        path.cubicTo(mCtrl[3].x * mScaleW, mCtrl[3].y * mScaleH, mCtrl[4].x * mScaleW, mCtrl[4].y * mScaleH, mData[3]
                .x * mScaleW, mData[3].y * mScaleH);
        path.quadTo(mCtrl[5].x * mScaleW, mCtrl[5].y * mScaleH, mData[4].x * mScaleW, mData[4].y * mScaleH);
  • 开启动画

final PathMeasure measure = new PathMeasure(path, false);
        final float[] pointF = new float[2];
        ValueAnimator viewAnimator = ValueAnimator.ofFloat(0, measure.getLength());
        viewAnimator.setDuration(duration);
        viewAnimator.setStartDelay(startDelay);
        viewAnimator.setRepeatCount(ValueAnimator.INFINITE);
        viewAnimator.setRepeatMode(ValueAnimator.INFINITE);
        viewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                // 获取当前点坐标封装到pointF
                measure.getPosTan(value, pointF, null);
                view.setX(pointF[0]);
                view.setTranslationY(pointF[1]);
            }
        });

PathMeasure.getLength(value, pointF, null)
通过属性动画的animation.getAnimatedValue();值去获取贝塞尔曲线具体某一点的坐标
封装到float[] pointF = new float[2];数组中
最后通过view.setTranslationY(pointF[1]);的方式去显示View的动画效果

 mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(0xFF343434); canvas.drawCircle(pointX, pointY, raduis, mPaint);

需求简要说明

  1. 默认状态为0星,不可提交
  2. 星星数量小于等于3,展示差评理由
  3. 差评理由云控,数量可变
  4. 差评理由可不选,可多选

最后

对于如何使用Path去绘制贝塞尔曲线或者如何去判断是使用二次贝塞尔曲线还是三次贝塞尔曲线这些都需要自己去实际经验中去不断的总结,多敲代码,一切就都明朗起来。

2.绘制外面的两个圆环 和 里面的 两个圆环。

我将分为3部分进行介绍。

 //外圈2个圆 mPaint.setStyle(Paint.Style.STROKE); mPaint.setColor(0xBF3F6AB5); mPaint.setStrokeWidth(4 * mDensityDpi); canvas.drawCircle(pointX, pointY, raduis, mPaint); mPaint.setStrokeWidth(3 * mDensityDpi); canvas.drawCircle(pointX, pointY, raduis - 10 * mDensityDpi, mPaint); //内圈2个圆 mPaint.setStrokeWidth(5 * mDensityDpi); mPaint.setColor(0xE73F51B5); canvas.drawCircle(pointX, pointY, raduis / 2, mPaint); mPaint.setColor(0x7E3F51B5); canvas.drawCircle(pointX, pointY, raduis / 2 + 5 * mDensityDpi, mPaint); mPaint.setStrokeWidth(3 * mDensityDpi);

图片 10介绍

3.绘制仪表盘的刻度。

  1. 评级的 RatingBar
  2. 差评理由 TagView
  3. 整体评价的 CardView
/** * 绘制刻度 */private void drawScale(Canvas canvas) { for (int i = 0; i < 60; i++) { if (i % 6 == 0) { canvas.drawLine(pointX - raduis + 10 * mDensityDpi, pointY, pointX - raduis + 50 * mDensityDpi, pointY, mPaint); } else { canvas.drawLine(pointX - raduis + 10 * mDensityDpi, pointY, pointX - raduis + 30 * mDensityDpi, pointY, mPaint); } canvas.rotate(6, pointX, pointY); }}

Android 原生就有这个空间
RatingBar,定制型不是很高,所以需要通过自定义来满足特定的产品需求。其实
RatingBar的主要用处就在于
评级,基本就是对服务进行等级评价,来决定服务的质量如何。

4.绘制仪表盘的速度标识和中间的速度 和 单位
文字。(这里有好的处理方法请留言)

有需求才会有对应的实现,那么有哪些需要控制的属性呢。

/** * 绘制速度标识文字 */private void drawText(Canvas canvas, int value) { String TEXT = String.valueOf; switch  { case 0: // 计算Baseline绘制的起点X轴坐标 baseX =  (pointX - sRaduis * Math.cos(Math.PI / 5) + textPaint.measureText / 2 + textScale / 2); // 计算Baseline绘制的Y坐标 baseY =  (pointY + sRaduis * Math.sin(Math.PI / 5) + textScale / 2); break; case 30: baseX =  (pointX - raduis + 50 * mDensityDpi + textPaint.measureText / 2); baseY =  (pointY + textScale); break; case 60: baseX =  (pointX - sRaduis * Math.cos(Math.PI / 5) + textScale); baseY =  (pointY - sRaduis * Math.sin(Math.PI / 5) + textScale * 2); break; case 90: baseX =  (pointX - sRaduis * Math.cos(2 * Math.PI / 5) - textScale / 2); baseY =  (pointY - sRaduis * Math.sin(2 * Math.PI / 5) + 2 * textScale); break; case 120: baseX =  (pointX + sRaduis * Math.sin(Math.PI / 10) - textPaint.measureText / 2); baseY =  (pointY - sRaduis * Math.cos(Math.PI / 10) + 2 * textScale); break; case 150: baseX =  (pointX + sRaduis * Math.cos(Math.PI / 5) - textPaint.measureText - textScale / 2); baseY =  (pointY - sRaduis * Math.sin(Math.PI / 5) + textScale * 2); break; case 180: baseX =  (pointX + sRaduis - textPaint.measureText - textScale / 2); baseY =  (pointY + textScale); break; case 210: baseX =  (pointX + sRaduis * Math.cos(Math.PI / 5) - textPaint.measureText - textScale / 2); baseY =  (pointY + sRaduis * Math.sin(Math.PI / 5) - textScale / 2); break; } canvas.drawText(TEXT, baseX, baseY, textPaint);}/** * 绘制中间文字内容 */private void drawCenter(Canvas canvas) { //速度 textPaint.setTextSize(60 * mDensityDpi); float tw = textPaint.measureText(String.valueOf; baseX =  (pointX - tw / 2); baseY =  (pointY + Math.abs(textPaint.descent() + textPaint.ascent; canvas.drawText(String.valueOf, baseX, baseY, textPaint); //单位 textPaint.setTextSize(20 * mDensityDpi); tw = textPaint.measureText; baseX =  (pointX - tw / 2); baseY =  (pointY + raduis / 4 + Math.abs(textPaint.descent() + textPaint.ascent; canvas.drawText("km/h", baseX, baseY, textPaint);}
属性名称 属性介绍
mStarTotal 评级的总数
mSelectedCount 评级选中的数量
mStarResId 星星的资源文件
mHeight 星星的高度
mIntervalWidth 星星之间间隔的宽度
mEditable 是否可被点击

5.绘制速度范围的扇形区域。

既然星星有两种状态可供选择,那么单个 View 就使用 CheckBox
代替,首先初始化的时候,需要根据 mStarTotal 来控制添加多少个
CheckBox ,并根据 mHeight 高度和 mIntervalWidth
间隔来控制摆放的位置。

/** * 绘制速度区域扇形 */private void drawSpeedArea(Canvas canvas) { int degree; if (speed < 210) { degree = speed * 36 / 30; } else { degree = 210 * 36 / 30; } canvas.drawArc(speedRectF, 144, degree, true, speedAreaPaint); // TODO: 2016/5/12 //不显示中间的内圈的扇形区域 mPaint.setColor(0xFF343434); mPaint.setStyle(Paint.Style.FILL); canvas.drawArc(speedRectFInner, 144, degree, true, mPaint); mPaint.setStyle(Paint.Style.STROKE);}
for (int i = 0; i < mStarTotal; i++) { CheckBox cb = new CheckBox(getContext; LayoutParams layoutParams; if (mHeight == 0) { layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } else { layoutParams = new LayoutParams mHeight,  mHeight); } layoutParams.gravity = Gravity.CENTER_VERTICAL; if (i != 0 && i != mStarTotal - 1) { layoutParams.leftMargin =  mIntervalWidth; layoutParams.rightMargin =  mIntervalWidth; } else if  { layoutParams.rightMargin =  mIntervalWidth; } else if (i == mStarTotal - 1) { layoutParams.leftMargin =  mIntervalWidth; } addView(cb, layoutParams);}

6.实现点击让 速度动起来。实现runnable 接口。

最后在父布局 LinearLayout 中添加 所有的 CheckBox

@Overridepublic void run() { int speedChange; while  { switch  { case 1://油门 speedChange = 3; break; case 2://刹车 speedChange = -5; break; case 3://手刹 speed = 0; default: speedChange = -1; break; } speed += speedChange; if (speed < 1) { speed = 0; } try { Thread.sleep; setSpeed; } catch (InterruptedException e) { e.printStackTrace(); break; } }}

至于点击事件的回调,可以在每次点击的时候进行遍历,获取 CheckBox
的选中状态,并通过 callback 回调出来。

在activity中启动线程,设置监听

for (int i = 0; i < mStarTotal; i++) { CheckBox cb =  getChildAt; if (i <= position) { cb.setChecked; } else if (i > position) { cb.setChecked; }}if (mOnRatingChangeListener != null) { mOnRatingChangeListener.onChange(mSelectedCount);}
 //设置监听 speedUp.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction { case MotionEvent.ACTION_DOWN: //按下的时候加速 speedControlView.setType; break; case MotionEvent.ACTION_UP: //松开做自然减速 speedControlView.setType; break; } return true; } }); speedDown.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction { case MotionEvent.ACTION_DOWN: //按下的时候减速 speedControlView.setType; break; case MotionEvent.ACTION_UP: //松开做自然减速 speedControlView.setType; break; } return true; } }); shutDown.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction { case MotionEvent.ACTION_DOWN: //按下的时候拉手刹 speedControlView.setType; break; case MotionEvent.ACTION_UP: //松开做自然减速 speedControlView.setType; break; } return true; } });@Overrideprotected void onResume() { super.onResume(); if (speedControlView != null) { speedControlView.setSpeed; speedControlView.setStart; } new Thread(speedControlView).start();}

最后的效果:

补上速度的设置函数

图片 11EvaluationRatingBar

// 设置速度 并重绘视图public void setSpeed(int speed) { this.speed = speed; postInvalidate();}

当用户给出差评的时候,需要展示对应的差评理由选择。理由云控,数量可变,内容可变。可单选,可不选,可多选。

搞定.看,是不是很简单。如果你喜欢我写的文章,请关注我。我的博客:

主要的难点和重点在于根据理由内容的长短进行展示,如果内容长则显示一条,如果内容短可以显示多条。

项目地址:github:
觉得可以的话点下starcsdn:

我们都知道 View 的测量工作主要是在 onMeasure 里进行。
宽度计算,可以先测量出每个子 View
的宽度,每次叠加,如果超过父布局限制的宽度则换行。
高度计算,每次换行叠加高度,每一行的高度取子 View 高度的最大值。

//遍历每个子元素for (int i = 0, childCount = getChildCount(); i < childCount; i++) { View childView = getChildAt; //测量每一个子view的宽和高 measureChild(childView, widthMeasureSpec, heightMeasureSpec); //获取到测量的宽和高 int childWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); //因为子View可能设置margin,这里要加上margin的距离 MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams(); int realChildWidth = childWidth + mlp.leftMargin + mlp.rightMargin; int realChildHeight = childHeight + mlp.topMargin + mlp.bottomMargin; //如果当前一行的宽度加上要加入的子view的宽度大于父容器给的宽度,就换行 if ((lineWidth + realChildWidth) > sizeWidth) { //换行 resultWidth = Math.max(lineWidth, realChildWidth); resultHeight += realChildHeight; //换行了,lineWidth和lineHeight重新算 lineWidth = realChildWidth; lineHeight = realChildHeight; } else { //不换行,直接相加 lineWidth += realChildWidth; //每一行的高度取二者最大值 lineHeight = Math.max(lineHeight, realChildHeight); } //遍历到最后一个的时候,肯定走的是不换行 if (i == childCount - 1) { resultWidth = Math.max(lineWidth, resultWidth); resultHeight += lineHeight; }}setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : resultWidth,modeHeight == MeasureSpec.EXACTLY ? sizeHeight : resultHeight);

既然 宽高 计算完了,剩下就是子 View 的摆放了,自然是在在
onLayout() 中实现。摆放就比较简单了,同样需要遍历所有的子
View,最终调用 layout(left, top, right, bottom)
方法进行位置的摆放。宽度不断叠加,当超过父布局的宽度,则将 left 置为
0,高度记上一行子 View 的最大高度,以此类推。

@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) { int realWidht = getWidth(); int childLeft = 0; int childTop = 0; //遍历子控件,记录每个子view的位置 for (int i = 0, childCount = getChildCount(); i < childCount; i++) { View childView = getChildAt; //跳过View.GONE的子View if (childView.getVisibility() == View.GONE) { continue; } //获取到测量的宽和高 int childWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); //因为子View可能设置margin,这里要加上margin的距离 MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams(); if (childLeft + mlp.leftMargin + childWidth + mlp.rightMargin > realWidht) { //换行处理 childTop += (mlp.topMargin + childHeight + mlp.bottomMargin); childLeft = 0; } //布局 int left = childLeft + mlp.leftMargin; int top = childTop + mlp.topMargin; int right = childLeft + mlp.leftMargin + childWidth; int bottom = childTop + mlp.topMargin + childHeight; childView.layout(left, top, right, bottom); childLeft += (mlp.leftMargin + childWidth + mlp.rightMargin); }}

来看一下最终的效果:

图片 12reasonsLayout

这个就简单了,配合着 AlertDialog 弹窗显示,将之前介绍的
EvaluationRatingBarEvaluationNegReasonsLayout
结合在一块,并根据自己特殊的产品需求来定制对应的效果。最后在点击提交的时候通过接口回调的方式,将最终的结果回调出来并处理。

public void setOnEvaluationCallback(OnEvaluationCallback callback) { this.mCallback = callback;}public interface OnEvaluationCallback { void onEvaluationCommitClick(int starCount, Set<String> reasons);}

starCount: 即为评级的等级。reasons:即为选择的差评理由

最终调用

EvaluationCardView cardView = new EvaluationCardView;List<String> reasonsData = new ArrayList<>();reasonsData.add;reasonsData.add;reasonsData.add;reasonsData.add("问题没有得到解决");cardView.setReasonsData(reasonsData);cardView.show();cardView.setOnEvaluationCallback(new EvaluationCardView.OnEvaluationCallback() { @Override public void onEvaluationCommitClick(int starCount, Set<String> reasons) { StringBuilder sb = new StringBuilder(); for (String reason : reasons) { sb.append.append; } Toasty.success(EvaluationCardViewActivity.this, "评价成功\n" + "星星数量:" + starCount + "\n差评理由:" + sb.toString(), Toast.LENGTH_LONG, true).show;

具体的实现代码请查看 EvaluationCardView。

FlowTag

原文地址

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图