Android进阶 - 自定义View

参考链接

View和ViewGroup

View是Android所有控件的基类,同时ViewGroup也是继承自View。ViewGroup作为View或者ViewGroup这些组件的容器,派生了 多种布局控件子类,比如LinearLayout、RelativeLayout等

View的部分继承关系

Android坐标系

在这里插入图片描述

Android视图坐标系

在这里插入图片描述

View获取自身宽高

  • getHeight():获取View自身高度
  • getWidth():获取View自身宽度

MotionEvent提供的方法

我们看上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法:

  • getX():获取点击事件距离控件左边的距离,即视图坐标
  • getY():获取点击事件距离控件顶边的距离,即视图坐标
  • getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
  • getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标

View滑动的六种方法

layout(int l, int t, int r, int b)

通过修改View的left、top、right、bottom这四种属性来控制View的坐标。

传进来里面的四个参数分别是View的四个点的坐标,它的坐标不是相对屏幕的原点,而且相对于它的父布局来说的。

public boolean onTouchEvent(MotionEvent event) {
        //获取到手指处的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                //计算移动的距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //调用layout方法来重新放置它的位置
                layout(getLeft()+offsetX, getTop()+offsetY,
                        getRight()+offsetX , getBottom()+offsetY);
                break;
        }

        return true;
    }

offsetLeftAndRight()与offsetTopAndBottom()

将ACTION_MOVE中的代码替换成如下代码:

case MotionEvent.ACTION_MOVE:
    //计算移动的距离
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    //对left和right进行偏移
    offsetLeftAndRight(offsetX);
    //对top和bottom进行偏移
    offsetTopAndBottom(offsetY);
    break;

LayoutParams(改变布局参数)

LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局的参数从而达到了改变View的位置的效果。同样的我们将ACTION_MOVE中的代码替换成如下代码:

LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
 layoutParams.leftMargin = getLeft() + offsetX;
 layoutParams.topMargin = getTop() + offsetY;
 setLayoutParams(layoutParams);

因为父控件是LinearLayout,所以我们用了LinearLayout.LayoutParams,如果父控件是RelativeLayout则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,我们还可以用ViewGroup.MarginLayoutParams来实现:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
 layoutParams.leftMargin = getLeft() + offsetX;
 layoutParams.topMargin = getTop() + offsetY;
 setLayoutParams(layoutParams);

scollTo与scollBy

scollTo(x,y)表示移动到一个具体的坐标点
scollBy(dx,dy)则表示移动的增量为dx、dy。
其中scollBy最终也是要调用scollTo的。scollTo、scollBy移动的是View的内容。

如果在ViewGroup中使用则是移动他所有的子View/子控件

Scroller
我们用scollTo/scollBy方法来进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用Scroller来实现有过度效果的滑动。

Activity的构成

一个Activity包含一个window对象,这个对象是由PhoneWindow来实现的,PhoneWindow将DecorView做为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域一个是TitleView一个是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的。
在这里插入图片描述

  • Activity 并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的
    是 Window。一个 Activity 包含了一个 Window,Window 才是真正代表一个窗口。
    Activity 就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来
    与 Window、以及 View 进行交互。

  • Window是视图的承载器,内部持有一个 DecorView,而这个DecorView才是 view的根布局。Window 是一个抽象类,实际在 Activity 中持有的是其子类PhoneWindow。PhoneWindow 中有个内部类 DecorView,通过创建 DecorView 来加载 Activity 中设置的布局 R.layout.activity_main。Window 通过WindowManager 将 DecorView 加载其中,并将 DecorView 交给 ViewRoot,进行视图绘制以及其他交互。
    Window 负责窗口管理(实际是子类 PhoneWindow),窗口的绘制和渲染交给 DecorView完成;

  • DecorView 是 FrameLayout 的子类,它可以被认为是 Android 视图树的根节点视
    图。DecorView 作为顶级 View,一般情况下它内部包含一个竖直方向的
    LinearLayout,在这个 LinearLayout 里面有上下三个部分,上面是个 ViewStub,延
    迟加载的视图(应该是设置 ActionBar,根据 Theme 设置),中间的是标题栏(根 据 Theme 设置,有的布局没有),下面的是内容栏。

MeasureSpec

参考-Android开发艺术探索

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配。

MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一 起才能决定View的MeasureSpec,从而进一步决定View的宽/高。对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的 LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。

MeasureSpec和LayoutParams的对应关系
在这里插入图片描述

