自定义控件&动画

绘制

public class CircleView extends View {
    Paint mPaint;
    //代码中创建
    public CircleView(Context context) {
        super(context);
        initPaint();
    }
    //xml中创建
    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }
    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    void initPaint() {
        mPaint = new Paint();
        random = new Random();
        mPaint.setStrokeWidth(10);
    }

    private int step = 15;
    private Random random;

    /**
     * 绘制控件的几个主要的方法
     * canvas.drawXXX()
     * <p>
     * paint.setStyle(Style)
     * Paint.Style.STROKE 边界模式
     * Paint.Style.FILL 填充模式
     * Paint.Style.FILL_AND_STROKE 既填充又描边
     * <p>
     * paint.setColor(int color)
     * <p>
     * paint.setStrokeWidth()//设置描边的宽度
     * <p>
     * mPaint.setAntiAlias(boolean)设置是否抗锯齿,可以在new Paint(boolean)的时候传入
     * 抗锯齿的原理是:修改图形边缘处的像素颜色,从而让图形在肉眼看来具有更加平滑的感觉
     * <p>
     * paint.setTextSize() 设置文字的大小
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //        canvas.drawCircle(100,100,100,mPaint);

        //        mPaint.setColor(Color.RED);
        //        mPaint.setStyle(Paint.Style.STROKE);
        //        mPaint.setStrokeWidth(20);
        //        canvas.drawCircle(300,300,200,mPaint);

        //        Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_launcher);
        //        canvas.drawBitmap(bitmap,0,0,mPaint);


        //在整个区域绘制颜色
        //        canvas.drawARGB(255, 255, 0, 0);//范围为0-255
        //        canvas.drawRGB(255,0,0);
        //        canvas.drawColor(Color.parseColor("#88880000"));

        //绘制矩形,同样可以设置填充模式
        //        canvas.drawRect(100,100,300,400,mPaint);
        //        Rect rect = new Rect(100,100,300,400);
        //        canvas.drawRect(rect,mPaint);

        //绘制点
/*        mPaint.setColor(Color.RED);
        mPaint.setStrokeCap(Paint.Cap.SQUARE);//设置点的样式
        mPaint.setStrokeWidth(10);
        canvas.drawPoint(0,0,mPaint);

        for (int j = (int) getY(); j < getHeight(); j += step)
            for (int i = (int) getX(); i < getWidth(); i += step) {
                mPaint.setARGB(random.nextInt(255), random.nextInt(255), random.nextInt(255),
                        random.nextInt(255));
                canvas.drawPoint(i, j, mPaint);
            }
        postInvalidateDelayed(1000);*/
/*        mPaint.setStrokeWidth(10);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        float[] pts=new float[]{100,100,200,200};
        //批量画点
        canvas.drawPoints(pts,mPaint);*/

        //画椭圆,android 5.0后才能使用
        //        canvas.drawOval(100,0,200,400,mPaint);

        //画线
/*        mPaint.setStrokeWidth(10);
//        canvas.drawLine(100,100,600,600,mPaint);
        float[] points = {20, 20, 120, 20, 70, 20, 70, 120, 20, 120, 120, 120, 150, 20,
                250, 20, 150, 20, 150, 120, 250, 20, 250, 120, 150, 120, 250, 120};
        canvas.drawLines(points,mPaint);*/

        //圆角矩形,rx ry是圆的x轴半径和y轴半径
//        canvas.drawRoundRect(100, 100, 500, 300, 100, 100, mPaint);

        //弧形和扇形
/*        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        canvas.drawArc(0,0,200,300,-45,135,true,mPaint);//弧度以X轴整周
        canvas.drawArc(0,0,200,300,90,135,false,mPaint);//弧度以X轴整周
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawArc(0,0,200,300,225,45,true,mPaint);
        canvas.drawArc(0,0,200,300,-75,15,false,mPaint);*/

        drawPath(canvas);
    }

    /**
     * 通过描述路径的方式来描述直线、二次曲线、三次曲线、圆、椭圆、弧形、矩形、圆角矩形,最终实现复杂图形
     *
     */
    Path path;
    {
        path=new Path();
        path.moveTo(500,200);
        path.arcTo(300,0,500,200,-210,210,true);
        /**
         * 前四个参数 四条边的位置
         * 参数五 开始的角度 以x轴为正方向
         * 参数六 扫过的角度
         * 参数七 是否强制移动的该点
         *      true path执行后的点就在900,200
         *      false path的点有条线连过来,如果有空白的话
         */
        path.arcTo(500,0,700,200,-180,210,true);
        path.quadTo(650,250,500,345);
        path.quadTo(350,250,313,150);

        //三阶贝塞尔曲线,有两个控制点
//        path.cubicTo();

        //相对坐标
//        path.rLineTo();
    }
    void drawPath(Canvas canvas){
        canvas.drawPath(path,mPaint);
    }
        /**
     * Path方法分为两类
     * addXXX
     *  path2.addCircle(200,200,100, Path.Direction.CW);后一个参数描述路径的方法,在判断是否圆时有用CW(clockwise)顺时针,ccw(counter-clockwise)逆时针
     *  addOval(float left, float top, float right, float bottom, Direction dir)
     *  addRect(float left, float top, float right, float bottom, Direction dir) / addRect(RectF rect, Direction dir)
     *  addRoundRect(RectF rect, float rx, float ry, Direction dir)
     *  path2.addArc(100,200,300,400,-180,180);
     *
     *  XXXto
     *  path.arcTo(500,0,700,200,-180,210,true);//最后一个参数表示上一个点和弧线要不要断开
     *
     * path.close();封闭当前图形,如果是填充的话默认是封闭的
     *
     * 第二类是辅助计算类
     *
     *
     */ 
}

