深入理解Android之自定义View_android 自定义view-程序员宅基地

技术标签: Android  

       自定义View多线程网络,被认为是Android开发者必须牢固掌握的最基础的三大基本功。而自定义View又是Android开发中特别重要的一环,很多地方都需要用到自定义View。这篇文章我就梳理一下自定义View的相关知识。

 

目录

(一)什么是自定义View   

(二)自定义组合View

(三)自定义View的绘制

1. Canvas.drawXXX()

Path用法详解

2. Paint的完全攻略

3. Canvas对绘制的辅助——范围裁切和几何变换

(1)范围裁切

(2)几何变换

4. 使用不同的绘制方法来控制绘制顺序

(四)自定义View的布局

(五)自定义View的触摸反馈


 

(一)什么是自定义View   

       什么是自定义View?自定义View,顾名思义就是现有的官方提供的View(控件)已经无法满足我们的日常看法的需要,需要我们自己去定义实现一个View。而在我们项目开发的过程中,遇到一些复杂的布局或者控件是在所难免的。因此,对于我们来说,学习好自定义View才会变得如此重要。

       首先我们先了解一下View的绘制流程,它对应了View中的三个抽象方法:即onMeasure->onLayout->onDraw

  • onMeasure: 测量View的宽高
  • onLayout: 计算View的位置并布局
  • onDraw: 绘制View

       其次我们应该明确,学习自定义View的三个关键点:

  • 布局
  • 绘制
  • 触摸反馈

       掌握了这三点也就基本掌握了自定义View。这里的布局是指自定义View在哪个位置显示,包括测量和布局两方面,对应了onMeasure和onLayout方法;绘制是让自定义View显示在屏幕上,对应了onDraw方法;而触摸反馈则是比较高级的概念,它对应了自定义View的行为。

       最后,我们着重了解一下如何自定义View。通常有以下三种方法

(1)自定义组合View

(2)继承系统控件或布局(系统View控件:如TextView,系统ViewGroup布局:如LinearLayout)

(3)直接继承View/ViewGroup

 

(二)自定义组合View

        这种方法非常特殊,因为它不需要重写相关方法就可以直接使用,因此开发起来非常方便。但是缺点也非常明显,它只能够通过现有的View(系统View)进行组合,如果我们需要自定义的View是形状非常不规则,无法通过现有View直接组合得出的话,这种方法是无法满足要求的。

        如下图,以实现一个自定义的TitleBar为例:

               

1. 自定义属性

       在values文件夹下,新建一个attrs.xml文件,并且自定义相关属性。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CusTitleBar">
        <attr name="bg_color" format="color"></attr>
        <attr name="text_color" format="color"></attr>
        <attr name="title_text" format="string"></attr>
    </declare-styleable>
</resources>

2. 自定义布局

      然后在layout文件夹,新建一个布局文件layout_custom_titlebar,并根据需要进行自定义布局。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:id="@+id/layout_titlebar_root">

    <ImageView
        android:id="@+id/btn_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:src="@drawable/ico_return"
        android:paddingLeft="10dp" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:textSize="20sp" />

    <ImageView
        android:id="@+id/btn_right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:src="@drawable/ico_title_right"
        android:paddingRight="10dp" />
</RelativeLayout>

3. 实现自定义View

       通过继承一个系统Layout父布局,并且将自定义View的布局和属性进行关联。再根据需要,编写一些功能代码。

public class CustomTitleBar extends RelativeLayout {
    private int mBgColor = Color.BLUE;
    private int mTextColor = Color.WHITE;
    private String mTitleText = "";

    private ImageView btn_left;
    private ImageView btn_right;
    private TextView tvTitle;
    private RelativeLayout relativeLayout;

    public CustomTitleBar(Context context) {
        super(context);
        initView(context);
    }

