Android自定义控件 | View绘制原理(画在哪?)

简介: 从源码的角度分析“定位(layout)”。 位置都是相对的,比如“我在你的右边”、“你在广场的西边”。为了表明位置,总是需要一个参照物。。。

View绘制就好比画画,抛开Android概念,如果要画一张图,首先会想到哪几个基本问题:

  • 画多大?
  • 画在哪?
  • 怎么画?

Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分别藏在:

  • 测量(measure)
  • 定位(layout)
  • 绘制(draw)

这一篇将从源码的角度分析“定位(layout)”。

如果想直接看结论可以移步到第三篇末尾。

这是Android自定义控件系列文章的第二篇,系列文章目录如下:

  1. Android自定义控件 | View绘制原理(画多大?)
  2. Android自定义控件 | View绘制原理(画在哪?)
  3. Android自定义控件 | View绘制原理(画什么?)
  4. Android自定义控件 | 源码里有宝藏之自动换行控件
  5. Android自定义控件 | 小红点的三种实现(上)
  6. Android自定义控件 | 小红点的三种实现(下)
  7. Android自定义控件 | 小红点的三种实现(终结)

如何描述位置

位置都是相对的,比如“我在你的右边”、“你在广场的西边”。为了表明位置,总是需要一个参照物。View的定位也需要一个参照物,这个参照物是View的父控件。可以在View的成员变量中找到如下四个描述位置的参数:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * The distance in pixels from the left edge of this view’s parent
     * to the left edge of this view.
     * view左边相对于父亲左边的距离
     */
    protected int mLeft;
    
    /**
     * The distance in pixels from the left edge of this view‘s parent
     * to the right edge of this view.
     * view右边相对于父亲左边的距离
     */
    protected int mRight;
    
    /**
     * The distance in pixels from the top edge of this view’s parent
     * to the top edge of this view.
     * view上边相对于父亲上边的距离
     */
    protected int mTop;
    
    /**
     * The distance in pixels from the top edge of this view‘s parent
     * to the bottom edge of this view.
     * view底边相对于父亲上边的距离
     */
    protected int mBottom;
    ...
}

View通过上下左右四条线围城的矩形来确定相对于父控件的位置以及自身的大小。 那这里所说的大小和上一篇中测量出的大小有什么关系呢?留个悬念,先看一下上下左右这四个变量在哪里被赋值。

确定相对位置

全局搜索后,找到下面这个函数:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Assign a size and position to this view.
     * 赋予当前view尺寸和位置
     *
     * This is called from layout.
     * 这个函数在layout中被调用
     *
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     * @return true if the new size and position are different than the previous ones
     */
    protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        ...
    }
}

沿着调用链继续往上查找:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    /**
     * Assign a size and position to a view and all of its
     * descendants
     * 将尺寸和位置赋予当前view和所有它的孩子
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     * 子类不应该重载这个方法,而应该重载onLayout(),并且在其中局部所有孩子
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    public void layout(int l, int t, int r, int b) {
        ...
        //为View上下左右四条线赋值
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        ...
        //如果布局改变了则重新布局
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ...
        }
    }
    ...
    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     * 当需要赋予所有孩子尺寸和位置的时候,这个函数在layout中被调用
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * 带有孩子的子类应该重载这个方法并调用每个孩子的layout()
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
}

结合调用链和代码注释,可以得出结论:孩子的定位是由父控件发起的,父控件会在ViewGroup.onLayout()中遍历所有的孩子并调用它们的View.layout()以设置孩子相对于自己的位置。

不同的ViewGroup有不同的方式来布局孩子,以FrameLayout为例:

public class FrameLayout extends ViewGroup {

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }
    
    //布局所有孩子
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        //遍历所有孩子
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //排除不可见孩子
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                //获得孩子在measure过程中确定的宽高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //确定孩子左边相对于父控件位置
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                //确定孩子上边相对于父控件位置
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                //调用孩子的layout(),确定孩子相对父控件位置
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
}

FrameLayout所有的孩子都是相对于它的左上角进行定位,并且在定位孩子右边和下边的时候直接加上了在measure过程中得到的宽和高。

测量尺寸和实际尺寸的关系

FrameLayout遍历孩子并触发它们定位的过程中,会用到上一篇测量的结果(通过getMeasuredWidth()getMeasuredHeight()),并最终通过layout()影响mRightmBottom的值。对比一下getWidth()getMeasuredWidth()

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public final int getWidth() {
        //控件右边和左边差值
        return mRight - mLeft;
    }
    
    /**
     * Like {@link #getMeasuredWidthAndState()}, but only returns the
     * raw width component (that is the result is masked by
     * 获得MeasureSpec的尺寸部分
     * {@link #MEASURED_SIZE_MASK}).
     *
     * @return The raw measured width of this view.
     */
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
}
  • getMeasuredWidth()是measure过程的产物,它是测量尺寸。getWidth()是layout过程的产物,它是布局尺寸。它们的值可能不相等。
  • 测量尺寸只是layout过程中可能用到的关于控件大小的参考值,不同的ViewGroup会有不同的layout算法,也就有不同的使用参考值的方法,控件最终展示尺寸由layout过程决定(以布局尺寸为准)。