辅助计算类

        /**
         * 填充模式
         * winding:
         *  由平面内任意一点发出任意角度的射线,计算与path相较的次数,规定方向加1,反方向-1,最终次数!=0则判定为图形内部,=0判定为图片内部
         * Even_odd:
         *  由平面内任意一点发出任意角度的射线,计算与图形相交的次数(相切不算),奇数则判定为图形内部
         *
         * inverse_winding:全填充的反色
         * inverser_even_odd:交叉填充的反色
         */

这里写图片描述

填充模式

public class FillTypeView extends View {
    Paint mPaint;

    public FillTypeView(Context context) {
        super(context);
        initPaint();
    }

    public FillTypeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public FillTypeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    void initPaint() {
        mPaint = new Paint();
        mPaint.setStrokeWidth(10);
    }
    Path path=new Path();
    {
        path.addCircle(200,200,100, Path.Direction.CW);
        path.addCircle(250,200,100, Path.Direction.CCW);
        /**
         * 填充模式
         * winding:
         *  由平面内任意一点发出任意角度的射线,计算与path相较的次数,规定方向加1,反方向-1,最终次数!=0则判定为图形内部,=0判定为图片内部
         * Even_odd:
         *  由平面内任意一点发出任意角度的射线,计算与图形相交的次数(相切不算),奇数则判定为图形内部
         *
         * inverse_winding:全填充的反色
         * inverser_even_odd:交叉填充的反色
         */
        path.setFillType(Path.FillType.WINDING);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawPath(canvas);
    }
    void drawPath(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, mPaint);
    }
}

混合着色器与PorterDuff.Mode

//        mPaint.setShader(new LinearGradient(100,100,150,150, Color.RED,Color.YELLOW, Shader.TileMode.REPEAT));
//        mPaint.setShader(new RadialGradient(300,300,150,Color.BLACK,Color.RED, Shader.TileMode.CLAMP));
//        mPaint.setShader(new SweepGradient(300,300, Color.BLACK,Color.RED));
//        mPaint.setShader(new BitmapShader(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)
//                , Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
//        BitmapShader bs1 = new BitmapShader(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)
//                , Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//        BitmapShader bs2 = new BitmapShader(BitmapFactory.decodeResource(getResources(), android.R.drawable.btn_plus)
//                , Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//        SweepGradient sg = new SweepGradient(300, 300, Color.BLACK, Color.RED);
//            mPaint.setShader(new ComposeShader(bs1,sg, PorterDuff.Mode.DST_OVER));
//        canvas.drawCircle(300,300,2000,mPaint);

12
这里写图片描述

离屏缓冲

使用 Xfermode 来绘制的内容,除了注意使用离屏缓冲,还应该注意控制它的透明区域不要太小,要让它足够覆盖到要和它结合绘制的内容,否则得到的结果很可能不是你想要的。我用图片来具体说明一下:

这里写图片描述

Paint详解

public class PaintView extends View {
    Paint mPaint;

    public PaintView(Context context) {
        super(context);
        initPaint();
    }

    public PaintView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public PaintView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    void initPaint() {
        mPaint = new Paint();
        mPaint.setStrokeWidth(10);
    }