    public CustomTitleBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        initTypeValue(context,attrs);
        initView(context);
    }

    public void initTypeValue(Context context ,AttributeSet attrs){
        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CusTitleBar);
        mBgColor = a.getColor(R.styleable.CusTitleBar_bg_color, Color.YELLOW);
        mTitleText = a.getString(R.styleable.CusTitleBar_title_text);
        mTextColor = a.getColor(R.styleable.CusTitleBar_text_color,Color.RED);
        a.recycle();
    }

    public void initView(Context context){
        LayoutInflater.from(context).inflate(R.layout.layout_custom_titlebar,this,true);

        btn_left = findViewById(R.id.btn_left);
        btn_right = findViewById(R.id.btn_right);
        tvTitle = findViewById(R.id.tv_title);
        relativeLayout = findViewById(R.id.layout_titlebar_root);

        relativeLayout.setBackgroundColor(mBgColor);
        tvTitle.setTextColor(mTextColor);
        tvTitle.setText(mTitleText);
    }

    public void setBackClickListener(OnClickListener listener){
        btn_left.setOnClickListener(listener);
    }

    public void setRightClickListener(OnClickListener listener){
        btn_right.setOnClickListener(listener);
    }

    public void setTitleText(String str){
        if(!TextUtils.isEmpty(str)){
            tvTitle.setText(str);
        }
    }

}

4. 使用自定义View

      用法非常简单,在需要使用的layout布局中,将自定义的View导入,并完善相关属性。最后在Java代码中进行调用即可。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:apps="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#eee"
    tools:context=".MainActivity">

    <software.baby.learncustomview.CustomTitleBar
        android:id="@+id/custom_title_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        apps:title_text="@string/app_name"
        apps:text_color="@color/colorWhite"
        apps:bg_color = "@color/colorPrimary" />
</LinearLayout>
public class MainActivity extends AppCompatActivity {
    private CustomTitleBar customTitleBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        customTitleBar = findViewById(R.id.custom_title_bar);
        customTitleBar.setTitleText("标题标题");
        customTitleBar.setBackClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "Click Back!", Toast.LENGTH_SHORT).show();
            }
        });
        customTitleBar.setRightClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "Click Right!", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

 

(三)自定义View的绘制

  • 自定义绘制的方式是重写绘制方法 onDraw()
  • 绘制的关键是 Canvas 的使用
    • Canvas 的绘制类方法: drawXXX() (关键参数:Paint)
    • Canvas 的辅助类方法:范围裁切和几何变换
  • 可以使用不同的绘制方法来控制遮盖关系

        自定义绘制的上手非常容易:提前创建好 Paint 对象,重写 onDraw(),把绘制代码写在 onDraw() 里面,就是自定义绘制最基本的实现。大概就像这样:

Paint paint = new Paint();

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

    // 绘制一个圆
    canvas.drawCircle(300, 300, 200, paint);
}

1. Canvas.drawXXX()

        指的是Canvas类下的所有 draw- 打头的方法,例如 drawCircle() drawBitmap()

drawCircle(float centerX, float centerY, float radius, Paint paint)

        该方法用于绘制圆形。前两个参数 centerX centerY 是圆心的坐标,第三个参数 radius 是圆的半径,单位都是像素,它们共同构成了这个圆的基本信息(即用这几个信息可以构建出一个确定的圆);第四个参数 paint提供基本信息之外的所有风格信息,例如颜色、线条粗细、阴影等。

canvas.drawCircle(300, 300, 200, paint);

drawRect(float left, float top, float right, float bottom, Paint paint)

        该方法用于绘制矩形。lefttoprightbottom 是矩形四条边的坐标。

canvas.drawRect(100, 100, 500, 500, paint);

drawPoint(float x, float y, Paint paint)

        该方法用于绘制点。x 和 y 是点的坐标。

drawPoint(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint)

        该方法用于批量绘制点。pts 这个数组是点的坐标,每两个成一对;offset 表示跳过数组的前几个数再开始记坐标;count 表示一共要绘制几个点。

float[] points = {0, 0, 50, 50, 50, 100, 100, 50, 100, 100, 150, 50, 150, 100};
// 绘制四个点:(50, 50) (50, 100) (100, 50) (100, 100)
canvas.drawPoints(points, 2 /* 跳过两个数,即前两个 0 */,
          8 /* 一共绘制 8 个数(4 个点)*/, paint);

drawOval(float left, float top, float right, float bottom, Paint paint)

       该方法用于绘制椭圆。只能绘制横着的或者竖着的椭圆,不能绘制斜的。lefttoprightbottom 是这个椭圆的左、上、右、下四个边界点的坐标。

drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 

       该方法用于绘制线。startXstartYstopXstopY 分别是线的起点和终点坐标。由于直线不是封闭图形,所以 setStyle(style) 对直线没有影响。