前面已经提到,对于普通View,其MeasureSpec由父容器的 MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和View本身不同的LayoutParams, View就可以有多种MeasureSpec。这里简单说一下,当View采用固定宽/高的时候,不管父容器的 MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。当View的 宽/高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩 余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当 View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不 能超过父容器的剩余空间。可能读者会发现,在我们的分析中漏掉了UNSPECIFIED模式,那是因为这个 模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。

通过上图可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定出 子元素的MeasureSpec了,有了MeasureSpec就可以进一步确定出子元素测量后的大小了。

自定义view学习

刘望舒-Android View体系
Android自定义View全解
自定义View,有这一篇就够了

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()
测量模式表示意思
UNSPECIFIED未指定模式,父容器没有对当前View有任何限制,当前View可以任意取尺寸
EXACTLY精确模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是specSize的值。
AT_MOST最大模式,对应于wrap_comtent属性,只要尺寸不超过父控件允许的最大尺寸就行。

而上面的测量模式跟我们的引用自定义View时在xml中设置wrap_content、match_parent以及写成固定的尺寸有什么对应关系呢?

match_parent—>EXACTLY。怎么理解呢?match_parent就是要利用父布局给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。

wrap_content—>AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父布局给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

固定尺寸(如100dp)—>EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

 private int getMySize(int defaultSize, int measureSpec) {
        int mySize = defaultSize;

        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        switch (mode) {
            case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
                mySize = defaultSize;
                break;
            }
            case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
                //我们将大小取最大值,你也可以取其他值
                mySize = size;
                break;
            }
            case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
                mySize = size;
                break;
            }
        }
        return mySize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMySize(100, widthMeasureSpec);
        int height = getMySize(100, heightMeasureSpec);

        if (width < height) {
            height = width;
        } else {
            width = height;
        }

        setMeasuredDimension(width, height);
}

  • draw()步骤