    /**
     * Paint的Api可分为四类:
     *
     * color:
     * setColor(int)
     * setARGB(int,int,int,int)
     * setShader(shader)
     *   shader中文翻译为着色器,优先级高于setcolor等,一般使用其四个子类
     *     linearGradient,线性渐变
     *     radialGradient,辐射渐变
     *     sweepGradient,渐变形成一个圆
     *     bitmapShader:把图片作为着色器
     *     composeShader:混合着色,把两个着色器一起使用,使用两个相同类型的着色器取消关闭硬件加速
     *   tilemode:
     *     clamp:延伸边界颜色
     *     repeat:重复
     *     mirror:镜像对称
     *   PorterDuff.Mode:有17种规则
     *
     * 效果
     *   setColorFilter(filter),颜色过滤,一般使用其三个子类
     *      lightingColorFilter,模拟简单的光照效果
     *          计算公式:
     *           R' = R * mul.R / 0xff + add.R
     *           G' = G * mul.G / 0xff + add.G
     *           B' = B * mul.B / 0xff + add.B
     *      porterDuffColorFilter:将一个指定颜色和一种指定的PorterDuff.Mode来绘制颜色
     *      ColorMatrixColorFilter:通过传入一个颜色矩阵对象来计算并改变颜色
     *              ColorMatrix colorMatrix = new ColorMatrix();
     *                  本质是一个4*5矩阵
     *                  [ a, b, c, d, e,
     *                    f, g, h, i, j,
     *                    k, l, m, n, o,
     *                    p, q, r, s, t ]
     *                  计算公式:
     *                  R’ = a*R + b*G + c*B + d*A + e;
     *                  G’ = f*R + g*G + h*B + i*A + j;
     *                  B’ = k*R + l*G + m*B + n*A + o;
     *                  A’ = p*R + q*G + r*B + s*A + t;
     *                  colorMatrix.setSaturation();//设置饱和读
     *                  colorMatrix.setRotate();
     *                  colorMatrix.setScale();
     *                  //https://github.com/chengdazhi/StyleImageView第三方库
     *
     * setXfermode():"Xfermode" 其实就是 "Transfer mode",用 "X" 来代替 "Trans" 是一些美国人喜欢用的简写方式。
     *      严谨地讲, Xfermode 指的是你要绘制的内容和 Canvas 的目标位置的内容应该怎样结合计算出最终的颜色
     *      注意:需要使用离屏缓冲,否则周围一片黑
     *          1.canvas.saveLayer();setXfer;setXfer(null);canvas.restoreToCount();
     *          2.View.setLayerType(int);LAYER_TYPE_HARDWARE,使用GPU;LAYER_TYPE_SOFTWARE,使用一个bitmap来缓冲
     *
     * 设置线条
     *      setStrokeWidth(float)
     *      setStrokeCap(Paint.Cap)设置线头的相撞
     *          Butt平头
     *          Round圆头
     *          Squre方头
     *      setStrokeJoin()设置角的形状
     *          miter:尖角
     *          bevel:平角
     *          round:圆角
     *      setStrokeMiter(float),当设置为尖角时,角的延长线的补充方式
     *          默认值为4,大约29度的角,大约这个角度的角会被保留,小的则设置为平头
     *          公式为miter=a/b,
     *
     * 色彩优化:
     *      setDither(boolean)设置抖动
     *      setFilterBitmap(boolean)是否使用双线性过滤来绘制bitmap
     *
     * 设置path效果
     *      setPathEffect,对canvas所有的图形也有效
     *          cornerPathEffect,把所有的拐角变成圆角
     *          DiscretePathEffect,线条进行随机的偏离,手抖效果
     *          DashPathEffect,虚线
     *          PathDashPathEffect,用一个path(形状,如三角形)来绘制虚线
     *          SumPathEffect,组合效果
     *          ComposePathEffect:是先对目标 Path 使用一个 PathEffect,然后再对这个改变后的 Path 使用另一个 PathEffect。
     *              Canvas.drawLine() 和 Canvas.drawLines() 方法画直线时,setPathEffect() 是不支持硬件加速的;
     *              PathDashPathEffect 对硬件加速的支持也有问题,所以当使用 PathDashPathEffect 的时候,最好也把硬件加速关了。
     *  阴影效果
     *      setShadowLayer(int radius,int,int(偏移量),int color);在下面绘制效果
     *          clearShadowLayer()
     *          在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影。
     *          如果 shadowColor 是半透明的,阴影的透明度就使用 shadowColor 自己的透明度;而如果 shadowColor 是不透明的,阴影的透明度就使用 paint 的透明度
     *      setMaskFilter();在上方绘制效果
     *          BlurMaskFilter,模糊效果
     *              normal:内外都绘制
     *              solid:内部正常绘制,外部模糊
     *              inner:内部模糊,外部不绘制
     *              outer:外部模糊,内部不绘制
     *
     *          EmbossMaskFilter,浮雕效果
     * 获取绘制的path
     *      getFillpath(srcpath,dstpath)
     *      getTextPath(String,intstart,intend,)
     *
     *
     * 初始化
     *      reset(),将所有值复位为默认值
     *      set(Paint),复制所有属性
     *      setFlags(int),批量设置所有Flags,相当于依次调用他们的set方法
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
//        mPaint.setShader(new LinearGradient(100,100,150,150, Color.RED,Color.YELLOW, Shader.TileMode.REPEAT));
//        mPaint.setShader(new RadialGradient(300,300,150,Color.BLACK,Color.RED, Shader.TileMode.CLAMP));
//        mPaint.setShader(new SweepGradient(300,300, Color.BLACK,Color.RED));
//        mPaint.setShader(new BitmapShader(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)
//                , Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
//        BitmapShader bs1 = new BitmapShader(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)
//                , Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//        BitmapShader bs2 = new BitmapShader(BitmapFactory.decodeResource(getResources(), android.R.drawable.btn_plus)
//                , Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//        SweepGradient sg = new SweepGradient(300, 300, Color.BLACK, Color.RED);
//            mPaint.setShader(new ComposeShader(bs1,sg, PorterDuff.Mode.DST_OVER));
//        canvas.drawCircle(300,300,2000,mPaint);


        //屏蔽红色,增强绿色
//        LightingColorFilter colorFilter = new LightingColorFilter(0x00ffff, 0x003000);
//        PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
//                mPaint.setColorFilter(colorFilter);

//        ColorMatrix colorMatrix = new ColorMatrix();
//        colorMatrix.setSaturation();//设置饱和读
//        colorMatrix.setRotate();
//        colorMatrix.setScale();
//        ColorMatrixColorFilter colorFilter = new ColorMatrixColorFilter();
//        canvas.drawCircle(300,300,1000,mPaint);

        //离屏缓冲
//        int flag = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
//        canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher),0,0,mPaint);
//        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
//        canvas.drawBitmap(BitmapFactory.decodeResource(getResources(),android.R.drawable.checkbox_on_background),0,0,mPaint);
//        mPaint.setXfermode(null);
//        canvas.restoreToCount(flag);

        /*Path path = new Path();
        path.rLineTo(100,100);
        path.rLineTo(0,-50);

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.MITER);
        mPaint.setStrokeWidth(10);

        Path path2 = new Path();
        path2.addCircle(10,10,5, Path.Direction.CW);

        *//*athDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中
        , shape 参数是用来绘制的 Path ; advance 是两个相邻的 shape 段之间的间隔,不过注意,这个间隔是两个 shape 段的起点的间隔
        ,而不是前一个的终点和后一个的起点的距离; phase 和 DashPathEffect 中一样,是虚线的偏移;
        最后一个参数 style,是用来指定拐弯改变的时候 shape 的转换方式。style 的类型为 PathDashPathEffect.Style ,
        是一个 enum ,具体有三个值:
        TRANSLATE:位移
        ROTATE:旋转
        MORPH:变体*//*
        PathDashPathEffect pathDashPathEffect = new PathDashPathEffect(path2, 10, 10, PathDashPathEffect.Style.TRANSLATE);
        mPaint.setPathEffect(pathDashPathEffect);
        canvas.drawPath(path,mPaint);*/

//        mPaint.setShadowLayer(10,0,0, Color.RED);

//        mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
        /**
         * float[] 三个元素的数组,指定了光源的方向
         * ambient 环境光的强度 0-1
         * specular炫光的系数
         * blurRadius应用光的范围
         */
//        mPaint.setMaskFilter(new EmbossMaskFilter())
        mPaint.setTextSize(56);
        canvas.drawText("你好啊",100,100,mPaint);
    }
}