drawLines(float[] pts, int offset, int count, Paint paint) / drawLines(float[] pts, Paint paint) 

       该方法用于批量绘制线。具体参数参照批量绘制点。

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, paint);

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 

       该方法用于绘制圆角矩形。lefttoprightbottom 是四条边的坐标,rx 和 ry 是圆角的横向半径和纵向半径。另外,它还有一个重载方法 drawRoundRect(RectF rect, float rx, float ry, Paint paint),让你可以直接填写 RectF 来绘制圆角矩形。

drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)

       该方法用于批量绘制弧形或扇形。drawArc() 是使用一个椭圆来描述弧形的。lefttoprightbottom 描述的是这个弧形所在的椭圆;startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。

paint.setStyle(Paint.Style.FILL); // 填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint); // 绘制扇形
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint); // 绘制弧形
paint.setStyle(Paint.Style.STROKE); // 画线模式
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint); // 绘制不封口的弧形

drawPath(Path path, Paint paint)

         该方法用于绘制自定义图形。drawPath(path) 这个方法是通过描述路径的方式来绘制图形的,它的 path 参数就是用来描述图形路径的对象。

public class PathView extends View {

    Paint paint = new Paint();
    Path path = new Path(); // 初始化 Path 对象
    
    ......
    
    {
      path.addArc(200, 200, 400, 400, -225, 225);
      path.arcTo(400, 200, 600, 400, -180, 225, false);
      path.lineTo(400, 542);
    }

    @Override
    protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);
      
      canvas.drawPath(path, paint); // 绘制出 path 描述的图形(心形)
    }
}


Path用法详解

第一类:直接描述路径

这一类方法还可以细分为两组:添加子图形和画线(直线或曲线)

   第一组: addXxx() ——添加子图形

  • addCircle(float x, float y, float radius, Direction dir) 添加圆
  • addOval(float left, float top, float right, float bottom, Direction dir) / addOval(RectF oval, 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) / addRoundRect(float left, float top, float right, float bottom, float rx,       float ry, Direction dir) / addRoundRect(RectF rect, float[] radii, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) 添加圆角矩形
  • addPath(Path path) 添加另一个 Path

      Direction 指路径的方向。路径方向有两种:顺时针 (CW clockwise) 和逆时针 (CCW counter-clockwise) 。

path.addCircle(300, 300, 200, Path.Direction.CW);

   第二组:xxxTo() ——画线(直线或曲线)

      和第一组 addXxx() 方法的区别在于,第一组是添加的完整封闭图形(除了 addPath() ),而这一组添加的只是一条线。

  • lineTo(float x, float y) / rLineTo(float x, float y) 画直线

      从当前位置向目标位置画一条直线, x 和 y 是目标位置的坐标。这两个方法的区别是,lineTo(x, y) 的参数是绝对坐标,而 rLineTo(x, y) 的参数是相对当前位置的相对坐标 (前缀 r 指的就是 relatively 「相对地」)。

  • quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 画二次贝塞尔曲线
  • cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) / rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 画三次贝塞尔曲线
  • moveTo(float x, float y) / rMoveTo(float x, float y) 移动到目标位置
  • arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(RectF oval, float startAngle, float sweepAngle) 画弧形

      close() 封闭当前子图形。

