欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

重学Android - 自定义View

程序员文章站 2022-06-27 18:38:55
View和ViewGroupView是Android所有控件的基类,同时ViewGroup也是继承自View。ViewGroup作为View或者ViewGroup这些组件的容器,派生了 多种布局控件子类,比如LinearLayout、RelativeLayout等Android坐标系Android视图坐标系View获取自身宽高getHeight():获取View自身高度getWidth():获取View自身宽度MotionEvent提供的方法我们看上图那个深蓝色的点,假设就是我们触...

参考链接

View和ViewGroup

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

重学Android - 自定义View

Android坐标系

重学Android - 自定义View

Android视图坐标系

重学Android - 自定义View

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中的。
重学Android - 自定义View

MeasureSpec

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

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

MeasureSpec和LayoutParams的对应关系
重学Android - 自定义View

前面已经提到,对于普通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的分类

  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带有滑动嵌套情形时,需要处理好滑动冲突

本文地址:https://blog.csdn.net/qq_40595341/article/details/109643302