当前位置: 移动技术网 > IT编程>开发语言>C/C++ > 面试官:怎么实现一个自定义View?

面试官:怎么实现一个自定义View?

2020年11月11日  | 移动技术网IT编程  | 我要评论
文章目录前提自定义View的方式方式1: 继承系统UI控件1 添加布局2 添加自定义属性3 TitleBar代码实现方式2: 继承View / ViewGroup1 重写onMeasure为什么我们需要重写onMeasure?MesureSpec测量模式自定义FlowLayout onMeasure方法说明(看注释 !看助手!看注释!)2 重写onDraw3 重写onLayout自定义FlowLayout onLayout方法说明(看注释 !看注释!看注释!)效果图如下FlowLayout完成代码前提为

前提

为什么要自定义View?
怎么自定义View?

当 Android SDK 中提供的系统 UI 控件无法满足业务需求时,我们就需要考虑自己实现 UI 控件。

自定义View的方式

  1. 继承系统提供的成熟控件(比如 LinearLayout、RelativeLayout、ImageView 等);
  2. 直接继承自系统 View 或者 ViewGroup,并自绘显示内容。

建议:尽量直接使用系统提供的UI控件、或者方式1实现需求效果,因为google提供的UI控件等都已经做了非常完整的边界、特殊case处理。自定义的View可能由于考虑不周全在适配,某些特殊case下出现问题等。

方式1: 继承系统UI控件

举例:继承RelativeLayout实现一个titleBar

1 添加布局

注意:这里使用merge标签,因为后面我们extends RelativeLayout,不需要再套一层

<merge xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tool="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">

  <ImageView
    android:id="@+id/left_button"
    android:layout_width="40dp"
    android:layout_height="56dp"
    android:layout_marginStart="6dp"
    android:paddingStart="5dp"
    android:paddingTop="13dp"
    android:paddingEnd="5dp"
    android:paddingBottom="13dp"/>

  <TextView
    android:id="@+id/title_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerVertical="true"
    android:layout_marginStart="20dp"
    android:layout_marginEnd="20dp"
    android:layout_toStartOf="@+id/right_button"
    android:layout_toEndOf="@id/left_button"
    android:ellipsize="end"
    android:gravity="center"
    android:maxLines="1"
    android:textColor="@color/color_ffffff"
    android:textSize="18sp"
    tool:text="This is tools text"/>

  <ImageView
    android:id="@+id/right_button"
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:layout_alignParentEnd="true"
    android:layout_marginTop="7dp"
    android:layout_marginEnd="6dp"
    android:padding="5dp"/>
</merge>

2 添加自定义属性

在valules的attrs.xml中定义如下自定义属性

<declare-styleable name="TitleBar">
    <attr name="title_bar_title_text" format="string"/>
    <attr name="title_bar_text_color" format="color|reference"/>
    <attr name="title_left_icon" format="reference"/>
    <attr name="title_right_icon" format="reference"/>
</declare-styleable>

name : 是属性名称
format : 代表属性的格式
使用自定义属性的时候需要添加命名空间如:xmlns:app(可以自己定义)

3 TitleBar代码实现

public class TitleBarLayout extends RelativeLayout {
  public TitleBarLayout(Context context) {
    this(context, null);
  }

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

  public TitleBarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    // 引入步骤1中添加的布局
    ViewUtils.inflate(this, R.layout.layout_title_bar, true);
    // 获取自定义属性
    TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.TitleBar);

    Drawable leftDrawable = typedArray.getDrawable(R.styleable.TitleBar_title_left_icon);
    Drawable rightDrawable = typedArray.getDrawable(R.styleable.TitleBar_title_right_icon);
    CharSequence titleText = typedArray.getString(R.styleable.TitleBar_title_bar_title_text);
    int titleTextColor = typedArray.getColor(R.styleable.TitleBar_title_bar_text_color, Color.BLACK);

    // 如果不调用recycle,As会有提示
    typedArray.recycle();

    ImageView leftButton = findViewById(R.id.left_button);
    ImageView rightButton = findViewById(R.id.right_button);
    TextView titleTextView = findViewById(R.id.title_view);

    if (leftDrawable != null) {
      leftButton.setImageDrawable(leftDrawable);
    }
    if (rightDrawable != null) {
      rightButton.setImageDrawable(rightDrawable);
    }
    titleTextView.setText(titleText);
    titleTextView.setTextColor(titleTextColor);
  }
}

