Android面试自定义View

Posted Rose J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android面试自定义View相关的知识,希望对你有一定的参考价值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g9LuQKFd-1621012605452)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620552906833.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bq7NgiZW-1621012605455)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620552437749.png)]

自定义View和自定义ViewGroup的区别在于

自定义View主要是实现onMeasure + onDraw(注重绘制)

自定义ViewGroup主要是实现onMeasure + onLayout(注重布局)

1.继承ViewGourp派生特殊的Layout

(手写流式布局(仿淘宝))

这种方法主要用于实现自定义的布局,即除了LinearLayout,RelativeLayout,FrameLayout这几种系统的布局以外,重新定义一种新布局。注重测量和布局

样例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4sVZqvyD-1621012605457)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620555341941.png)]

demo运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtfsBbS9-1621012605462)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620556138737.png)]

构造函数

//在new一个的时候,只需要传一个参数public FlowLayout(Context context) {   
super(context);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MjJXE5rs-1621012605464)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620523987875.png)]

//反射
//xml解析的时候需要通过反射,反射的时候会应用这个构造函数,来构建对应的java类型的flowLayoutpublic FlowLayout(Context context,AttributeSet attrs) {    
super(context, attrs);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YqzZaFzN-1621012605466)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620524272800.png)]

   //主题style
    //xml解析时如果要用到自定义主题
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

//第四个参数是一个style的资源引用(为view设置style属性),支持view的默认值,只有第三个参数为0或者未在theme中设置时才会生效,这个构造函数几乎不会用到
public FlowLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

重写方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5mTgUNbb-1621012605466)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620536551661.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ziCiUIlH-1621012605468)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620538748810.png)]

onMeasure

问题:如下,父布局宽度为wrap_content,子布局为match_parent,子布局TextView的宽度为多宽?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QvooUZch-1621012605470)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620536747237.png)]

答案是:不知道,但是我们可以通过onMeasure的实现逻辑去知道。

onMeasure

是用来确定子View大小

确定子View坐标

最后就可以确定自己的大小(按照子View的坐标,计算并且相加,最终得到的就是view的大小

ps:ViewGroup 和view 大小相当于,买房子的合同面积(建筑面积)和实际使用面积)、

 //度量:具体规划
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

在onMeasure传进来的是父布局给的大小宽高,实际的使用大小由子布局来确定。

所以Measure的作用是 通过测量孩子布局的大小,来计算出自己的大小

View的层级结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpO2A6BW-1621012605473)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620538875249.png)]

DecorView会调用ViewGroup的onMeasure函数,然后在onMeasure函数中调用子元素的mesure函数,然后子元素又会调用自己的onMeasure函数,又会在自己onMeasure函数中调用子元素的mesure函数,层层遍历,保证所有的子元素都被测量,

1.测量子元素宽高

在onMeasure方法中,先对所有行和每一行的行高初始化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aa1hOWL-1621012605474)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620554494261.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ugQ6LZyF-1621012605476)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620554486396.png)]

initMeasureParams();//初始化

在onMeasure方法中,我们首先要拿到子元素的数量,再通过getLayoutParams去获取子元素的宽高,我们不会获取到具体的数值,这里引进了一个MeasureSpec

MeasureSpec是什么

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l2HD95WJ-1621012605477)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620541010896.png)]

MeasureSpec是View中的内部类,基本都是二进制运算,由于int是32位的,用高两位表示mode,低30位表示size,MODE_SHIFT=30的作用是移位

UNSPECIFIED:不对View大小做限制,系统使用

EXACTLY:确切的大小:如100dp

AT_MOST:大小不可超过某数值,如:match_Parent,最大不能超过父布局

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QBHq0lO9-1621012605481)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620541837896.png)]

MeasureSpec通过父布局提供的大小,减去padding的约束,然后再在所剩区域内获取子元素的大小 最终计算得出的MeasureSpec,然后再通过measure方法测量校准后,获取子view的宽高(子View可能是match_Parent或者wrap_content属性)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U8UwEKmW-1621012605483)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620542769503.png)]

