自定义View知识架构体系

第一章 入门基本类熟悉

第1节 Canvas

Canvas 使用方法概述

  • drawXXX() -> 绘制基本图形类方法:
  • clipRect() -> 类似于现实中的A4画纸裁剪
  • save(), restore(), rotate() translate() -> 可以联想现实中把画纸移动、旋转找的合适的下笔位置,在回复位置

方法举例

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 画圆角矩形

drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制弧形或扇形

  • startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度)
  • useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。
  1. Canvas.scale(float sx, float sy, float px, float py) 放缩

  2. skew(float sx, float sy) 错切

  3. Matrix 来做自定义变换

    Matrix matrix = new Matrix();  
    float pointsSrc = {left, top, right, top, left, bottom, right, bottom};  
    float pointsDst = {left - 10, top + 50, right + 120, top - 90, left + 20, bottom + 30, right + 20, bottom + 60};
    
    ...
    
    matrix.reset();  
    matrix.setPolyToPoly(pointsSrc, 0, pointsDst, 0, 4);
    
    canvas.save();  
    canvas.concat(matrix);  
    canvas.drawBitmap(bitmap, x, y, paint);  
    canvas.restore();

Canvas图层的概念

主要方法

//新建图层
public int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags)
//切换到指定图层的上一级图层
public void restoreToCount(int saveCount) {
//获取图层数量
public int getSaveCount()

drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)

第二是以clipXXX为主的裁剪方法

clipPath(Path path, Region.Op op)  
clipRect(Rect rect, Region.Op op)  
clipRect(RectF rect, Region.Op op)  
clipRect(float left, float top, float right, float bottom, Region.Op op)  
clipRegion(Region region, Region.Op op)  
  • Region.Op参数

    与图片的混合模式一样
  • Region和Rect有什么区别
    • Region表示的是一个区域,而Rect表示的是一个矩形
    • Region有个很特别的地方是它不受Canvas的变换影响,Canvas的local不会直接影响到Region自身。

第三是以scale、skew、translate和rotate组成的Canvas变换方法

  • translate(float dx, float dy) 会改变画布的原点坐标
  • scale(float sx, float sy, float centerX, float centerY)

最后一类则是以saveXXX和restoreXXX构成的画布锁定和还原

  • saveLayer
    int sc = canvas.saveLayer(0, 0, screenW, screenH, null, Canvas.ALL_SAVE_FLAG);   将绘制操作保存到新的图层
    canvas.restoreToCount(sc);  
  • canvas.saveLayerAlpha(0, 0, canvas.getWidth(), canvas.getHeight(), 75, Canvas.ALL_SAVE_FLAG); // 锁定画布并设置画布透明度为75%
  • restoreToCount(int saveCount)方法来指定在还原的时候还原哪一个保存操作.
  • Canvas中有六个常量值:
    • 有LAYER单词的只能saveLayerXX方法使用,其他通用。
    • ALL_SAVE_FLAG 保持所有/CLIP_SAVE_FLAG保持剪裁/MATRIX_SAVE_FLAG 保持变换
    • CLIP_TO_LAYER_SAVE_FLAG 当前图层执行裁剪操作需要对齐图层边界
    • FULL_COLOR_LAYER_SAVE_FLAG 当前图层的色彩模式至少需要是8位色
    • HAS_ALPHA_LAYER_SAVE_FLAG 当前图层中将需要使用逐像素Alpha混合模式 色彩深度和Alpha混合 wiki
  • save和saveLayerXXX方法

    save和saveLayerXXX方法有着本质的区别,saveLayerXXX方法会将所有操作在一个新的Bitmap中进行,而save则是依靠stack栈来进行

@Override  
protected void onDraw(Canvas canvas) {  
    /* 
     * 保存并裁剪画布填充绿色 
     */  
    int saveID1 = canvas.save(Canvas.CLIP_SAVE_FLAG);  
    canvas.clipRect(mViewWidth / 2F - 300, mViewHeight / 2F - 300, mViewWidth / 2F + 300, mViewHeight / 2F + 300);  
    canvas.drawColor(Color.YELLOW);  

    /* 
     * 保存并裁剪画布填充绿色 
     */  
    int saveID2 = canvas.save(Canvas.CLIP_SAVE_FLAG);  
    canvas.clipRect(mViewWidth / 2F - 200, mViewHeight / 2F - 200, mViewWidth / 2F + 200, mViewHeight / 2F + 200);  
    canvas.drawColor(Color.GREEN);  

    /* 
     * 保存画布并旋转后绘制一个蓝色的矩形 
     */  
    int saveID3 = canvas.save(Canvas.MATRIX_SAVE_FLAG);  
    canvas.rotate(5);  
    mPaint.setColor(Color.BLUE);  
    canvas.drawRect(mViewWidth / 2F - 100, mViewHeight / 2F - 100, mViewWidth / 2F + 100, mViewHeight / 2F + 100, mPaint);  
}  