方式2: 继承View / ViewGroup

自定义View的时候,一般是3步走:

1 重写onMeasure

测量子View及View本身的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

为什么我们需要重写onMeasure?

如果我们直接在 XML 布局文件中定义好 View 的宽高,然后让自定义 View 在此宽高的区域内显示即可,那么就不需要重写onMeasure了。

但是Android 系统提供了 wrap_contentmatch_parent 属性来规范控件的显示规则。它们分别代表自适应大小和填充父视图的大小,但是这两个属性并没有指定具体的大小,因此我们需要在 onMeasure 方法中过滤出这两种情况,真正的测量出自定义 View 应该显示的宽高大小.

MesureSpec

MesureSpec 定义 :
测量规格,View根据该规格从而决定自己的大小。
MeasureSpec是由一个32位 int 值来表示的。其中该 int 值对应的二进制的高2位代表SpecMode,低30位代表SpecSize

测量模式

EXACTLY:表示在 XML 布局文件中宽高使用 match_parent 或者固定大小的宽高;
AT_MOST:表示在 XML 布局文件中宽高使用 wrap_content;
UNSPECIFIED:父容器没有对当前 View 有任何限制,当前 View 可以取任意尺寸,比如 ListView 中的 item。

自定义FlowLayout onMeasure方法说明(看注释 !看助手!看注释!)

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

    // 宽度测量模式
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    // 高度测量模式
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    // 测量宽度
    int width = MeasureSpec.getSize(widthMeasureSpec);
    // 测量高度
    int height = MeasureSpec.getSize(heightMeasureSpec);

    // 每一行的宽度(FlowLayout当标签长度超过一行后会换行)
    int lineWidth = 0;
    // 最终测量的width
    int resultWidth = 0;
    // 最终测量的height
    int resultHeight = 0;

    // 换行次数
    int lineCount = 1;

    // 遍历所有子View,拿到每一行的最大宽度 和 换行次数
    for (int i = 0; i < getChildCount(); i++) {
      View childAt = getChildAt(i);
      measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
      MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

      lineWidth += childAt.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
       // 换行
      if (lineWidth > width) {
        resultWidth = Math.max(lineWidth - childAt.getMeasuredWidth(), resultWidth);
        lineWidth = 0;
        lineCount++;
      }
    }

    View lastChild = getChildAt(getChildCount() - 1);
    MarginLayoutParams marginParams = (MarginLayoutParams) lastChild.getLayoutParams();
    // 根据换行次数 + 2(第一行和最后一行) * 高度,算出最终的高度
    resultHeight += (lastChild.getMeasuredHeight() + marginParams.topMargin + marginParams.bottomMargin) * (lineCount + 2);

    resultWidth += getPaddingLeft() + getPaddingRight();
    resultHeight += getPaddingTop() + getPaddingBottom();

    // 重写onMeasure必须调用这个方法来保存测量的宽、高
    setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? resultWidth : width,
        heightMode == MeasureSpec.AT_MOST ? resultHeight : height);
  }

2 重写onDraw

绘制自身内容

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

onDraw 方法接收一个 Canvas 类型的参数。Canvas 可以理解为一个画布,在这块画布上可以绘制各种类型的 UI 元素。
Canvas 中每一个绘制操作都需要传入一个 Paint 对象。Paint 就相当于一个画笔,我们可以通过设置画笔的各种属性。

如果不想看Canvas、Paint枯燥的文档,可以搜索Hencoder,看下hencoder大佬的自定义教程,有趣生动。

3 重写onLayout

摆放子View位置(继承ViewGroup必须要重写)