getFillPath

        setLayerType(View.LAYER_TYPE_SOFTWARE, null);//经过测试需要关闭硬件加速

        String text = "这是一条正常的文本";
        Paint srcPaint = new Paint();
        srcPaint.setTextSize(100);
        srcPaint.setColor(Color.BLACK);
        srcPaint.setStyle(Paint.Style.FILL);
//        canvas.drawText(text,50,100,srcPaint);
        //获取文本路径
        canvas.translate(0,150);
        Path desPath = new Path();
        Paint desPaint = new Paint();
        desPaint.setColor(Color.BLACK);
        desPaint.setStyle(Paint.Style.STROKE);
        srcPaint.getTextPath(text,0,text.length(),50,100,desPath);
//        this.paint.getTextPath("你好啊",0,3,0,0,dest);        
        canvas.drawPath(desPath,desPaint);

文字的绘制

public class DrawTextView extends View {
    Paint mPaint;

    public DrawTextView(Context context) {
        super(context);
        initPaint();
    }

    public DrawTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public DrawTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    void initPaint() {
        mPaint = new Paint();
        mPaint.setStrokeWidth(10);
    }

    /**
     * Canvas绘制文字的方式有三种
     * 1.drawText(str,x,y,paint);以文字的基线为坐标原点
     *
     * 2.drawTextRun(),
     *      根据上下文绘制文字,对我们没用
     *      设置文字方向
     *
     * 3.drawTextOnPath()根据路径绘制文字,记得拐角要是要用圆角,否则很难看
     *
     * 4.StaticLayout,因为DrawText不能换行,也不能使用转移字符
     *      staticlayout不是view和viewgroup的子类
     */
    /**
     * Paint绘制文字类
     *      setTextSize)(
     *      setTypeface()//修改字体
     *      setFakeBoldText()//是否使用伪粗体
     *      setStrikeThruText()//是否添加删除线
     *      setUnderlinetext()是否添加下划线
     *      setTextSkewX()设置文字倾斜角度
     *      setTextScale设置横向缩放
     *      setLetterSpacing()设置文字间距
     *      setFontFeatureSettings(),通过css来设置文字样式
     *      setTextAlign(paint.align)设置文字的对齐方式
     *      setTextLocale(Locale)设置文字的国家
     *      setHintint(boolean)是否开启字体微调,开了效果好一些
     *      setElegantTextHeight(boolean)是否调整过高的字符,对中国人来说没用的属性
     *      setSubpixelText(boolean)是否开启像素级的抗锯齿
     *      setLinerText()不知道
     *
     *  Paint测量文字类
     *      getFontSpacing(),获取推荐的行距,作用是可以在换行的时候给y值来进行下移
     *      getFontMetrics(),获取文字权值
     *          ascent:
     *          descent:
     *          top:
     *          bottom:
     *          baseline
     *      getTextBound()获取文字的显示范围
     *      float measureText(string)测量文字宽度
     *      getWidths(str,float[])获取字符串中每个字符的宽度,并把结果填入参数 widths。
     *      int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)
     *          ,breakText() 是在给出宽度上限的前提下测量文字的宽度。如果文字的宽度超出了上限,那么在临近超限的位置截断文字。
     *
     *  光标相关:
     *      getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)
     *          对于一段文字,计算出某个字符处光标的 x 坐标。 start end 是文字的起始和结束坐标;contextStart contextEnd 是上下文的起始和结束坐标;isRtl 是文字的方向;offset 是字数的偏移,即计算第几个字符处的光标
     *      getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)
     *          给出一个位置的像素值,计算出文字中最接近这个位置的字符偏移量(即第几个字符最接近这个坐标)。
     *      检查指定的字符串中是否是一个单独的字形 (glyph);
     *          "a"true "ab"false "\uD83c"true
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

//        canvas.drawText("你好啊", 100, 150, mPaint);
//        canvas.drawText("你好啊", 100, 150 + mPaint.getFontSpacing(), mPaint);
//        canvas.drawText("你好啊", 100, 150 + mPaint.getFontSpacing ()* 2, mPaint);
    }
}

图片的裁剪

public class ClipView extends View {
    Paint mPaint;

    public ClipView(Context context) {
        super(context);
        initPaint();
    }

    public ClipView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public ClipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    void initPaint() {
        mPaint = new Paint();
        mPaint.setStrokeWidth(10);
    }

    /**
     * 裁剪绘制
     *      ClipXXX
     *      ClipPath
     *
     * 几何变换
     *      Canvas.translate(float dx, float dy) 平移
     *      Canvas.rotate(float degrees, float px, float py) 旋转
     *      Canvas.scale(float sx, float sy, float px, float py) 放缩
     *      skew(float sx, float sy) 错切
     *
     *      Matrix进行变换,然后应用到Canvas中
     *          canvas.concat(matrix),关联,原来的矩阵和关联的矩阵相乘
     *          canvas.setMatrix(matrix),设置,新的矩阵取代原来的矩阵
     *          canvas的变换方法,matrix都有
     *          matrix.setPolyToPoly()点对点的方式进行映射,意思把指定的点从原来的点移动到新的点,从而发生形变
     *
     *      使用Camera进行三维变换
     *          camera.rotate()
     *
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
        canvas.save();

//        canvas.clipRect(100,100,300,300);

/*        Path path = new Path();
        2path.addCircle(200,200,200, Path.Direction.CW);
        path.addCircle(200,200,250, Path.Direction.CW);
        path.setFillType(Path.FillType.EVEN_ODD);
        canvas.clipPath(path);
        canvas.drawBitmap(bitmap,0,0,mPaint);
        canvas.restore();*/