以上的代码产生
Canvas内部Stack

  1. 没有restore前saveID3,saveID2,saveID1,Default Stack ID相互影响。
  2. restoreToCount(int saveCount) 还原特定Stack空间。
  3. getSaveCount() 查询当前栈中有多少save的空间

第2节 Paint

初始化类

  • reset() 相当于重新 new 一个,不过性能当然高一些啦

  • set(Paint src) 属性全部复制过来

  • setFlags(int flags)

    paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
    //等价于
    paint.setAntiAlias(true);  
    paint.setDither(true);  

笔头属性

  1. Paint.setStyle(Paint.Style style)
    1. FILL 是填充模式 (默认)
    2. STROKE 是画线模式(即勾边模式)
    3. FILL_AND_STROKE 是两种模式一并使用
  2. Paint.setStrokeWidth(float width) 非填充模式下下,设置笔头(线条)粗细
  3. setDither(boolean dither); 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰

  4. 抗锯齿
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 
    //或
    Paint.setAntiAlias(boolean aa)

    原理:修改图形边缘处的像素颜色,从而让图形在肉眼看来具有更加平滑的感觉

笔锋属性及线条

mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.BEVEL);

PathEffect

setPathEffect(PathEffect effect)

https://hencoder.com/ui-1-2/

使用 PathEffect 来给图形的轮廓设置效果。对 Canvas 所有的图形绘制有效,也就是 drawLine() drawCircle() drawPath() 这些方法。

  • CornerPathEffect 拐角变成圆角 可以将路径的转角变得圆滑

  • DiscretePathEffect 把线条进行随机的偏离

  • DashPathEffect 虚线来绘制线条

  • PathDashPathEffect 用一个 Path 来绘制「虚线」

  • SumPathEffect 分别单独绘制

  • ComposePathEffect 叠加绘制

PathEffect六个子类
对应的效果

StrokeMiter

setStrokeMiter(float miter) 是对setStrokeJoin 的补充

  1. 作用

  1. 怎么计算miter合理的值

知道角度1 / sin ( θ / 2 )

文本绘制属性

paint.setTextSize(18);  
mPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, 100, 25, paint);  
  1. setTypeface(Typeface typeface) 字体样式
  2. setUnderlineText(boolean underlineText) 下划线
  3. setStrikeThruText(boolean strikeThruText) 删除线
  4. setFakeBoldText(boolean fakeBoldText) 粗体
  5. setTextScaleX(float scaleX) 缩放因子,默认值为1
  6. setTextSkewX(float skewX) 文本在水平方向上的倾斜,默认值为0,推荐的值为-0.25
  7. setLetterSpacing(float letterSpacing) 设置行的间距,默认值是0,负值行间距会收缩
  8. getFontSpacing() 返回字符行间距
  9. measureForwards 向前还是向后/ maxWidth给定最大距离能测几个字符
    应用场景:文本阅读器的翻页效果,我们需要在翻页的时候动态折断或生成一行字符串

FontMetrics

mFontMetrics = mPaint.getFontMetrics();


【注】:字符上方或者下方有可能有特殊字符,op的意思其实就是除了Baseline到字符顶端的距离外还应该包含这些符号的高度,bottom的意思也是一样。 通常情况下很少用到,所以忽略,但是Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。

  • ascent()/descent()

笔头着色属性

paint.setColor(Color.parseColor("#009688")); 
paint.setARGB(100, 255, 0, 0);  
  • setDither(boolean dither)
    1. 用在纯色的绘制
    2. 避免出现大片的色带与色块
  • setShadowLayer(float radius, float dx, float dy, int shadowColor)
    • radius 是阴影的模糊范围; dx dy 是阴影的偏移量
    • 清除阴影层,使用 clearShadowLayer()
    • 在硬件加速开启的情况下, setShadowLayer() 只支持文字的绘制,文字之外的绘制必须关闭硬件加速才能正常绘制阴影
    • 阴影的透明度就使用 paint 的透明度