自定义FlowLayout onLayout方法说明(看注释 !看注释!看注释!)

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 获取FlowLayout 宽度
    int parentWidth = getMeasuredWidth();
    // 子View摆放的位置
    int left, top, right, bottom;

    // 一行的宽度
    int lineWidth = 0;
    // 一行的高度
    int lineHeight = 0;

    // 遍历子View 计算它们应该摆放的位置
    for (int i = 0; i < getChildCount(); i++) {
      View childAt = getChildAt(i);

      int childWidth = childAt.getMeasuredWidth();
      int childHeight = childAt.getMeasuredHeight();
      MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

      left = lineWidth + layoutParams.leftMargin;
      right = left + childWidth + layoutParams.rightMargin;
      top = lineHeight + layoutParams.topMargin;
      bottom = top + childHeight + layoutParams.bottomMargin;

      if (right > parentWidth) {
        lineWidth = 0;
        lineHeight += childHeight + layoutParams.topMargin + layoutParams.bottomMargin;

        left = lineWidth + layoutParams.leftMargin;
        right = left + childWidth + layoutParams.rightMargin;
        top = lineHeight + layoutParams.topMargin;
        bottom = top + childHeight + layoutParams.bottomMargin;
      }

      childAt.layout(left, top, right, bottom);

      lineWidth += childWidth + layoutParams.rightMargin + layoutParams.leftMargin;
    }
 }

效果图如下

在这里插入图片描述

FlowLayout完成代码

public class FlowLayout extends ViewGroup {
  public FlowLayout(Context context) {
    super(context);
  }

  public FlowLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

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

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    int lineWidth = 0;

    int resultWidth = 0;
    int resultHeight = 0;

    int lineCount = 1;

    for (int i = 0; i < getChildCount(); i++) {
      View childAt = getChildAt(i);
      measureChild(childAt, widthMeasureSpec, heightMeasureSpec);
      MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

      lineWidth += childAt.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
      if (lineWidth > width) {
        resultWidth = Math.max(lineWidth - childAt.getMeasuredWidth(), resultWidth);
        // 换行
        lineWidth = 0;
        lineCount++;
      }
    }

    View lastChild = getChildAt(getChildCount() - 1);
    MarginLayoutParams marginParams = (MarginLayoutParams) lastChild.getLayoutParams();
    resultHeight += (lastChild.getMeasuredHeight() + marginParams.topMargin + marginParams.bottomMargin) * (lineCount + 2);

    resultWidth += getPaddingLeft() + getPaddingRight();
    resultHeight += getPaddingTop() + getPaddingBottom();

    setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? resultWidth : width,
        heightMode == MeasureSpec.AT_MOST ? resultHeight : height);
  }

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

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int parentWidth = getMeasuredWidth();
    int left, top, right, bottom;

    int lineWidth = 0;
    int lineHeight = 0;

    for (int i = 0; i < getChildCount(); i++) {
      View childAt = getChildAt(i);

      int childWidth = childAt.getMeasuredWidth();
      int childHeight = childAt.getMeasuredHeight();
      MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();

      left = lineWidth + layoutParams.leftMargin;
      right = left + childWidth + layoutParams.rightMargin;
      top = lineHeight + layoutParams.topMargin;
      bottom = top + childHeight + layoutParams.bottomMargin;

      if (right > parentWidth) {
        lineWidth = 0;
        lineHeight += childHeight + layoutParams.topMargin + layoutParams.bottomMargin;

        left = lineWidth + layoutParams.leftMargin;
        right = left + childWidth + layoutParams.rightMargin;
        top = lineHeight + layoutParams.topMargin;
        bottom = top + childHeight + layoutParams.bottomMargin;
      }

      childAt.layout(left, top, right, bottom);

      lineWidth += childWidth + layoutParams.rightMargin + layoutParams.leftMargin;
    }
  }

  @Override
  public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
  }
}

本文地址:https://blog.csdn.net/wen_and_zi/article/details/109624796

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网