        /**
         * 0为原版
         */
        /*canvas.skew(0.9f,0f);
        canvs.drawBitmap(bitmap,0,0,mPaint);*/

        /*Matrix matrix = new Matrix();
        //貌似不能同时设置多种效果,后面的会覆盖前面的
        matrix.setScale(0.3f,0.3f);
        matrix.setSkew(0f,0.1f);
//        canvas.concat(matrix);
        canvas.setMatrix(matrix);
        canvas.drawBitmap(bitmap,0,0,mPaint);*/

        Camera camera = new Camera();
        camera.save();
        camera.rotateX(20);
        camera.applyToCanvas(canvas);
        camera.restore();
        canvas.drawBitmap(bitmap,0,0,mPaint);

        // Camera 中,相机的默认位置是 (0, 0, -8)(英寸)。8 x 72 = 576,所以它的默认位置是 (0, 0, -576)(像素)。
        //而且由于换算单位被写死成了 72 像素,而不是和设备 dpi 相关的,所以在像素越大的手机上,这种「糊脸」效果会越明显。
        /*
        camera.save(); // 保存 Camera 的状态
        camera.rotateX(30); // 旋转 Camera 的三维空间
        canvas.translate(centerX, centerY); // 旋转之后把投影移动回来
        camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
        canvas.translate(-centerX, -centerY); // 旋转之前把绘制内容移动到轴心(原点)
        camera.restore(); // 恢复 Camera 的状态

        canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
        canvas.restore();*/
    }
}

Camera在绘制中的使用

/*因为坐标体系的不同,这个是未修正版*/
        //创建Camera对象
        new Camera();

        //Camera的使用
        canvas.save();
        camera.save();
        camera.rotateX(de);
        camera.applyToCanvas(canvas);
        canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
        camera.restore();
        canvas.restore();
        de+=5;
        if (de>90)de=0;
        postInvalidateDelayed(300);
/*
因为默认的旋转中心是0.0
但我们理想的旋转中心应该是图片的中间,所以需要对其进行修正
*/
        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();

        //获取中心点
        int center1X = point1.x + bitmapWidth / 2;
        int center1Y = point1.y + bitmapHeight / 2;

        camera.save();
        matrix.reset();
        //旋转30度,然后获取其矩阵
        camera.rotateX(30);
        camera.getMatrix(matrix);
        camera.restore();
        //对矩阵进行修正,将其旋转中心设置为图片的中心
        matrix.preTranslate(-center1X,-center1Y);
        matrix.postTranslate(center1X,center1Y);
        //canvas关联矩阵,然后绘制
        canvas.save();
        canvas.concat(matrix);
        canvas.drawBitmap(bitmap,point1.x,point1.y,paint);
        canvas.restore();
        //Camera的缩放效果
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        float newZ = - displayMetrics.density * 6;//-24.0
        //默认值是 0 0 -8
        camera.setLocation(0, 0, newZ);
//翻页效果
        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;
        int x = centerX - bitmapWidth / 2;
        int y = centerY - bitmapHeight / 2;

        // 第一遍绘制:上半部分
        canvas.save();
        canvas.clipRect(0, 0, getWidth(), centerY);
        canvas.drawBitmap(bitmap, x, y, paint);
        canvas.restore();

        // 第二遍绘制:下半部分
        canvas.save();
        //通过判断角度,来判断是裁剪上半部分还是下半部分
        if (degree < 90) {
            canvas.clipRect(0, centerY, getWidth(), getHeight());
        } else {
            canvas.clipRect(0, 0, getWidth(), centerY);
        }
        camera.save();
        camera.rotateX(degree);
        canvas.translate(centerX, centerY);
        camera.applyToCanvas(canvas);
        canvas.translate(-centerX, -centerY);
        camera.restore();

        canvas.drawBitmap(bitmap, x, y, paint);
        canvas.restore();