setShader(Shader shader)

  1. BitmapShader 用 Bitmap 的像素来作为图形或文字的填充
  2. LinearGradient 线性渐变
  3. RadialGradient 辐射渐变
  4. SweepGradient 扫描渐变
  5. ComposeShader 混合着色器 两个 Shader 一起使用

    // 第一个 Shader:头像的 Bitmap
    Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.batman);  
    Shader shader1 = new BitmapShader(bitmap1, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    
    // 第二个 Shader:从上到下的线性渐变(由透明到黑色)
    Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo);  
    Shader shader2 = new BitmapShader(bitmap2, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    
    // ComposeShader:结合两个 Shader
    Shader shader = new ComposeShader(shader1, shader2, PorterDuff.Mode.SRC_OVER);  
    paint.setShader(shader);
    ...
    
    canvas.drawCircle(300, 300, 300, paint);  

    其中PorterDuff.Mode 有

setColorFilter(ColorFilter colorFilter)

  • LightingColorFilter(int mul, int add) 模拟简单的光照效果, 算法

    R' = R * mul.R / 0xff + add.R  
    G' = G * mul.G / 0xff + add.G  
    B' = B * mul.B / 0xff + add.B  
  • PorterDuffColorFilter (int color, PorterDuff.Mode mode) 使用一个指定的颜色和一种指定的 PorterDuff.Mode 来与绘制对象进行合成

  • ColorMatrixColorFilter

    参考附录一StyleImageView

合成 Xfermode

使用 setXfermode() 正常绘制,必须使用离屏缓存 (Off-screen Buffer) 把内容绘制在额外的层上,再把绘制好的内容贴回 View 中

// 可以做短时的离屏缓冲
int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
...
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方  
paint.setXfermode(xfermode); // 设置 Xfermode  
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆  
paint.setXfermode(null); // 用完及时清除 Xfermode  
canvas.restoreToCount(saved);

setMaskFilter

基于整个画面来进行过滤

  1. BlurMaskFilter

    paint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.NORMAL));
    ...
    canvas.drawBitmap(bitmap, 100, 100, paint);  

    另一种思维实现模糊

    // 获取位图的Alpha通道图  
         shadowBitmap = srcBitmap.extractAlpha();  
    // 先绘制阴影  
         canvas.drawBitmap(shadowBitmap, x, y, shadowPaint);  
         // 再绘制位图  
         canvas.drawBitmap(srcBitmap, x, y, null); 
  2. EmbossMaskFilter 浮雕效果

第3节 Path

  • quadTo(float x1, float y1, float x2, float y2)

    // 实例化路径  
    mPath = new Path();  
    // 移动点至[100,100]  
    mPath.moveTo(100, 100);  
    // 连接路径到点  
    mPath.quadTo(200, 200, 300, 100);  
  • cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
    // 实例化路径  
    mPath = new Path();  
    // 移动点至[100,100]  
    mPath.moveTo(100, 100);  
    // 连接路径到点  
    mPath.cubicTo(200, 200, 300, 0, 400, 100);  
  • arcTo (RectF oval, float startAngle, float sweepAngle)
    // 实例化路径  
    mPath = new Path();  
    // 移动点至[100,100]  
    mPath.moveTo(100, 100);  
    // 连接路径到点  
    RectF oval = new RectF(100, 100, 200, 200);  
    mPath.arcTo(oval, 0, 90);  
  • arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
    mPath.arcTo(oval, 0, 90, true);
  • rXXXTo方法,坐标相对于当前点,而不是画布原点。
    rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)  
    rLineTo(float dx, float dy)  
    rMoveTo(float dx, float dy)  
    rQuadTo(float dx1, float dy1, float dx2, float dy2)  
  • addXXX方法
    addCircle(float x, float y, float radius, Path.Direction dir)  
    addOval(float left, float top, float right, float bottom, Path.Direction dir)  
    addRect(float left, float top, float right, float bottom, Path.Direction dir)  
    addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) 
    • addOval Sample 弧线并没与线段连接
      mPath = new Path();  
      // 移动点至[100,100]  
      mPath.moveTo(100, 100);  
      // 连接路径到点  
      mPath.lineTo(200, 200);  
      // 添加一条弧线到Path中  
      RectF oval = new RectF(100, 100, 300, 400);  
      mPath.addArc(oval, 0, 90);  
    • Path.Direction.CCW
    • Path.Direction.CW
  • computeBounds()
    mComputeRect = new RectF();  
    mEndPath = new Path();  
    mEndPath.addCircle(380, 380, 150, Direction.CW);  
    mEndPath.addRect(new RectF(200, 300, 500, 500), Direction.CW);  
    mEndPath.computeBounds(mComputeRect, false);
    //图片如下,返回结果为: (200,230,530,530)

第4节 Camera 【了解】

https://hencoder.com/ui-1-4/

Camera 的三维变换有三类:旋转、平移、移动相机

Camera.rotate*() 三维旋转

canvas.save();

camera.save(); // 保存 Camera 的状态  
camera.rotateX(30); // 旋转 Camera 的三维空间  
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas  
camera.restore(); // 恢复 Camera 的状态

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

Camera.translate(float x, float y, float z) 移动