paint.setStyle(Style.STROKE);
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
path.close(); // 使用 close() 封闭子图形。等价于 path.lineTo(100, 100)

        不是所有的子图形都需要使用 close() 来封闭。当需要填充图形时(即 Paint.Style 为 FILL 或 FILL_AND_STROKEPath 会自动封闭子图形。

第二类:辅助的设置或计算

Path.setFillType(Path.FillType ft) 设置填充方式

方法中填入不同的 FillType 值,就会有不同的填充效果。FillType 的取值有四个:

  • EVEN_ODD
  • WINDING (默认值)
  • INVERSE_EVEN_ODD
  • INVERSE_WINDING


drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 

      该方法用于绘制Bitmap对象。其中 left 和 top 是要把 bitmap 绘制到的位置坐标。

drawText(String text, float x, float y, Paint paint) 

      该方法用于绘制文字。参数 text 是用来绘制的字符串,x 和 y 是绘制的起点坐标。

2. Paint的完全攻略

  Paint 类的几个最常用的方法。具体是:

Paint.setStyle(Style style) 设置绘制模式

        Style 具体来说有三种: FILLSTROKE 和 FILL_AND_STROKE FILL 是填充模式,STROKE 是画线模式(即勾边模式),FILL_AND_STROKE 是两种模式一并使用:既画线又填充。它的默认值是 FILL,填充模式。

paint.setStyle(Paint.Style.STROKE); // Style 修改为画线模式
canvas.drawCircle(300, 300, 200, paint);

Paint.setColor(int color) 设置颜色

paint.setColor(Color.RED); 

Paint.setColorFilter(ColorFilter colorFilter) 设置颜色过滤

        颜色过滤的意思,就是为绘制的内容设置一个统一的过滤策略。ColorFilter 并不直接使用,而是使用它的子类。它共有三个子类:

  • LightingColorFilter   模拟简单的光照效果
  • PorterDuffColorFilter    指定的颜色和一种指定的 PorterDuff.Mode 来与绘制对象进行合成
  • ColorMatrixColorFilter 使用一个 ColorMatrix 来对颜色进行处理
ColorFilter lightingColorFilter = new LightingColorFilter(0xffffff, 0x003000);
paint.setColorFilter(lightingColorFilter);

Paint.setStrokeWidth(float width) 设置线条宽度

 Paint.setTextSize(float textSize) 设置文字大小

 Paint.setShader(Shader shader) 设置 Shader 

        Shader 这个英文单词很多人没有见过,它的中文叫做「着色器」,也是用于设置绘制颜色的。「着色器」不是 Android 独有的,它是图形领域里一个通用的概念,它和直接设置颜色的区别是,着色器设置的是一个颜色方案,或者说是一套着色规则。当设置了 Shader 之后,Paint 在绘制图形和文字时就不使用 setColor/ARGB() 设置的颜色了,而是使用 Shader 的方案中的颜色。

        Android 的绘制里使用 Shader ,并不直接用 Shader 这个类,而是用它的几个子类。具体来讲有 

  • LinearGradient   线性渐变
  • RadialGradient   辐射渐变
  • SweepGradient 扫描渐变
  • BitmapShader   用 Bitmap 的像素来作为图形或文字的填充
  • ComposeShader 混合着色器
Shader shader = new LinearGradient(100, 100, 500, 500, Color.parseColor("#E91E63"),
        Color.parseColor("#2196F3"), Shader.TileMode.CLAMP);
paint.setShader(shader);
     
...
     
canvas.drawCircle(300, 300, 200, paint);

Paint.setAntiAlias(boolean aa) 设置抗锯齿开关

       在绘制的时候,往往需要开启抗锯齿来让图形和文字的边缘更加平滑。开启抗锯齿很简单,只要在 new Paint() 的时候加上一个 ANTI_ALIAS_FLAG 参数就行。

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

       另外,你也可以使用 Paint.setAntiAlias(boolean aa) 来动态开关抗锯齿。效果如下:

3. Canvas对绘制的辅助——范围裁切和几何变换

(1)范围裁切

      范围裁切有两个方法: clipRect() 和 clipPath()

clipRect()  按矩形区域裁切

     记得要加上 Canvas.save() 和 Canvas.restore() 来及时恢复绘制范围。

canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

clipPath()   按路径裁切

      和 clipRect() 用法完全一样,只是把参数换成了 Path。

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

(2)几何变换

    1)使用 Canvas 来做常见的二维变换

  • Canvas.translate(float dx, float dy) 平移
  • Canvas.rotate(float degrees, float px, float py) 旋转
  • Canvas.scale(float sx, float sy, float px, float py) 放缩
  • Canvas.skew(float sx, float sy) 错切
canvas.save();
canvas.translate(200, 0);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

    2)使用 Matrix 来做常见和不常见的二维变换

       i. 使用 Matrix 来做常见变换

  Matrix 做常见变换的方式:

  • 创建 Matrix 对象;
  • 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
  • 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas

      Matrix 应用到 Canvas 有两个方法: 

  • Canvas.setMatrix(matrix) 用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换
  • Canvas.concat(matrix) 用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换
Matrix matrix = new Matrix();

...

matrix.reset();
matrix.postTranslate();
matrix.postRotate();

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

      ii. 使用 Matrix 来做自定义变换

  Matrix 的自定义变换使用的是 setPolyToPoly() 方法。

  • Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) 用点对点映射的方式设置变换
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();

    3)使用 Camera 来做三维变换

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

  • Camera.rotate*() 三维旋转
  • Camera.translate(float x, float y, float z) 移动
  • Camera.setLocation(x, y, z) 设置虚拟相机的位置
canvas.save();

camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas

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