拓展控件

View的绘制顺序

public class OrderView extends ImageView {
    Paint mPaint;

    public OrderView(Context context) {
        super(context);
        initPaint();
    }

    public OrderView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public OrderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    void initPaint() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(3);
    }

    /**
     * 拓展控件:
     * 1.写在super.onDraw()后,由于绘制代码会在原有内容绘制结束之后才执行,所以绘制内容就会盖住控件原来的内容。
     *   写在super.onDraw()前,一般用于绘制背景
     *
     * 2.
     */

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.RED);
//        canvas.drawCircle(50,50,100,mPaint);//绘制背景
        super.onDraw(canvas);
        canvas.drawCircle(25,25,23,mPaint);//绘制前景
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawCircle(25,25,26,mPaint);//绘制背景的背景
        super.draw(canvas);
        //todo 绘制前景
    }
}

ViewGroup的绘制顺序

public class ViewGroupOrderView extends LinearLayout {
    public ViewGroupOrderView(Context context) {
        super(context);
        initPaint();
    }

    public ViewGroupOrderView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public ViewGroupOrderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    Paint mPaint;

    void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(3);
    }

    /**
     * 对于ViewGroup,不能在onDraw()中绘制前景
     * 造成这种情况的原因是 Android 的绘制顺序:
     * 在绘制过程中,每一个 ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View
     *
     * 想在滑动边缘渐变、滑动条和前景之间插入绘制代码?
     * 不行,虽然这三部分是依次绘制的,但它们被一起写进了 onDrawForeground()
     *
     * draw()是总调度方法,在里面一次调用了drawBackground onDraw dispatchDraw onDrawForeground
     * 由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的下面,那么这段代码会在其他所有绘制完成之后再执行
     *      1.super.draw()之后,相当于绘制前景
     *      2.super.draw()之前,相当于绘制背景的背景,如果在设置xml中的background属性会把EditText的背景去掉,所以可以在此处重写
     *      3.注意从效率考虑,ViewGroup会默认跳过draw()方法直接执行dispatchDraw(),所以有可能需要View.setWillNotDraw(false)来设置
     *          如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw();
     *
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //        canvas.drawCircle(25,25,1000,mPaint);//此代码未能实现需求
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        //相当与绘制主体和绘制子View之间,可绘制背景
        super.dispatchDraw(canvas);
//        canvas.drawCircle(25, 25, 100, mPaint);//绘制前景
    }

绘制顺序

private drawBackground()
onDraw()
dispatchDraw()
onDrawForeground()//滚动条等,api23前至支持FrameLayout

这里写图片描述

动画

View中的动画方法

        iv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                iv.setClickable(false);
                iv.animate()
                        .translationXBy(500)
                        .scaleXBy(2)
                        .scaleYBy(2)
                        .setDuration(5000)
                        .withEndAction(new Runnable() {
                            @Override
                            public void run() {
                                iv.animate()
                                        .translationXBy(-500)
                                        .scaleXBy(-2)
                                        .scaleYBy(-2)
                                        .withEndAction(new Runnable() {
                                            @Override
                                            public void run() {
                                                iv.setClickable(true);
                                            }
                                        });
                            }
                        });
            }
        });

这里写图片描述

ObjectAnimator

//继承于ValueAnimtor
                /**
                 * ObjectAnimator
                 *
                 * 如果是自定义控件,需要添加 setter / getter 方法;
                 * 用 ObjectAnimator.ofXXX() 创建 ObjectAnimator 对象;
                 * 用 start() 方法执行动画
                 *
                 * setInterpolator(Interpolator interpolator)
                 *      AccelerateDecelerateInterpolator,先加速后减速,加速度为一条余弦线
                 *      LinearInterpolator,线性
                 *      AccelerateInterpolator,加速度为正
                 *      DecelerateInterpolator,加速度为负
                 *      AnticipateInterpolator,先往后蓄力,加往前加速
                 *      OvershootInterpolator,动画会超过目标值一些,然后再弹回来
                 *      AnticipateOvershootInterpolator,开始前回拉,最后超过一些然后回弹
                 *      BounceInterpolator,在目标值处弹跳。有点像玻璃球掉在地板上的效果。
                 *      CycleInterpolator,这个也是一个正弦 / 余弦曲线,和 AccelerateDecelerateInterpolator 的区别是,它可以自定义曲线的周期
                 *      PathInterpolator,可以定制出任何你想要的速度模型。定制的方式是使用一个 Path 对象来绘制出你要的动画完成度 / 时间完成度曲线
                 *          Path interpolatorPath = new Path();
                 *          interpolatorPath.lineTo(1, 1);
                 *      FastOutLinearInInterpolator,曲线公式是用的贝塞尔曲线
                 *      FastOutSlowInInterpolator,先加速再减速。
                 *      LinearOutSlowInInterpolator,持续减速。
                 *
                 * 设置监听器:
                 *      addListener() 和  addUpdateListener() 来添加一个或多个监听器
                 *      ,移除监听器则是通过 remove[Update]Listener() 来指定移除对象。
                 *          onAnimationStart(Animator animation)
                 *          onAnimationEnd(Animator animation)
                 *          onAnimationCancel(Animator animation)
                 *          onAnimationRepeat(Animator animation)
                 *      ObjectAnimator 支持使用 pause() 方法暂停,
                 *          addPauseListener()
                 *          removePauseListener() 的支持
                 *
                 * 多属性动画
                 *      PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 1);
                 *      PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 1);
                 *      PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 1);
                 *      ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder1, holder2, holder3)
                 * 两个动画依次执行
                 *      AnimatorSet animatorSet = new AnimatorSet();
                 *      animatorSet.playSequentially(animator1, animator2);
                 *      animatorSet.start();
                 * 设置播放顺序
                 *      animatorSet.play(animator1).with(animator2);
                 *      animatorSet.play(animator1).before(animator2);
                 *      animatorSet.play(animator1).after(animator2);
                 *
                 */