Camera.setLocation(x, y, z) 设置虚拟相机的位置

单位不是像素,而是 inch

Camera 中,相机的默认位置是 (0, 0, -8)(英寸)。8 x 72 = 576,所以它的默认位置是 (0, 0, -576)(像素)

View绘制流程

  1. ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false)
  2. 在重写的方法有多个选择时,优先选择 onDraw()

第二章 初学易犯错误

构造方法缺失

原因:把自定义View放在Xml布局文件加载,没有实现带属性的构造方法

public class SampleCustomView extends View {
  public SampleCustomView(Context context) {
    super(context);
  }

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

异常如下:

 Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]

原因:布局在inflate SampleCustomView 的时候要把解析完成的属性专递给自定义View

属性继承

CustomView 代码

public class CustomView extends View {   
}

attrs.xml

<resources>
  <declare-styleable name="CustomView">
    <attr format="reference|color" name="background"/>
  </declare-styleable>
</resources>

系统异常提示:

app\build\intermediates\incremental\mergeDebugResources\merged.dir\values\values.xml:2552: error: duplicate value for resource 'attr/background' with config ''.

如果是继承不是系统的View,异常

src\main\res\values\attrs.xml: Error: Found item Attr/radius more than one time

可以看出Android 在编译的时候会把View 和CustomView 的属性进行合并,也就是说自定义属性具有继承性, 即:

  1. 自定义View的属性不能和父View同名;
  2. 子View继承父View的所有属性;

drawText 坐标错误

  • drawText(String text, float x, float y, Paint paint) ,(x, y)指如下:

不知道不会用硬件加速

什么是硬件加速

一句话:用GPU绘制View

理解:现在很多主流手机机型都会有GPU,甚至比一些电脑的配置都高。手机和电脑硬件架构基本类似,不开启和开启硬件加速,就像集显和有1G显卡的不同,程序员应该秒懂。

具体的优化效果是

  1. 绘制块;
  2. 刷新频率提高;

怎么开启

ObjectAnimator

view.setLayerType(LAYER_TYPE_HARDWARE, null);  
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180);

animator.addListener(new AnimatorListenerAdapter() {  
    @Override
    public void onAnimationEnd(Animator animation) {
        view.setLayerType(LAYER_TYPE_NONE, null);
    }
});

animator.start();  

ViewPropertyAnimator

view.animate()  
        .rotationY(90)
        .withLayer(); // withLayer() 可以自动完成上面这段代码的复杂操作