4. 使用不同的绘制方法来控制绘制顺序

       使用不同的绘制方法,以及在重写的时候把绘制代码放在 super.绘制方法() 的上面或下面不同的位置,以此来实现需要的遮盖关系。

注意事项:

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

 

(四)自定义View的布局

       自定义View的布局实际上分为两个阶段,测量阶段布局阶段。其具体的流程如下:

        

        在这个过程中,我们需要重写布局过程的相关方法:

  • 测量阶段:onMeasure
  • 布局阶段:onLayout

        对于重写过程大致上又可以分为三类

1. 重写onMeasure()来修改已有View的尺寸

  • 重写onMeasure(),并调用super.onMeasure()触发原先的测量
  • 用getMeasuredWIdth()和getMeasuredHeight()取得之前测得的尺寸,利用这两个尺寸来计算出最终尺寸
  • 使用setMeasuredDimension()保存尺寸
public class SquareImageView extends ImageView {

    ...

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //先执行原测量算法
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取原先的测量结果
        int measuredWidth = getMeasuredWidth();
        int measuredHeight = getMeasuredHeight();

        //利用原先的测量结果计算出新尺寸
        if (measuredWidth > measuredHeight) {
            measuredWidth = measuredHeight;
        } else {
            measuredHeight = measuredWidth;
        }
        //保存计算后的结果
        setMeasuredDimension(measuredWidth, measuredHeight);
    }
}

2. 重写onMeasure()来全新计算自定义View的尺寸

  • 重写onMeasure()把尺寸计算出来
  • 把计算出来的结果用resolveSize()过滤一遍
public class CustomView extends View {

    ...

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        measuredWidth = ...;
        measuredHeight = ...;

        measuredWidth = resolveSize(measuredWidth, widthMeasureSpec);
        measuredHeight = resolveSize(measuredHeight, heightMeasureSpec);

        setMeasuredDimension(measuredWidth, measuredHeight);
    }
}

3. 重写onMeasure()和onLayout()来全新计算自定义View的内部布局

(1) 重写onMeasure()来计算内部布局

  • 计算子View的尺寸  调用每个子View的measure(),让子View自我测量

        关键:1)宽度和高度的MeasureSpec的计算

                   2)结合开发者的要求(layout_xxx)和自己地可用空间(自己的尺寸上限 - 已用空间)

  • 计算子View的位置并保存子View的位置和尺寸  根据子View给出的尺寸,得出子View的位置,并保存它们的位置和尺寸
  • 计算自己的尺寸并保存  根据子View的位置和尺寸计算出自己的尺寸,并用setMeasuredDimension()保存

    难点:【可用空间】的获得

  • EXACTLY(固定值): MeasureSpec中的Size
  • AT_MOST(尺寸上限):  MeasureSpec中的Size
  • UNSPECIFIED(没有固定限制): 无限大
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);

            //子View的尺寸限制
            int childWidthSpec;
            
            //已使用的Width
            int usedWidth = ...;
            int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            LayoutParams lp = childView.getLayoutParams();
            switch (lp.width) {
                case LayoutParams.MATCH_PARENT:  //子View填满父布局(可用空间)
                    if (selfWidthSpecMode == MeasureSpec.EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, MeasureSpec.EXACTLY);
                    } else {
                        //由于无上限的可用空间无法顶满
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0 ,MeasureSpec.UNSPECIFIED);
                    }
                    break;
                case LayoutParams.WRAP_CONTENT:  //子View自适应,隐含条件不能超过父布局(可用空间)
                    if (selfWidthSpecMode == MeasureSpec.EXACTLY || selfWidthSpecMode == MeasureSpec.AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, MeasureSpec.AT_MOST);
                    } else {
                        //不用考虑额外的限制
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0 ,MeasureSpec.UNSPECIFIED);
                    }
                    break;
                default:  //具体的值
                    childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
                    break;
            }

            childView.measure(childWidthSpec, childHeightSpec);
        }
    }

(2)重写onLayout()来摆放子View

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            childView.layout(childLeft[i], childTop[i], childRight[i], childBottom[i]);
        }
    }

 

(五)自定义View的触摸反馈

        自定义触摸反馈的关键:

  1. 重写 onTouchEvent(),在里面写上你的触摸反馈算法,并返回 true(关键是 ACTION_DOWN 事件时返回 true)。
  2. 如果是会发生触摸冲突的 ViewGroup,还需要重写 onInterceptTouchEvent(),在事件流开始时返回 false,并在确认接管事件流时返回一次 true,以实现对事件的拦截。
  3. 当子 View 临时需要阻止父 View 拦截事件流时,可以调用父 View 的 requestDisallowInterceptTouchEvent() ,通知父 View 在当前事件流中不再尝试通过 onInterceptTouchEvent() 来拦截。

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_34519487/article/details/104076633