/*                ObjectAnimator height = ObjectAnimator.ofFloat(tv, "x", 0, 1000);
                height.setInterpolator(new AnticipateInterpolator(2));
                height.setDuration(1000);
                height.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationCancel(Animator animation) {
                    }
                });
//                height.removeListener(传入监听器对象);
                height.start();*/

TypeEvaluator

                /**
                 * TypeEvaluator
                 *      ArgbEvaluator,实现颜色的变换效果,不设置的话,会闪
                 *          ObjectAnimator.setEvaluator(new ArgbEvaluator());
                 *      自定义Evaluator,通过ObjectAnimator.ofObject(view, "position",
                 *          new PointFEvaluator(), new PointF(0, 0), new PointF(1, 1));
                 *          private class PointFEvaluator implements TypeEvaluator<PointF> {
                 *          PointF newPoint = new PointF();
                 *              @Override
                 *              public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
                 *                  float x = startValue.x + (fraction * (endValue.x - startValue.x));
                 *                  float y = startValue.y + (fraction * (endValue.y - startValue.y));
                 *                  newPoint.set(x, y);
                 *                  return newPoint;
                 *                }
                 *            }
                 */
                ObjectAnimator textColor = ObjectAnimator.ofInt(tv, "textColor", 0xffff0000, 0xff00ff00);
                textColor.setEvaluator(new ArgbEvaluator());
                textColor.setDuration(1000 * 10);
                textColor.start();

                //所谓硬件加速,指的是把某些计算工作交给专门的硬件来做,而不是和普通的计算工作一样交给 CPU 来处理。
            }
        });
    //自定义TypeEvaluator
    private class HsvEvaluator implements TypeEvaluator<Integer> {
        float[] startHsv = new float[3];
        float[] endHsv = new float[3];
        float[] outHsv = new float[3];

        @Override
        /**
         * 百分比
         * 开始值
         * 结束值
         */
        public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
            // 把 ARGB 转换成 HSV
            Color.colorToHSV(startValue, startHsv);
            Color.colorToHSV(endValue, endHsv);

            // 计算当前动画完成度(fraction)所对应的颜色值
            if (endHsv[0] - startHsv[0] > 180) {
                endHsv[0] -= 360;
            } else if (endHsv[0] - startHsv[0] < -180) {
                endHsv[0] += 360;
            }
            outHsv[0] = startHsv[0] + (endHsv[0] - startHsv[0]) * fraction;
            if (outHsv[0] > 360) {
                outHsv[0] -= 360;
            } else if (outHsv[0] < 0) {
                outHsv[0] += 360;
            }
            outHsv[1] = startHsv[1] + (endHsv[1] - startHsv[1]) * fraction;
            outHsv[2] = startHsv[2] + (endHsv[2] - startHsv[2]) * fraction;

            // 计算当前动画完成度(fraction)所对应的透明度
            int alpha = startValue >> 24 + (int) ((endValue >> 24 - startValue >> 24) * fraction);

            // 把 HSV 转换回 ARGB 返回
            return Color.HSVToColor(alpha, outHsv);
        }
    }

自定义Layout

public class MyLayout extends ViewGroup{

