自定义 View —— 基本知识准备

本文深入解析自定义View的必要性及实现过程,涵盖构造函数、onMeasure、onDraw、onTouchEvent方法,解决ScrollView嵌套ListView显示不全的问题,以及如何使用自定义属性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自定义 View —— 知识准备

目录

自定义 View —— 知识准备

一、为什么要自定义 View?

二、自定义 view 构造函数的调用

二、onMeasure 方法

2.1 由于 测量模式,引起的 Scrollview 嵌套 ListView 显示不全的问题(源码)

三、onDraw 方法

四、onTouchEvent 方法

五、自定义属性


一、为什么要自定义 View?

当 Android 系统内置的 View 无法实现我们的需求,我们就需要根据需求写一个想要的 View。

二、自定义 view 构造函数的调用

    /**
     * TextView textView = new TextView(this);
     * 会在代码中 new 的时候调用
     */
    public TextView(Context context) {
        super(context);
    }
    /**
        <com.yuan.customview.day01.TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="111"/>
     *  在布局 layout 中使用
     */
    public TextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    /**
     *    <com.yuan.customview.day01.TextView
     *         style="@style/TextView_Default"
     *         android:text="111"/>
     *
     *  在布局 layout 中使用,但是会有 style,多个布局中,有相同 style 时,写一个 style 是一个    
     *  很好的方式
     */
    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

二、onMeasure 方法

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 布局的宽高 都是由这个方法指定
        // 指定控件的宽高,需要测量
        // 获取宽高的测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getMode(heightMeasureSpec);
        /**
         *  3 种模式
         *  MeasureSpec.AT_MOST:在布局中指定了 wrap_content
         *  MeasureSpec.EXACTLY:在布局中指定确切的值 150dp ,match_parent
         *  MeasureSpec.UNSPECIFIED:尽可能的大 很少会用到 ListView,ScrollView 在测量子布局 
         *  的时候会调用 UNSPECIFIED
         */
    }

2.1 由于 测量模式,引起的 Scrollview 嵌套 ListView 显示不全的问题(源码)

首先,我们看一下 ScrollView 的 onMeasure 方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 继承父类的方法
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mFillViewport) {
            return;
        }
        .......
}

接着,我们看父类的 onMeasure 方法,也就是 FrameLayout 的 onMeasure 方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        ......

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                // 循环查询自己的子 View,如果子 View 不是 GONE,就执行下面这个方法
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

而 measureChildWithMargins 方法,ScrollView 中重写了该方法,接下来,我们看一下 ScrollView 的 measureChildWithMargins 方法

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                heightUsed;
        // 在这里,你会发现,对 childHeightMeasureSpec 进行了设值,设置他的高度的测量模式
        // heightMode 为 MeasureSpec.UNSPECIFIED 模式
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);
        // 紧接着就进入了子 View 的OnMeasure 方法,也就是 ListView 的 onMeasure 方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

接着看 ListView 的 onmeasure 方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        // 这里 通过 MeasureSpec.getMode() 将 ScrollView 传过来的heightMode 获取到
        // 也就是 MeasureSpec.UNSPECIFIED 模式
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        
        // 省略一些代码,看我们想看的代码
        ......
        
        // 之后就会进入 这个 if 判断
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            // 这里获取到 ListView 的高度 = top + bottom + 一个 子view 的高度
            // 也就是导致了 ListView 只会显示一个 item 高度
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }
        
        // ------ 其实我们需要的是进入这个 if 判断中
        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            // measureHeightOfChildren 是循环获取 item 的高度,并相加
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }
        
        // 获取完宽、高后,进行设置
        setMeasuredDimension(widthSize, heightSize);

        mWidthMeasureSpec = widthMeasureSpec;
    }

所以,就有了解决方案:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 重新修改 ListView 的 heightMode
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

解释一下为什么这么写:

MeasureSpec.makeMeasureSpec(int size,int mode)
第二个参数测量模式 修改为 MeasureSpec.AT_MOST ,应该好理解,因为我们需要进入 ListView 的 onmeasure 方法中 
if (heightMode == MeasureSpec.AT_MOST) 这个判断中。

第一个参数为什么是 Integer.MAX_VALUE >> 2?

先说一下是什么意思,就是 将 Integer.MAX_VALUE 右移两位。

再说一下 MeasureSpec,MeasureSpec 其实是一个 32 位的 int 值,高两位代表 SpecMode,也就是测量模式,低 30 位代表 SpecSize 也就是测量的值。

而在 MeasureSpec.makeMeasureSpec(int size,int mode) 这个方法中,我们已经指定了 高两位 的 mode,剩下的就是值的大小,Integer.MAX_VALUE 表示的是一个 32 位的值,所以我们需要将它右移两位,变成 30 位的值,这样 + 高两位的mode 就等于  32位了。

举个简单的例子吧:简单可以理解为 32 - 2 = 30

三、onDraw 方法

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 用于绘制
        // 画文本
        canvas.drawText();
        // 画弧
        canvas.drawArc();
        // 画圆
        canvas.drawCircle();
        
        // .....等等
    }

四、onTouchEvent 方法

   /**
     * 处理与用户交互的事件,手指触摸等等
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                // 手指按下
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指滑动
                break;
            case MotionEvent.ACTION_UP:
                // 手指抬起
                break;
        }
        return super.onTouchEvent(event);
    }

五、自定义属性

 1、在res 下的 values 中创建 attrs.xml 资源文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 自定义属性,就是用来配置自定义 View 的    -->
    <!-- 自定义属性的 name 最好就是 自定义 View 的名字 -->
    <declare-styleable name="TextView">
        <!-- name 属性名称 ,format 格式 -->
        <!-- 格式: string 文字,color 颜色,dimension 宽高、字体大小,
                    integer 数字,reference 资源(drawable) -->
        <attr name="CustomText" format="string"/>
        <attr name="CustomColor" format="color"/>
        <attr name="CustomTextSize" format="dimension"/>
        <attr name="CustomMaxLength" format="integer"/>
        <!-- 枚举 -->
        <attr name="inputType">
            <enum name="number" value="1"/>
            <enum name="text" value="2"/>
            <enum name="password" value="3"/>
        </attr>
    </declare-styleable>
</resources>

2、在 layout 布局中使用(先在 layout 中声明命名空间,然后在自己的自定义 View 中使用)

 命名空间:xmlns:app="http://schemas.android.com/apk/res-auto"

  
     <com.yuan.customview.day01.TextView
        style="@style/TextView_Default"
        app:CustomText="111"
        app:CustomColor="@android:color/holo_blue_bright"/>

3、在自己写的自定义 View 中获取属性

public class TextView extends View {

    private String mText;
    private int mTextSize = 14;
    private int mTextColor = Color.BLACK;

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

    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    // 使用 this,最终调用这个构造方法
    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 获取自定义属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView);
        mText = array.getString( R.styleable.TextView_CustomText);
        mTextSize = array.getDimensionPixelSize(R.styleable.TextView_CustomTextSize, mTextSize);
        mTextColor = array.getColor(R.styleable.TextView_CustomColor, mTextColor);
        // 记得先回收
        array.recycle();
    }
}

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值