然后通过子view的宽度和来和父布局给的宽度做判断是否需要换行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-radcO5Sg-1621012605485)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620554301781.png)]

然后每次记录下自己的宽高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EB22YPVa-1621012605486)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620554327685.png)]

因为最后一个元素会跳出判断,所以要特别处理一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B1Z0cF7u-1621012605488)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620554364384.png)]

2.确定自己大小

通过子view的测量结果,来重新测量自己的ViewGroup

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsqXqmZi-1621012605490)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620554625411.png)]

onLayout

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zzLGoRF1-1621012605492)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620547867224.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RFc2vpWf-1621012605493)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620549220295.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L1MbUEgi-1621012605495)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620555024368.png)]

根据子元素的坐标位置,一行一行的确定子元素

onDraw

流式布局没有特殊的绘制,主要注重测量和布局

全部代码
public class FlowLayout extends ViewGroup {

    private static  final  String TAG ="FlowLayout";
    private int mHorizontalSpacing =dp2px(16);


    private int mVerticalSpacing =dp2px(8);//可以加入空格变量
    private static  int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp, Resources.getSystem().getDisplayMetrics());

    }

    private List<List<View>> allLines;//记录所有的行,一行一行的存储,用于layout
    List<Integer> lineHeights = new ArrayList<>();// 记录每一行的行高,用于layout


    //在new一个的时候,只需要传一个参数
        public FlowLayout(Context context)
        {
            super(context);
        }

    //反射
    //xml解析的时候需要通过反射,反射的时候会应用这个构造函数,来构建对应的java类型的flowLayout
    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    //主题style
    //xml解析时如果要用到自定义主题
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private  void  initMeasureParams(){
        allLines =new ArrayList<>();
        lineHeights =new ArrayList<>();
    }
    //度量:具体规划
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //度量孩子
        initMeasureParams();//初始化

        int childCount =getChildCount();//首先拿到子元素的数量
        int paddingLeft =getPaddingLeft();
        int paddingRight=getPaddingRight();
        int paddingTop=getPaddingTop();
        int paddingBottom=getPaddingBottom();//拿到内边距

        List<View> lineView =new ArrayList<>();//保存一行中的所有的View
        int lineWidthUsed =0;//记录这行已经使用了多宽的size
        int lineHeightUsed =0;//一行的行高

        //基于父布局给的宽高
        int selfWidth =MeasureSpec.getSize(widthMeasureSpec);//ViewGroup解析的宽度
        int selfHigjt=MeasureSpec.getSize(heightMeasureSpec);//ViewGroup解析的高度

        int parentNeededWidth=0;// measure 过程中,子View要求的父ViewGroup的宽
        int parentNeededHeight=0;// measure 过程中,子View要求的父ViewGroup的高

        for (int i=0;i<childCount;i++){
            View childView=getChildAt(i);//拿到子元素引用
            LayoutParams childLP=childView.getLayoutParams();//拿到 子元素设置的宽高值   MATCH_PARENT = -1    WRAP_CONTENT = -2  -->转为size(不会有明确的size  手机大小不同) -->MeasureSpec
            int childWidthMeasureSpec=getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width);
            int childHightMeasureSpec=getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height);
            childView.measure(childWidthMeasureSpec,childHightMeasureSpec);

            //获取子View的宽高
            int childMeasuredWidth=childView.getMeasuredWidth();
            int childMeasuredHight=childView.getMeasuredHeight();


            //如果需要换行
            if (childMeasuredWidth +lineWidthUsed+mHorizontalSpacing>selfWidth){

                allLines.add(lineView);//记录行
                lineHeights.add(lineHeightUsed);//记录行高
                //一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
                parentNeededHeight=parentNeededHeight + lineHeightUsed;
                parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed);
                lineView =new ArrayList<>();
                lineWidthUsed =0;
                lineHeightUsed =0;//重置
            }
            lineView.add(childView);
            //每行都有自己的宽和高
            lineWidthUsed=lineWidthUsed + childMeasuredWidth ;
            lineHeightUsed=lineHeightUsed + childMeasuredHight ;

            //处理最后一行数据
            if (i == childCount-1){

                allLines.add(lineView);//记录行
                lineHeights.add(lineHeightUsed);//记录行高
                parentNeededHeight=parentNeededHeight + lineHeightUsed;
                parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed);
            }
        }


        //根据子View的测量结果,来重新测量自己的ViewGroup
        //作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给它提供的宽高来测量
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);

        int realWidth=(widthMode ==MeasureSpec.EXACTLY)?selfWidth:parentNeededWidth;
        int realHeight=(heightMode ==MeasureSpec.EXACTLY)?selfHigjt:parentNeededHeight;

        //确定自己的大小
        setMeasuredDimension(realWidth,realHeight);
    }

    //布局:所有的子View进行布局
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {


            int curL =getPaddingLeft();//左边距
            int curT =getPaddingTop();//上边距
            int lineCount =allLines.size();//获取layout中的行数
            for (int i=0;i<lineCount;i++){
                List<View> lineViews =allLines.get(i);//获取行
                int lineHeight=lineHeights.get(i);
                for (int j =0 ;j<lineViews.size();j++){
                    View view=lineViews.get(j);//获取元素
                    int left=curL;//左部起始坐标
                    int top=curT;//顶部起始坐标
//
//                    int right=left+view.getWidth();
//                    int bottom=top+view.getHeight();
                    int right=left+view.getMeasuredWidth();
                    int bottom=top+view.getMeasuredHeight();
                    view.layout(left,top,right,bottom);
                    curL=right+mHorizontalSpacing;

                }
                curL =getPaddingLeft();
                curT =curT +lineHeight;
            }


    }


    //绘制:将View绘制到屏幕上
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