    public MyLayout(Context context) {
        super(context);
    }
    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    /**
     * 自定义布局概论:
     *
     * 布局:
     * 在程序运行时通过布局文件的代码计算出实际尺寸的过程,分为两个阶段
     *      1.测量阶段
     *          从上倒下递归调用每个View或者ViewGroup的measure(),测量他们的尺寸并计算他们的位置
     *          测量阶段,measure()方法被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量。
     *              View.measure(),在onMeasure()中测量然后保存;
     *              ViewGroup.measure(),在onMeasure()遍历所有子控件并调用他们的measure(),并根据他们的测量结果计算出他们的实际
     *              尺寸和位置;
     *      2.布局阶段
     *          从上到下递归调用每个View或者ViewGroup的layout()方法,把测得的尺寸和位置赋值给他们
     *          layout()方法被父View调用,在 layout()中它会保存父View传进来的自己的位置和尺寸,并且调用onLayout()来进行实际的内部布局。
     *              View.layout(),没有子控件,onLayout中什么也不做;
     *              ViewGroup.layout(),调用自己所有子控件的layout()方法,把他们的尺寸和位置传给他们,让他们完成自我的内部布局
     *
     *  自定义布局的方式:
     *      1.重写onMeasure() 来修改已有的 View 的尺寸;
     *          在super.onMeasure()后,调用getMeasureHeight()/getMeasureWidth()取出之前计算的结果,
     *          重新计算宽高,再调用setMeasureDimension()保存即可;
     *      2.重写 onMeasure() 来全新定制自定义 View 的尺寸;
     *          需要满足父View的限制ViewGroup.onMeasure()中调用child.measure(childWidthMeasureSpec,childHeightMeasureSpec),
     *          这个两个参数会传入子view.onMeasure(childWidthMeasureSpec,childHeightMeasureSpec),里面有对子控件宽高限制的信息
     *              限制的类型:int mode=MeasureSpec.getMode(childWidthMeasureSpec);
     *                  1.UNSPECIFIED不限制,一般来说不再进行修正,处理方式一般是把方法计算参数作为结果返回
     *                  2.AT_MOST,上限限制,比较是否超出上限,如果是则返回上限值
     *                  3.EXACTLY,精确值限制,只返回限制值
     *              如何遵守限制:
     *                  在子控件计算完宽高之后,调用resolveSize(measureHeight,childHeightMeasureSpec)/resolveSize(measureWidth,childWidthMeasureSpec)
     *                  调用resolveSize的控制过程就是按上面的逻辑
     *      3.重写 onMeasure() 和 onLayout() 来全新定制自定义 ViewGroup 的内部布局。
     *          1.重写onMeasure()来计算内部布局(子控件的布局和尺寸,以及自己的尺寸)
     *              1.调用每个子控件的measure(),让子控件自我测量
     *                  1.获取ViewGroup自身的可用宽高
     *                  根据MeasureSpec.mode
     *                      1.Exactly,根据MeasureSpec.size
     *                      2.At_most,根据MeasureSpec.size
     *                      3.UNSPECIFIED,随意
     *              2.根据子控件给出的尺寸,得出子View的位置,并保存他们的位置和尺寸
     *                  1.不是所有的layout都需要去保存子控件的位置(如linearlayout)
     *                  2.在特殊情况下对某些子控件需要重复测量多次才能得到正确的尺寸和位置
     *                  如ViewGroup是wrap_content,子控件是match_parent(linearLayout不会按照一般的处理原则,而是将自己的宽度传下去,
     *                  但这时子控件测量的宽度并不是最终宽度,而是需要多次测量,linearlayout会在测量结束后把所有是match_parent的子控件
     *                  再测量一遍,这次传过去的mode是Exactly,size是多个子控件中最宽的那个)
     *              3.根据子控件的位置和尺寸计算出自己的尺寸,并使用setMeasureDimension()保存
     *          2.根据计算结果摆放子控件
     */
    int[] childLeft=new int[getChildCount()];
    int[] childTop=new int[getChildCount()];
    int[] childRight=new int[getChildCount()];
    int[] childBottom=new int[getChildCount()];

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //-------------------------------------------1.调用每个子控件的measure(),让子控件自我测量-------------------------------------------------------------
        //ViewGroup可用宽高
        int ViewGroupwidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int ViewGroupwidth = MeasureSpec.getSize(widthMeasureSpec);
        //遍历子控件
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            //获取子控件中的属性
            LayoutParams params = childView.getLayoutParams();
            //wrap_content->WRAP_CONTENT;match_parent->MATCH_PARENT;dp->具体像素值
            int height = params.height;//对应layout_height
            int width = params.width;//对应layout_width

            //结合ViewGroup的可用空间来计算子控件的尺寸
            int childWidthSpec=0;
            switch (params.width){
                //获取子控件的宽高类型和ViewGroup的可用空间
                case LayoutParams.WRAP_CONTENT:
                    if (ViewGroupwidthMode==MeasureSpec.EXACTLY||ViewGroupwidthMode==MeasureSpec.AT_MOST){
                        //把可用空间和类型传入即可
                        //注意这个宽度是会变,按自己的业务逻辑来
                        childWidthSpec=MeasureSpec.makeMeasureSpec(ViewGroupwidth,MeasureSpec.AT_MOST);
                    }else{
                        //如果是UNSPECIFIED,把参数分发下去即可,宽度随便填,对于UNSPECIFIED而言宽度是没有意义的
                        childWidthSpec=MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
                    }
                    break;
                //获取可用空间
                case LayoutParams.MATCH_PARENT:
                    if (ViewGroupwidthMode==MeasureSpec.EXACTLY||ViewGroupwidthMode==MeasureSpec.AT_MOST){
                        childWidthSpec=MeasureSpec.makeMeasureSpec(ViewGroupwidth,MeasureSpec.EXACTLY);
                    }else{
                        childWidthSpec=MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);
                    }
                    break;
                default:
                    childWidthSpec=MeasureSpec.makeMeasureSpec(params.width,MeasureSpec.EXACTLY);
                    break;
            }
        }
        //------------------------------2.根据子控件给出的尺寸,得出子View的位置,并保存他们的位置和尺寸------------------------------
            //在上面的遍历中保存
        //------------------------------3.根据子控件的位置和尺寸计算出自己的尺寸,并使用setMeasureDimension()保存------------------------------

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //注意是左上右下的四个相对坐标
        for (int i = 0; i <getChildCount() ; i++) {
            View childView = getChildAt(i);
            childView.layout(childLeft[i],childTop[i],childRight[i],childBottom[i]);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值