智能推荐

C#连接OPC C#上位机链接PLC程序源码 1.该程序是通讯方式是CSharp通过OPC方式连接PLC_c#opc通信-程序员宅基地

文章浏览阅读565次。本文主要介绍如何使用C#通过OPC方式连接PLC,并提供了相应的程序和学习资料,以便读者学习和使用。OPC服务器是一种软件,可以将PLC的数据转换为标准的OPC格式,允许其他软件通过标准接口读取或控制PLC的数据。此外,本文还提供了一些学习资料,包括OPC和PLC的基础知识,C#编程语言的教程和实例代码。这些资料可以帮助读者更好地理解和应用本文介绍的程序。1.该程序是通讯方式是CSharp通过OPC方式连接PLC,用这种方式连PLC不用考虑什么种类PLC,只要OPC服务器里有的PLC都可以连。_c#opc通信

Hyper-V内的虚拟机复制粘贴_win10 hyper-v ubuntu18.04 文件拷贝-程序员宅基地

文章浏览阅读1.6w次,点赞3次,收藏10次。实践环境物理机:Windows10教育版,操作系统版本 17763.914虚拟机:Ubuntu18.04.3桌面版在Hyper-V中的刚安装好Ubuntu虚拟机之后,会发现鼠标滑动很不顺畅,也不能向虚拟机中拖拽文件或者复制内容。在VMware中,可以通过安装VMware tools来使物理机和虚拟机之间达到更好的交互。在Hyper-V中,也有这样的工具。这款工具可以完成更好的鼠标交互,我的..._win10 hyper-v ubuntu18.04 文件拷贝

java静态变量初始化多线程,持续更新中_类初始化一个静态属性 为线程池-程序员宅基地

文章浏览阅读156次。前言互联网时代,瞬息万变。一个小小的走错,就有可能落后于别人。我们没办法去预测任何行业、任何职业未来十年会怎么样,因为未来谁都不能确定。只能说只要有互联网存在,程序员依然是个高薪热门行业。只要跟随着时代的脚步,学习新的知识。程序员是不可能会消失的,或者说不可能会没钱赚的。我们经常可以听到很多人说,程序员是一个吃青春饭的行当。因为大多数人认为这是一个需要高强度脑力劳动的工种,而30岁、40岁,甚至50岁的程序员身体机能逐渐弱化,家庭琐事缠身,已经不能再进行这样高强度的工作了。那么,这样的说法是对的么?_类初始化一个静态属性 为线程池

idea 配置maven,其实不用单独下载Maven的。以及设置新项目配置,省略每次创建新项目都要配置一次Maven_安装idea后是不是不需要安装maven了?-程序员宅基地

文章浏览阅读1w次,点赞13次,收藏43次。说来也是惭愧,一直以来,在装环境的时候都会从官网下载Maven。然后再在idea里配置Maven。以为从官网下载的Maven是必须的步骤,直到今天才得知,idea有捆绑的 Maven 我们只需要搞一个配置文件就行了无需再官网下载Maven包以后再在新电脑装环境的时候,只需要下载idea ,网上找一个Maven的配置文件 放到 默认的 包下面就可以了!也省得每次创建项目都要重新配一次Maven了。如果不想每次新建项目都要重新配置Maven,一种方法就是使用默认的配置,另一种方法就是配置 .._安装idea后是不是不需要安装maven了?

奶爸奶妈必看给宝宝摄影大全-程序员宅基地

文章浏览阅读45次。家是我们一生中最重要的地方,小时候,我们在这里哭、在这里笑、在这里学习走路,在这里有我们最真实的时光,用相机把它记下吧。  很多家庭在拍摄孩子时有一个看法,认为儿童摄影团购必须是在风景秀丽的户外,即便是室内那也是像大酒店一样...

构建Docker镜像指南,含实战案例_rocker/r-base镜像-程序员宅基地