2.继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。注重绘制。

这种方式需要自己支持wrap_content,并且padding也需要自己处理

Paint画笔标志位Flag含义

Paint.ANTI_ALIAS_FLAG :抗锯齿标志
Paint.FILTER_BITMAP_FLAG : 使位图过滤的位掩码标志
Paint.DITHER_FLAG : 使位图进行有利的抖动的位掩码标志
Paint.UNDERLINE_TEXT_FLAG : 下划线
Paint.STRIKE_THRU_TEXT_FLAG : 中划线
Paint.FAKE_BOLD_TEXT_FLAG : 加粗
Paint.LINEAR_TEXT_FLAG : 使文本平滑线性扩展的油漆标志
Paint.SUBPIXEL_TEXT_FLAG : 使文本的亚像素定位的绘图标志
Paint.EMBEDDED_BITMAP_TEXT_FLAG : 绘制文本时允许使用位图字体的绘图标志

绘制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXmf3214-1621012605495)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620558306034.png)]

运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I1trxHzb-1621012605496)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620558250287.png)]

margin属性有效

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZcS1CXBx-1621012605497)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620560432634.png)]

padding和wrap_content

padding属性和wrap_content属性无效

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HFQFbrao-1621012605499)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620560676171.png)]

在绘制的时候考虑padding,padding属性和wrap_content属性生效

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IwLpZj70-1621012605500)(C:\\Users\\Lenovo\\AppData\\Roaming\\Typora\\typora-user-images\\1620561043726.png)]

上述代码中心思想就是在绘制的时候考虑到View四周的空白即可,其中圆心和半径都会考虑到View四周的padding,从而做相应的调整

以上是关于Android面试自定义View的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义View

Android:在片段内膨胀自定义视图

2019年Android岗位BAT等大厂面试题

android自定义的dialog怎么设置view

android自定义的dialog怎么设置view

android文字横向滚动的自定义view