使用条件

  1. 受到 GPU 绘制方式的限制,Canvas 的有些方法在硬件加速开启式会失效或无法正常工作,如下图

    Canvas` 调用以上方法需要检测系统API

  2. 只有你在对 translationX translationY rotation alpha 等无需调用 invalidate() 的属性做动画的时候,这种方法才适用 ,故结论是

     这种方式不适用于基于自定义属性绘制的动画

  3. View 在初次绘制时以及每次 invalidate() 后重绘时,需要进行两次的绘制工作(一次绘制到 Layer,一次从 Layer 绘制到显示屏),所以其实它的每次绘制的效率是被降低了的

第三章 怎么向自定义View传参

通过Android自定义属性可以向自定义View传入特定参数

第1节 最简单案例

第一步:申明属性,一般存放在value/attrs.xml文件下

<declare-styleable name="CustomView">
    <attr format="dimension" name="radius"/>
</declare-styleable>

注意:申明属性的名称最好和自定义View名称一样, 如上面自定义属性,自定义View名称是CustomView

第二步:在布局文件xml中使用自定义属性

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:iview="http://schemas.android.com/apk/res-auto"
    >
  <com.iview.easyview.CustomView
      android:layout_width="200dp"
      android:layout_height="200dp"
      android:layout_marginStart="16dp"
      iview:radius="30dp"
      app:layout_constraintBottom_toBottomOf="parent"
      />
</android.support.constraint.ConstraintLayout>

解析:

  • xmlns:app ,xmlns:iview 虽然指向同一个命名空间(xmlns)http://schemas.android.com/apk/res-auto,但是用不同的名称,意图是让阅读者清楚区分不同的自定义属性归属。 xmlns:app 指向 ConstraintLayoutxmlns:iview 指向CustomView

第三步:在自定义View中获取自定义属性,进行动态配置

  public CustomView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRadius = ta.getDimensionPixelSize(R.styleable.CustomView_radius, 200);
    ta.recycle();
  }

第2节 8种基本类型传参(format)

<declare-styleable name = "View">
     <attr name = "background" format = "xxx" />
</declare-styleable>

其中xxx可为如下:

  1. reference:参考某一资源ID
  2. color:颜色值
  3. boolean:布尔值
  4. dimension:尺寸值
  5. float:浮点值
  6. integer:整型值
  7. string:字符串
  8. fraction:百分数 如 “200%”

第3节 单选,多选混合传参

单选,enum:枚举值

   <attr format="dimension" name="layout_height">
      <enum name="fill_parent" value="-1"/>
      <enum name="match_parent" value="-1"/>
      <enum name="wrap_content" value="-2"/>
    </attr>

多选,flag:位或运算

    <attr name="gravity">
      <flag name="top" value="0x30"/>
      <flag name="bottom" value="0x50"/>
      <flag name="left" value="0x03"/>
      <flag name="right" value="0x05"/>
      <flag name="center_vertical" value="0x10"/>
    </attr>

混合,混合类型:

 <attr format="reference|color" name="background"/>

综合应该像这样

  <declare-styleable name="CustomView">
    <attr format="dimension" name="radius"/>
    <!--单选-->
    <attr format="dimension" name="layout_height">
      <enum name="fill_parent" value="-1"/>
      <enum name="match_parent" value="-1"/>
      <enum name="wrap_content" value="-2"/>
    </attr>
    <!--多选-->
    <!--android:gravity="bottom|left"/>-->
    <attr name="gravity2">
      <flag name="top" value="0x30"/>
      <flag name="bottom" value="0x50"/>
      <flag name="left" value="0x03"/>
      <flag name="right" value="0x05"/>
      <flag name="center_vertical" value="0x10"/>
    </attr>
    <!--混合-->
    <attr format="reference|color" name="background"/>
  </declare-styleable>

第四章 控件默认Style从那来的?Theme原理

相信很多初学者甚至一些做了几年Android的人对style,对theme都有一种超级复杂的感觉,不想触碰,遇到问题百度一下,解决就算了。但是作为前端UI style的架构逻辑不清楚,真的是一个致命伤的,相信我这个过来人说的话!

我原来也觉得太复杂了,但是研究img 真的很简单。

Theme原理

如上图是从Android studio 直接用拖出来的原生控件,学习了自定义属性后,自然会有如下疑问:

  1. 上面的属性是那来的;
  2. 修改主题为什么对应的控件也会出现变化;

参考TextView的源代码

public TextView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.textViewStyle);
}

白话翻译:当xml布局没有该属性时,读取属性R.attr.textViewStyle指定的属性。

是否这里一脸懵逼,听不懂,怎么用,抓狂呢?具体设置如下:

第一步:增加自定义属性,如下属性StyleInTheme属性;

attr.xml

<resources>
    <!--自定义属性-->
  <declare-styleable name="CustomView">
    <attr format="string" name="mview_1"/>
    <attr format="string" name="mview_2"/>
    <attr format="string" name="mview_3"/>
  </declare-styleable>
    <!--定义CustomView默认的style-->
  <attr format="reference" name="StyleInTheme"/>
</resources>

第二步:Theme中设置上面的属性指向一个Style:

  <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
      <!--自定style-->
    <item name="StyleInTheme">@style/StyleForTheme</item>
  </style>

  <style name="StyleForTheme">
    <item name="mview_1">declare in theme by style</item>
    <item name="mview_2">declare in theme by style</item>
  </style>

第三步:在自定义View中指定

  public CustomView(Context context, @Nullable AttributeSet attrs) {
    //R.attr.StyleInTheme 必须和Theme中的属性对应
    this(context, attrs,R.attr.StyleInTheme);
  }

  public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
     // 可以获取StyleForTheme中设置的属性
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView,defStyleAttr,R.style.DefaultStyleRes);
    mRadius = a.getDimensionPixelSize(R.styleable.CustomView_mview_1, 200);
  }

设置属性的5等级

明白可设置属性的5等级,style和theme都是浮云

如上所有的属性解析都指向

context.obtainStyledAttributes(attrs,R.styleable.CustomView,defStyleAttr,R.style.DefaultStyleRes);

源码指向Resources#obtainStyledAttributes,如下重点注释

/**
* 上面省略...
* <ol>
*     <li> Any attribute values in the given AttributeSet.
*     <li> The style resource specified in the AttributeSet (named
*     "style").
*     <li> The default style specified by <var>defStyleAttr</var> and
*     <var>defStyleRes</var>
*     <li> The base values in this theme.
* </ol>
* ....
**/
public TypedArray obtainStyledAttributes(AttributeSet set,int[] attrs, int defStyleAttr, int defStyleRes) {
   return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
}

注释意思是: 定义了一个获取属性的优先级,当两个以上级别同时存在时优先选择级别高的。

如图数字越小优先级越高:

数字越小优先级越高

根据obtainStyledAttributes 和上图理解Android style 和 Theme的架构体系,So Easy!!

获取Style常用方法

 final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes);
ColorStateList backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor)
if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupAnimationStyle)) {
     final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, 0);
}
a.recycle();

第五章 View 动画(属性的修改)

View 动画的本质是一定时间段内动态的改变View的一个或多个属性,以达到动画的效果

三个关键要素

  1. 起始时间
  2. 改变的属性
  3. 改变的方式

第1节 改变属性

最小案例

// imageView1: 500 毫秒
imageView1.animate()  
        .translationX(500)
        .setDuration(500);

ObjectAnimator animator = 
    ObjectAnimator.ofFloat(imageView, "translationX", 500/*改变属性的最终值*/);
animator.setDuration(2000);  
animator.start();  

因此动画可改变的属性有

  1. 继承于View的属性

  2. 自定义View中的自定义属性

第2节 改变方式

2.1 Interpolator

https://hencoder.com/ui-1-6/

EasingInterpolator

2.2 自定义 Evaluator

给起始值和进行的百分比,想要什么值自己定算法

案例一:颜色渐变Demo

// 自定义 HslEvaluator h 色度,s饱和度 l亮度
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);
   }
}

ObjectAnimator animator = ObjectAnimator.ofInt(view, "color", 0xff00ff00);  
// 使用自定义的 HslEvaluator
animator.setEvaluator(new HsvEvaluator());  
animator.start();

案例二:使用Point(坐标)进行物体线路改变

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 animator = ObjectAnimator.ofObject(view, "position",  
        new PointFEvaluator(), new PointF(0, 0), new PointF(1, 1));
animator.start();  

2.3 一次改变多个属性

三种方式

  1. 原生方式
view.animate()  
        .scaleX(1)
        .scaleY(1)
        .alpha(1);
  1. PropertyValuesHolder 方式

    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)  
    animator.start();  
  2. animatorSet方式

    animatorSet.playTogether(animator1, animator2);  
    animatorSet.start();  

2.4 多属性改变设置顺序

// 使用 AnimatorSet.play(animatorA).with/before/after(animatorB)
// 的方式来精确配置各个 Animator 之间的关系
animatorSet.play(animator1).with(animator2);  
animatorSet.play(animator1).before(animator2);  
animatorSet.play(animator1).after(animator2);  
animatorSet.start();  

2.5 同一个属性关键帧拆分

 // 在 0% 处开始
Keyframe keyframe1 = Keyframe.ofFloat(0, 0);
// 时间经过 50% 的时候,动画完成度 100%
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
// 时间见过 100% 的时候,动画完成度倒退到 80%,即反弹 20%
Keyframe keyframe3 = Keyframe.ofFloat(1, 80);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("progress", keyframe1, keyframe2, keyframe3);

ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder);
animator.start();

注解:代码逻辑是根据关键帧(Keyframe),指定Value;定义三个个点(Keyframe, value)

  1. (0,0)
  2. (0.5,100)
  3. (1,80)

第3节 监听改变

AnimatorListenerAdapter.java

public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener,
        Animator.AnimatorPauseListener {

    @Override
    public void onAnimationCancel(Animator animation) {
        //onAnimationEnd()也会调用,后于Cancel
    }

    @Override
    public void onAnimationEnd(Animator animation) {
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
        //由于 ViewPropertyAnimator 不支持重复,所以这个方法对 ViewPropertyAnimator 相当于无效
    }

    @Override
    public void onAnimationStart(Animator animation) {
    }

    @Override
    public void onAnimationPause(Animator animation) {
    }

    @Override
    public void onAnimationResume(Animator animation) {
    }
}

第4节 实战笔记

4.1 Tween Animation

第一步: res/anim/hyperspace_jump.xml 动画修改逻辑xml配置

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@[package:]anim/interpolator_resource"
    android:shareInterpolator=["true" | "false"] >
    <alpha
        android:fromAlpha="float"
        android:toAlpha="float" />
    <scale
        android:fromXScale="float"
        android:toXScale="float"
        android:fromYScale="float"
        android:toYScale="float"
        android:pivotX="float"
        android:pivotY="float" />
    <translate
        android:fromXDelta="float"
        android:toXDelta="float"
        android:fromYDelta="float"
        android:toYDelta="float" />
    <rotate
        android:fromDegrees="float"
        android:toDegrees="float"
        android:pivotX="float"
        android:pivotY="float" />
    <set>
        ...
    </set>
</set>

第二步:启动xml动画设置

ImageView image = (ImageView) findViewById(R.id.image);
Animation hyperspaceJump = AnimationUtils.loadAnimation(this, R.anim.hyperspace_jump);
image.startAnimation(hyperspaceJump);

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext,
    R.animator.property_animator);
set.setTarget(myObject);
set.start();

第三步:停止

public void stopAnimation(View v) {
    v.clearAnimation();
    if (canCancelAnimation()) {
       v.animate().cancel();    
    }    
    animation.setAnimationListener(null);    
    v.setAnimation(null);
}

/**
 * Returns true if the API level supports canceling existing animations via the 
 * ViewPropertyAnimator, and false if it does not 
 * @return true if the API level supports canceling existing animations via the 
 * ViewPropertyAnimator, and false if it does not 
 */
public static boolean canCancelAnimation() {
    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
}

// 设置切换动画,从右边进入,左边退出
on Activity
overridePendingTransition(R.anim.in_from_right, R.anim.out_to_left);

//in_from_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator" >
    <translate
        android:duration="200"
        android:fromXDelta="100%p"
        android:toXDelta="0%p" />
</set>

//out_to_left.xml
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator" >
    <translate
        android:duration="20"
        android:fromXDelta="0%p"
        android:toXDelta="-100%p" />
</set>

4.2 Frame Animation

第一步:定义xml文件, 位置res/drawable/rocket_thrust.xml如:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item android:drawable="@drawable/thrust1" android:duration="200" />
    <item android:drawable="@drawable/thrust2" android:duration="200" />
    <item android:drawable="@drawable/thrust3" android:duration="200" />
</animation-list>

第二步: 使用

ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
rocketImage.setBackgroundResource(R.drawable.rocket_thrust);
rocketAnimation = (AnimationDrawable) rocketImage.getBackground();
rocketImage.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        rocketAnimation.start();
      }
  });

4.3 Spring Animation

CustomView案例笔记

//知道对应的边,求角度
int angle = (int) Math.toDegrees(Math.acos((mArcRadius - mCurrentProgressPosition)        / (float) mArcRadius));
//使用Matrix旋转,位移图片
matrix.postTranslate(transX, transY);
postRotate(float degrees, float px, float py);
canvas.drawBitmap(mLeafBitmap, matrix, mBitmapPaint);

第六章 UI 布局

MeasureSpec 常用方法

  • MeasureSpec类中的三个Mode常量值的意义
    UNSPECIFIED 表示未指定
    EXACTLY 老爸已经指定好
    AT_MOST 表示至多,有一个最大限制
  • 获取Mode,int mode = MeasureSpec.getMode(heightMeasureSpec);
  • int size = MeasureSpec.getSize(measureSpec); 获得实际大小
  • int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
  • measureChildren /measureChild/measureChildWithMargins
  • setMeasuredDimension() 设置最终测量值
  • API 11 以前 使用 resolveSize ,以后使用 resolveSizeAndState
    setMeasuredDimension(
    resolveSizeAndState(maxWidth, widthMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT),
    resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
    final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
    final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);

ViewGroup/View


  • shouldDelayChildPressedState 自定义ViewGroup 为不滚动Group返回false;
  • Gravity.apply(int gravity, int w, int h, Rect container, Rect outRect)
    第一个参数表示我们的对其方式值,第二三个参数呢则表示我们要对齐的元素,这里通俗地说就是我们父容器下的子元素,而container参数表示的则是我们父容器的矩形区域,最后一个参数是接收计算后子元素位置区域的矩形对象,随便new个传进去就行,可见apply方式是根据矩形区域来计算对其方式的,所以说非常好用,我们只需在onLayout方法中确定出父容器的矩形区域就可以轻松地计算出子元素根据对其方式出现在父容器中的矩形区域,这一个过程留给大家自行尝试.

触摸反馈

Android的事件分发机制以及滑动冲突的解决

坐标

  • getGlobalVisibleRect方法的作用是获取视图在屏幕坐标中的可视区域
  • getLocalVisibleRect的作用是获取视图本身可见的坐标区域,坐标以自己的左上角为原点(0,0)
  • view.getLocationInWindow(location); //获取在当前窗口内的绝对坐标
  • view.getLocationOnScreen(location); //获取在整个屏幕内的绝对坐标
  • MotionEvent中getX()和getRawX()的区别 getRowX:触摸点相对于屏幕的坐标 getX: 触摸点相对于按钮的坐标

参考:

获取View位置

屏幕分辨率为1080x2160

var rect = Rect()
view.getGlobalVisibleRect(rect)
rect.toString()
  • getHitRect/getGlobalVisibleRect -> Rect(882, 1764 - 1036, 1918)

  • getDrawingRect/getLocalVisibleRect -> Rect(0, 0 - 154, 154)

附录一

参考博文

Android动画大合集

定义View 其实很简单

Canvas

Style

图片处理

开源库

附录二

知识点

Matrix

除了平移外,缩放、旋转、错切、透视都是需要一个中心点作为参照的,如果没有平移,默认为图形的[0,0]点,平移只需要指定移动的距离即可,平移操作会改变中心点的位置!非常重要!记牢了!

  • 而preXXX和postXXX就是分别表示矩阵的左右乘,左右乘的结果是不同的。
  • 平移left个单位↓平移top个单位,也就是说原本shader的原点应该是画布,不是屏幕。

仿写练习

https://hencoder.com/activity-mock-2/

即刻仿写https://insight.io/github.com/arvinljw/ThumbUpSample/tree/master/
关于仿写者刘金伟:
github: https://github.com/arvinljw
简书: http://www.jianshu.com/u/8fcc3372beb7

薄荷健康仿写https://insight.io/github.com/totond/BooheeRuler/tree/master/
关于仿写者严积楷:
github: https://github.com/totond
CSDN: http://blog.csdn.net/totond
邮箱: yanzhikai_yjk@qq.com

小米运动仿写https://insight.io/github.com/SickWorm/MISportsConnectWidget/tree/master/
关于仿写者陈浩:
github: https://github.com/SickWorm

Flipboard 仿写https://insight.io/github.com/sunnyxibei/HenCoderPractice/tree/master/
关于仿写者贾元斌:
github: https://github.com/sunnyxibei
微博: https://weibo.com/812306989
微信: sun521xibei
个人博客: http://timeriver.com.cn/

自定义View其实很简单案例

  • 自定义控件其实很简单1/6

  • 自定义控件其实很简单1/3

    mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0, mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);createBitamp + matrix 得到图片的倒影图

    核心代码

    // 实例化混合模式  
          mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);  
    // 去饱和、提亮、色相矫正  
    mBitmapPaint.setColorFilter(new ColorMatrixColorFilter(new float[] { 0.8587F, 0.2940F, -0.0927F, 0, 6.79F, 0.0821F, 0.9145F, 0.0634F, 0, 6.79F, 0.2019F, 0.1097F, 0.7483F, 0, 6.79F, 0, 0, 0, 1, 0 }));  
    // 设置径向渐变,渐变中心当然是图片的中心也是屏幕中心,渐变半径我们直接拿图片的高度但是要稍微小一点  
          // 中心颜色为透明而边缘颜色为黑色  
          mShaderPaint.setShader(new RadialGradient(screenW / 2, screenH / 2, mBitmap.getHeight() * 7 / 8, Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP)); 
    @Override  
      protected void onDraw(Canvas canvas) {  
          canvas.drawColor(Color.BLACK);  
          // 新建图层  
          int sc = canvas.saveLayer(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), null, Canvas.ALL_SAVE_FLAG);  
          // 绘制混合颜色  
          canvas.drawColor(0xcc1c093e);  
          // 设置混合模式  
          mBitmapPaint.setXfermode(mXfermode);  
          // 绘制位图  
          canvas.drawBitmap(mBitmap, x, y, mBitmapPaint);  
          // 还原混合模式  
          mBitmapPaint.setXfermode(null);  
          // 还原画布  
          canvas.restoreToCount(sc);  
          // 绘制一个跟图片大小一样的矩形  
          canvas.drawRect(x, y, x + mBitmap.getWidth(), y + mBitmap.getHeight(), mShaderPaint);  
      }  
  • Matrix ImageView中的应用

  • Matrix 在ListView中的应用

      @Override  
      protected void onDraw(Canvas canvas) {  
          mCamera.save();  
          mCamera.rotate(30, 0, 0);  
          mCamera.getMatrix(mMatrix);  
          mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);  
          mMatrix.postTranslate(getWidth() / 2, getHeight() / 2);  
          canvas.concat(mMatrix);  
          super.onDraw(canvas);  
          mCamera.restore();  
      }  
  • 自定义View

    关键代码:

     canvas.save();  
          // 平移和旋转画布  
          canvas.translate(ccX, ccY);  
          canvas.rotate(100);  
          // 依次画:(间隔)线(间隔)-圈  
          canvas.drawLine(0, -largeCricleRadiu - space, 0, -lineLength * 2 - space, strokePaint);  
          canvas.drawCircle(0, -lineLength * 2 - smallCricleRadiu - space * 2, smallCricleRadiu, strokePaint);  
          // 释放画布  
          canvas.restore();  
  • View Path/Canvas的综合应用 5/12

  • 翻页
    翻页(1)
    翻页-1

难点未理解:折叠区域图片的绘制。

canvas.rotate(90 - mDegrees);  
canvas.translate(0, -mViewHeight);  
canvas.scale(-1, 1);  
canvas.translate(-mViewWidth, 0);  

Paste_Image.png

20141216152332401.gif

优秀博文