文章浏览阅读429次。Dockerfile介绍Dockerfile是构建镜像的指令文件,由一组指令组成,文件中每条指令对应linux中一条命令,在执行构建Docker镜像时,将读取Dockerfile中的指令,根据指令来操作生成指定Docker镜像。Dockerfile结构:主要由基础镜像信息、维护者信息、镜像操作指令、容器启动时执行指令。每行支持一条指令,每条指令可以携带多个参数。注释可以使用#开头。指令说明FROM 镜像 : 指定新的镜像所基于的镜像MAINTAINER 名字 : 说明新镜像的维护(制作)人,留下_rocker/r-base镜像

随便推点

毕设基于微信小程序的小区管理系统的设计ssm毕业设计_ssm基于微信小程序的公寓生活管理系统-程序员宅基地

文章浏览阅读223次。该系统将提供便捷的信息发布、物业报修、社区互动等功能,为小区居民提供更加便利、高效的服务。引言: 随着城市化进程的加速,小区管理成为一个日益重要的任务。因此,设计一个基于微信小程序的小区管理系统成为了一项具有挑战性和重要性的毕设课题。本文将介绍该小区管理系统的设计思路和功能,以期为小区提供更便捷、高效的管理手段。四、总结与展望: 通过本次毕设项目,我们实现了一个基于微信小程序的小区管理系统,为小区居民提供了更加便捷、高效的服务。通过该系统的设计与实现,能够提高小区管理水平,提供更好的居住环境和服务。_ssm基于微信小程序的公寓生活管理系统

如何正确的使用Ubuntu以及安装常用的渗透工具集.-程序员宅基地

文章浏览阅读635次。文章来源i春秋入坑Ubuntu半年多了记得一开始学的时候基本一星期重装三四次=-= 尴尬了 觉得自己差不多可以的时候 就吧Windows10干掉了 c盘装Ubuntu 专心学习. 这里主要来说一下使用Ubuntu的正确姿势Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的开源GNU/Linux操作系统,Ubuntu 是基于DebianGNU/Linux,支..._ubuntu安装攻击工具包

JNI参数传递引用_jni引用byte[]-程序员宅基地

文章浏览阅读335次。需求:C++中将BYTE型数组传递给Java中,考虑到内存释放问题,未采用通过返回值进行数据传递。public class demoClass{public native boolean getData(byte[] tempData);}JNIEXPORT jboolean JNICALL Java_com_core_getData(JNIEnv *env, jobject thisObj, jbyteArray tempData){ //resultsize为s..._jni引用byte[]

三维重建工具——pclpy教程之点云分割_pclpy.pcl.pointcloud.pointxyzi转为numpy-程序员宅基地

文章浏览阅读2.1k次,点赞5次,收藏30次。本教程代码开源:GitHub 欢迎star文章目录一、平面模型分割1. 代码2. 说明3. 运行二、圆柱模型分割1. 代码2. 说明3. 运行三、欧几里得聚类提取1. 代码2. 说明3. 运行四、区域生长分割1. 代码2. 说明3. 运行五、基于最小切割的分割1. 代码2. 说明3. 运行六、使用 ProgressiveMorphologicalFilter 分割地面1. 代码2. 说明3. 运行一、平面模型分割在本教程中,我们将学习如何对一组点进行简单的平面分割,即找到支持平面模型的点云中的所有._pclpy.pcl.pointcloud.pointxyzi转为numpy

以NFS启动方式构建arm-linux仿真运行环境-程序员宅基地

文章浏览阅读141次。一 其实在 skyeye 上移植 arm-linux 并非难事,网上也有不少资料, 只是大都遗漏细节, 以致细微之处卡壳,所以本文力求详实清析, 希望能对大家有点用处。本文旨在将 arm-linux 在 skyeye 上搭建起来,并在 arm-linux 上能成功 mount NFS 为目标, 最终我们能在 arm-linux 里运行我们自己的应用程序. 二 安装 Sky..._nfs启动 arm

攻防世界 Pwn 进阶 第二页_pwn snprintf-程序员宅基地

文章浏览阅读598次,点赞2次,收藏5次。00为了形成一个体系,想将前面学过的一些东西都拉来放在一起总结总结,方便学习,方便记忆。攻防世界 Pwn 新手攻防世界 Pwn 进阶 第一页01 4-ReeHY-main-100超详细的wp1超详细的wp203 format2栈迁移的两种作用之一:栈溢出太小,进行栈迁移从而能够写入更多shellcode,进行更多操作。栈迁移一篇搞定有个陌生的函数。C 库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 _pwn snprintf

推荐文章

热门文章

相关标签