public void draw(Canvas canvas) {
		...
		//步骤1:绘制View的背景
        drawBackground(canvas);
        ...
        //步骤2:如果需要的话,保存canvas的涂层,为fading做准备
        saveCount = canvas.getSaveCount();
        ...
        canvas.saveLayer(left,top,right, top + length, null, flags);
        ...
        //步骤3:绘制View的内容
        onDraw(canvas);      
        ...
        //步骤4:绘制View的子View
        dispatchDraw(canvas)
        ...
        //步骤5:如果需要的话,绘制View的fading边缘并恢复图层
        canvas.drawRect(left, top, right, top +length, p);
        ...
        canvas.restoreToCount(saveCount);
        ...
		//步骤6: 绘制View的装饰(例如滚动条)
		onDrawScrollBars(canvas)
		...

Android 代码动态布局 LayoutParams 使用

Android 代码动态布局 LayoutParams 使用
LayoutParams 的作用是:子控件告诉父控件,自己要如何布局。

inflate方法

public View inflate(int resource, ViewGroup root, boolean attachToRoot)
  • resource,要添加的 Layout
  • root,父布局
  • attachToRoot是否调用addview添加到父布局
  1. 如果 root 为 null,attachToRoot 将失去作用,设置任何值都没有意义。
  2. 如果 root 不为 null,attachToRoot 设为 true,则会给加载的布局文件的指定一个父布局,即 root。
  3. 如果 root 不为 null,attachToRoot 设为 false,则会将布局文件最外层的所有 layout 属性进行设置,当该 view 被添加到父 view 当中时,这些 layout属性会自动生效。
  4. 在不设置 attachToRoot 参数的情况下,如果 root 不为 null,attachToRoot 参数默认为 true。
layout_width 和 layout_height

它们其实是用于设置 View 在布局中的大小的,也就是说,首先 View 必须存在于一个布局中,layout_width 和 layout_height 才能生效。
平时在 Activity中指定布局文件的时候,最外层的那个布局是可以指定大小的呀,layout_width和 layout_height 都是有作用的。确实,这主要是因为,在 setContentView()
方法中,Android 会自动在布局文件的最外层再嵌套一个 FrameLayout,所以layout_width 和 layout_height 属性才会有效果。

如何自定义控件

  • 1.自定义属性的声明和获取
  • 2.分析需要的自定义属性

    在res/values/attrs.xml定义声明
    在layout文件中进行使用
    在View的构造方法中进行获取
  • 3.测量onMeasure
    布局onLayout(ViewGroup)
  • 4.绘制onDraw
  • 5.处理点击事件

状态的恢复与保存

Android 布局优化之 include、merge、ViewStub

include标签
include 就是为了解决重复定义相同布局的问题。
include 使用注意

1.一个xml布局文件有多个include标签需要设置ID,才能找到相应子View的控件,否则只能找到第一个include的layout布局,以及该布局的控件
2.include标签如果使用layout xx属性,会覆盖被include的xml文件根节点对应的layoutxx属性,建议在include标签调用的布局设置好宽高位置,防止不必要的bug
3.include 添加id,会覆盖被include的xml文件根节点ID,这里建议include和被include覆盖的xml文件根节点设置同名的ID,不然有可能会报空指针异常
4.如果要在include标签下使用RelativeLayout,如layout_margin等其他属性,记得要同时设置layout_width和 layout_height,不然其它属性会没反应

merge 标签
merge标签主要用于辅助 include标签,在使用 include后可能导致布局嵌套过多,多余的 layout 节点或导致解析变慢(可通过 hierarchy viewer 工具查看布局的嵌套情况)。官方文档说明:merge 用于消除视图层次结构中的冗余视图,例如根布局是Linearlayout,那么我们又 include 一个 LinerLayout 布局就没意义了,反而会减慢 UI 加载速度。
merge 官方文档

merge 标签使用注意

1.因为merge标签井不是View,所以在通过LayoutInflate.inflate()方法渲染的时候,第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点. 2.因为merge不是View,所以对merge标签设置的所有属性都是无效的.
3.注意如果include的layout用了merge,调用include的根布局也使用了merge标签,那么就失去布局的属性了 4. merge标签必须使用在根布局
5.ViewStub标签中的layout布局不能使用merge标签

ViewStub 标签
ViewStub 标签最大的优点是当你需要时才会加载,使用它并不会影响 UI 初始化时的性能.各种不常用的布局像进度条、显示错误消息等可以使用 ViewStub标签,以减少内存使用量,加快渲染速度.ViewStub 是一个不可见的,实际上是把宽高设置为 0 的 View.效果有点类似普通的 view.setVisible(),但性能体验提高不少

ViewStub 标签使用注意点

  1. ViewStub标签不支持merge标签
  2. ViewStub的inflate只能被调用一次,第二次调用会抛出异常,setVisibility可以被调用多次,但不建议这么做(ViewStub 调用过后,可能被GC掉,再调用setVisibility()会报异常)
  3. 为ViewStub赋值的android∶layout_XX属性会替换待加载布局文件的根节点对应的属性

Space 组件

在 ConstraintLayout 出来前,我们写布局都会使用到大量的 margin 或padding,但是这种方式可读性会很差,加一个布局嵌套又会损耗性能鉴于这种情况,我们可以使用 space,使用方式和 View一样,不过主要用来占位置,不会有任何显示效果

Android 优化之路(一)布局优化

自定义View的分类

  1. 继承View重写onDraw方法
    这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要 静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。 采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

  2. 继承ViewGroup派生特殊的Layout
    这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统 的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这 种方法来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

  3. 继承特定的View(比如TextView)
    这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实 现。这种方法不需要自己支持wrap_content和padding等。

  4. 继承特定的ViewGroup(比如LinearLayout)
    这种方法也比较常见,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实 现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区 别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。

自定义View须知

  1. 让View支持wrap_content
    这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。

解决方案:在onMeasure中添加对wrap_content情况的判断,为wrap_content可以设置一个默认大小。

@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
	int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 
	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 
	int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 
	//wrap_content时设置默认大小为200
	if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(200,200);
	} else if (widthSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(200,heightSpecSize);
	} else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize,200);
    }
}
  1. 如果有必要,让你的View支持padding
    这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用 的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其 造成的影响,不然将导致padding和子元素的margin失效。

解决方案:针对padding的问题,也很简单,只要在绘制的时候考虑一下padding即可,因此我们需要对 onDraw稍微做一下修改,修改后的代码如下所示。

protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingLeft();
final int paddingTop = getPaddingLeft();
final int paddingBottom = getPaddingLeft();
int width = getWidth() -paddingLeft -paddingRight;
int height = getHeight() -paddingTop -paddingBottom;
int radius = Math.min(width,height) / 2;
canvas.drawCircle(paddingLeft + width / 2,paddingTop + height/2,radius, mPaint);
}
  1. 尽量不要在View中使用Handler,没必要
    这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确地
    要使用Handler来发送消息。

  2. View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
    这一条也很好理解,如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时 机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调 用 , 和 此 方 法 对 应 的 是 onAttachedToWindow , 当 包 含 此 View 的 Activity 启 动 时 , View 的 onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及 时处理这种问题,有可能会造成内存泄漏。

  3. View带有滑动嵌套情形时,需要处理好滑动冲突

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值