总结

  1. 控件位置和最终展示的尺寸是通过上(mTop)、下(mBottom)、左(mLeft)、右(mRight)四条线围城的矩形来描述的。
  2. 控件定位就是确定自己相对于父控件的位置,子控件总是相对于父控件定位,当根布局的位置确定后,屏幕上所有控件的位置都确定了。
  3. 控件定位是由父控件发起的,父控件完成自己定位之后会调用onLayout(),在其中遍历所有孩子并调用它们的layout()方法以确定子控件相对于自己的位置。
  4. 整个定位过程的终点是View.setFrame()的调用,它表示着视图矩形区域的大小以及相对于父控件的位置已经确定。
目录
相关文章
|
4月前
|
Android开发 UED 计算机视觉
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
本文介绍了一款受游戏“金铲铲之战”启发的Android自定义View——线条等待动画的实现过程。通过将布局分为10份,利用`onSizeChanged`测量最小长度,并借助画笔绘制动态线条,实现渐变伸缩效果。动画逻辑通过四个变量控制线条的增长与回退,最终形成流畅的等待动画。代码中详细展示了画笔初始化、线条绘制及动画更新的核心步骤,并提供完整源码供参考。此动画适用于加载场景,提升用户体验。
431 5
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
|
4月前
|
Android开发
Android自定义view之利用PathEffect实现动态效果
本文介绍如何在Android自定义View中利用`PathEffect`实现动态效果。通过改变偏移量,结合`PathEffect`的子类(如`CornerPathEffect`、`DashPathEffect`、`PathDashPathEffect`等)实现路径绘制的动态变化。文章详细解析了各子类的功能与参数,并通过案例代码展示了如何使用`ComposePathEffect`组合效果,以及通过修改偏移量实现动画。最终效果为一个菱形图案沿路径运动,源码附于文末供参考。
|
4月前
|
Android开发 开发者
Android自定义view之利用drawArc方法实现动态效果
本文介绍了如何通过Android自定义View实现动态效果,重点使用`drawArc`方法完成圆弧动画。首先通过`onSizeChanged`进行测量,初始化画笔属性,设置圆弧相关参数。核心思路是不断改变圆弧扫过角度`sweepAngle`,并调用`invalidate()`刷新View以实现动态旋转效果。最后附上完整代码与效果图,帮助开发者快速理解并实践这一动画实现方式。
133 0
|
4月前
|
Android开发 数据安全/隐私保护 开发者
Android自定义view之模仿登录界面文本输入框(华为云APP)
本文介绍了一款自定义输入框的实现,包含静态效果、hint值浮动动画及功能扩展。通过组合多个控件完成界面布局,使用TranslateAnimation与AlphaAnimation实现hint文字上下浮动效果,支持密码加密解密显示、去除键盘回车空格输入、光标定位等功能。代码基于Android平台,提供完整源码与attrs配置,方便复用与定制。希望对开发者有所帮助。
|
4月前
|
XML Java Android开发
Android自定义view之网易云推荐歌单界面
本文详细介绍了如何通过自定义View实现网易云音乐推荐歌单界面的效果。首先,作者自定义了一个圆角图片控件`MellowImageView`,用于绘制圆角矩形图片。接着,通过将布局放入`HorizontalScrollView`中,实现了左右滑动功能,并使用`ViewFlipper`添加图片切换动画效果。文章提供了完整的代码示例,包括XML布局、动画文件和Java代码,最终展示了实现效果。此教程适合想了解自定义View和动画效果的开发者。
216 65
Android自定义view之网易云推荐歌单界面
|
4月前
|
XML 前端开发 Android开发
一篇文章带你走近Android自定义view
这是一篇关于Android自定义View的全面教程,涵盖从基础到进阶的知识点。文章首先讲解了自定义View的必要性及简单实现(如通过三个构造函数解决焦点问题),接着深入探讨Canvas绘图、自定义属性设置、动画实现等内容。还提供了具体案例,如跑马灯、折线图、太极图等。此外,文章详细解析了View绘制流程(measure、layout、draw)和事件分发机制。最后延伸至SurfaceView、GLSurfaceView、SVG动画等高级主题,并附带GitHub案例供实践。适合希望深入理解Android自定义View的开发者学习参考。
537 84
|
4月前
|
前端开发 Android开发 UED
讲讲Android为自定义view提供的SurfaceView
本文详细介绍了Android中自定义View时使用SurfaceView的必要性和实现方式。首先分析了在复杂绘制逻辑和高频界面更新场景下,传统View可能引发卡顿的问题,进而引出SurfaceView作为解决方案。文章通过Android官方Demo展示了SurfaceView的基本用法,包括实现`SurfaceHolder.Callback2`接口、与Activity生命周期绑定、子线程中使用`lockCanvas()`和`unlockCanvasAndPost()`方法完成绘图操作。
107 3
|
4月前
|
Android开发 开发者
Android自定义view之围棋动画(化繁为简)
本文介绍了Android自定义View的动画实现,通过两个案例拓展动态效果。第一个案例基于`drawArc`方法实现单次动画,借助布尔值控制动画流程。第二个案例以围棋动画为例,从简单的小球直线运动到双向变速运动,最终实现循环动画效果。代码结构清晰,逻辑简明,展示了如何化繁为简实现复杂动画,帮助读者拓展动态效果设计思路。文末提供完整源码,适合初学者和进阶开发者学习参考。
Android自定义view之围棋动画(化繁为简)
|
4月前
|
Java Android开发 开发者
Android自定义view之围棋动画
本文详细介绍了在Android中自定义View实现围棋动画的过程。从测量宽高、绘制棋盘背景,到创建固定棋子及动态棋子,最后通过属性动画实现棋子的移动效果。文章还讲解了如何通过自定义属性调整棋子和棋盘的颜色及动画时长,并优化视觉效果,如添加渐变色让白子更明显。最终效果既可作为围棋动画展示,也可用作加载等待动画。代码完整,适合进阶开发者学习参考。
|
4月前
|
传感器 Android开发 开发者
Android自定义view之3D正方体
本文介绍了如何通过手势滑动操作实现3D正方体的旋转效果,基于Android自定义View中的GLSurfaceView。相较于使用传感器控制,本文改用事件分发机制(onTouchEvent)处理用户手势输入,调整3D正方体的角度。代码中详细展示了TouchSurfaceView的实现,包括触控逻辑、OpenGL ES绘制3D正方体的核心过程,以及生命周期管理。适合对Android 3D图形开发感兴趣的开发者学习参考。

热门文章

最新文章