Android View 体系竟然还能这么理解?

发表于 3年以前  | 总阅读数:3547 次

来源 https://juejin.im/post/5e6e0b91f265da5716712288

0前言

很多小伙伴可能在学习view的绘制流程源码的时候有点抓不住重点,所以在分析代码的时候绕来绕去脑袋晕乎乎的。今天我就来给大家化繁为简,只关注它最核心的东西。

从数据结构与算法还有设计模式的角度带领大家真正去掌握。我这篇文章旨在让大家能更深刻理解View绘制流程的设计,不涉及具体的细节。

最好的效果是大家先看这篇文章,然后根据文中介绍的知识点去自行查看源码。或者感到吃力的话可以结合别的大牛写的文章去看源码。^_^

1先修知识点

首先,View体系的数据结构就是树形结构。

ViewGroup继承View,而且ViewGroup持有View的引用,所以这不就是一个树的节点嘛。数据结构跟他的算法是相关的,所以至少你要掌握树的遍历,尤其是树的先序遍历,也就是深度遍历。

在view体系设计中也涉及到了几个设计模式,分别是组合模式,责任链模式,模板方法模式。(当然还有其他如观察者模式,适配器模式等等,不在这次的讨论范围。)

组合模式

如果想实现一个树状的关系,那么就可以使用组合模式。如View和ViewGroup的关系,ViewGroup继承于View,同时也含有子View的引用集合。组合模式一般用于树形结构,所以在这里不需要展开。你只需要知道,View体系本身就是组合模式的体现。

责任链模式

如果想实现一个调用可以让多个类都有机会去处理,那么可以使用责任链模式。类Node含有一个自己的引用,相当于一个链表指针,指向下一个节点。

class Node {
    public String name;
    public Node next;

    public Node(String name) {
        this.name = name;
    }

    public void operate(int num) {
        //1.自己先来处理
        System.out.println(String.format("我是节点%s,我在处理:%d", name, num));
        //2.分发给下一节点处理
        if (next != null) {
            next.operate(num);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Node[] nodes = new Node[5];
        Node head = nodes[0] = new Node("0");
        for (int i = 1; i < 5; i++) {//构造的链表为:0->1->2->3->4
            nodes[i] = new Node(i + "");
            head.next = nodes[i];
            head = nodes[i];
        }
        head = nodes[0];
        head.operate(100);
    }
}
结果为:
我是节点0,我在处理:100
我是节点1,我在处理:100
我是节点2,我在处理:100
我是节点3,我在处理:100
我是节点4,我在处理:100

通过把每个处理者看成是链表上面的一个节点,实现一个调用可以分发给多个处理者去处理。

模板方法模式

如果某一个功能逻辑的流程是比较固定的,但是有一定的步骤,那么可以通过模板方法模式把具体步骤交给子类去实现。

这个怎么理解呢?

以上面责任链模式为例,每个节点的operate的流程是固定的:

1.自己处理消息, 2.把消息分发给下一个节点。

但是可以发现上面的例子有点鸡肋,因为每个Node节点的处理是完全一样的,这看起来没什么意义。

好吧,那结合模板方法模式来进行一个改造。

abstract class Node {
    public String name;
    public Node next;

    public Node(String name) {
        this.name = name;
    }

    public void operate(int num) {
        //1.自己先来处理
        int result = onOperate(num);
        System.out.println(String.format("处理的结果:%d", result));
        //2.分发给下一节点处理
        if (next != null) {
            next.operate(result);
        }
    }

    //抽象方法,具体的处理交给子类根据自己的需求去实现
    protected abstract int onOperate(int num);
}

可以看到把原先自己直接处理的逻辑抽成了一个抽象函数,这样子类就必须去实现onOperate方法去做自己的处理逻辑。

假设有这样的需求:

实现三个节点,一个是进行+1的操作;一个是进行-1的操作;一个是乘2的操作

class AddNode extends Node {
    public AddNode() {
        super("加法器");
    }

    @Override
    protected int onOperate(int num) {
        System.out.println(String.format("我是%s,我将对%d进行加1操作", name, num));
        return num + 1;
    }
}

class MinusNode extends Node {
    public MinusNode() {
        super("减法器");
    }

    @Override
    protected int onOperate(int num) {
        System.out.println(String.format("我是%s,我将对%d进行减1操作", name, num));
        return num - 1;
    }
}

class MultiNode extends Node {
    public MultiNode() {
        super("乘法器");
    }

    @Override
    protected int onOperate(int num) {
        System.out.println(String.format("我是%s,我将对%d进行乘2操作", name, num));
        return num * 2;
    }
}
public class Main {
    public static void main(String[] args) {
        int num = 100;
        //做运算:(num+1)*2-1 = 201
        Node add = new AddNode();
        Node minus = new MinusNode();
        Node multi = new MultiNode();

        add.next = multi;
        multi.next = minus;

        add.operate(num);
    }
}
结果为:
我是加法器,我将对100进行加1操作
处理的结果:101
我是乘法器,我将对101进行乘2操作
处理的结果:202
我是减法器,我将对202进行减1操作
处理的结果:201

模板方法模式体现在:因为每个节点具体的消息处理逻辑是不一样的,通过把operate流程固定,把消息处理逻辑写成抽象函数onOperate交给节点子类去实现。这样不同的节点就可以做不同的处理了。 发现一个彩蛋了没?onOperate函数怎么那么熟悉?

先来想想经常接触到的onXXX方法 onCreate,onMeasure,onInterceptTouchEvent……没错,事实上掌握了这几个设计模式,很多时候源码的阅读都会很流畅了。

如触摸事件分发,View绘制的三大过程,Activity生命周期回调,AsyncTask...等等的机制和原理。推荐大家一定要找时间深入研究,成体系地学习一下设计模式。这是高级工程师架构设计必备技能。

树的遍历

为了真正关注核心点而不被其他的东西干扰带偏,所以我假定View树是一个二叉树,或者说我选取一个二叉树View树来进行分析。

首先来回顾一下树的遍历(递归版):

class Node{
    int id;
    Node left;
    Node right;

    public Node(int id) {
        this.id = id;
    }
}
public class Main {
    public static void main(String[] args) {
        Node[] nodes = new Node[8];
        for (int i = 0; i < 8; i++) {
            nodes[i] = new Node(i);
        }
        nodes[0].left = nodes[1];
        nodes[0].right = nodes[2];
        nodes[1].left = nodes[3];
        nodes[2].left = nodes[4];
        nodes[2].right = nodes[5];
        nodes[3].left = nodes[6];
        nodes[3].right = nodes[7];
        dfs(nodes[0]);
    }

    private static void dfs(Node root) {
        if (root == null) {
            return;
        }
        System.out.println(root.id);
        if (root.left != null) {
            dfs(root.left);
        }
        if (root.right != null) {
            dfs(root.right);
        }
    }
}
结果为:
0
1
3
6
7
2
4
5

相信很多人都能写出上面的深度遍历代码,but,这显然不够“java”,严格来说这是c语言形式的写法,只是把节点看成是数据实体,不那么面向对象。那好,我们来实现更加面向对象的深度遍历写法。

面向对象也就是类里面有数据也有行为,那我们就把遍历的行为交给类去做。说白了就是把dfs函数写成成员函数。

class Node {
    int id;
    Node left;
    Node right;

    public Node(int id) {
        this.id = id;
    }

    //把dfs写成成员函数
    public void dfs() {
        System.out.println(this.id);
        if (left != null) {
            left.dfs();
        }
        if (right != null) {
            right.dfs();
        }

    }
}
public class Main {
    public static void main(String[] args) {
        Node[] nodes = new Node[8];
        for (int i = 0; i < 8; i++) {
            nodes[i] = new Node(i);
        }
        nodes[0].left = nodes[1];
        nodes[0].right = nodes[2];
        nodes[1].left = nodes[3];
        nodes[2].left = nodes[4];
        nodes[2].right = nodes[5];
        nodes[3].left = nodes[6];
        nodes[3].right = nodes[7];
        //dfs(nodes[0]);
        nodes[0].dfs();
    }
}
结果为:
0
1
3
6
7
2
4
5

好了,树的遍历主要是想说明Java版的面向对象的写法。

因为我在百度随意搜索了一下,发现基本都是用c语言版本的写法来写的。

2View的Measure流程的核心

前面洋洋洒洒写了那么多,现在终于可以应用啦。理解上面的知识点能让你更加容易理解复杂的View的Measure流程。

为了真正关注核心点而不被其他的东西干扰带偏,所以我假定View树是一个二叉树,或者说我选取一个二叉树View树来进行分析。

测量就是计算每个View的大小,先来定义View类。

abstract class View {
    int id;
    int width;
    int height;
    View left;
    View right;

    public View(int id) {
        this.id = id;
    }

    final public void measure(int width, int height) {
        //1.具体如何测量交给子类决定
        onMeasure(width, height);
    }

    //设置测量值
    public void setMeasuredDimension(int w, int h) {
        width = w;
        height = h;
        System.out.println(String.format("%d的测量结果是w=%d,h=%d", id, width, height));
    }

    protected abstract void onMeasure(int width, int height);
}

很简陋的一个类,但是包含了最基本的要素了。measure方法里就用了模板方法模式,把具体如何测量交给子类实现。而且用final关键字,所以子类不能覆写measure,也就是说measure方法的流程不让改动。

注意:下文子节点是指View树的子节点,父节点是指View树的父节点,注意跟父类子类区分开。这是两回事来的。

好了,再来实现两个子类,不妨就叫TextView,ImageView。TextView具体的测量就是把父节点传递过来的值减去10,而ImageView是减去20。

class TextView extends View {

    public TextView(int id) {
        super(id);
    }

    @Override
    protected void onMeasure(int width, int height) {
        int myW = width - 10;
        int myH = height - 10;
        setMeasuredDimension(myW, myH);
        //去测量子节点
        if (left != null) {
            left.measure(myW, myH);
        }
        if (right != null) {
            right.measure(myW, myH);
        }
    }
}
class ImageView extends View {

    public ImageView(int id) {
        super(id);
    }

    @Override
    protected void onMeasure(int width, int height) {
        int myW = width - 20;
        int myH = height - 20;
        setMeasuredDimension(myW, myH);
        //去测量子节点
        if (left != null) {
            left.measure(myW, myH);
        }
        if (right != null) {
            right.measure(myW, myH);
        }
    }
}

大家可以看到,子节点的测量也是交给子类去负责分发测量了。跟之前讨论模板方法模式时有点不同,但是本质上是一样的。只是模板方法模式的例子是父类负责分发,这里是子类分发。

构造上图的View树进行测试。

public class Main {
    public static void main(String[] args) {
        View decorView = new ImageView(0);
        View imageView1 = new ImageView(1);
        View imageView2 = new ImageView(2);
        View textView3 = new TextView(3);
        View textView4 = new TextView(4);

        decorView.left = imageView1;
        decorView.right = imageView2;
        imageView1.left = textView3;
        imageView1.right = textView4;

        //获取window窗口大小(一般是手机屏幕大小),假设是1080x1920
        int windowW = 1080;
        int windowH = 1920;
        decorView.measure(windowW, windowH);
    }
}
结果为:
0的测量结果是w=1060,h=1900
1的测量结果是w=1040,h=1880
3的测量结果是w=1030,h=1870
4的测量结果是w=1030,h=1870
2的测量结果是w=1040,h=1880

根据上图和运行结果可知,View的测量是深度遍历的。测量到一个节点时,这个节点负责去发起子节点的测量,这是责任链模式;而为了把具体测量实现交给子类,使用了模板方法模式。

3更进一步

有的小伙伴可能说了,你这个跟Android实际的View代码出入有点大啊,你看都没有体现出View跟ViewGroup呢!好吧,那我们来实现更加贴近Android的代码实现吧。

public class Main {
    public static void main(String[] args) {
        LinearLayout linearLayout0 = new LinearLayout(0);
        LinearLayout linearLayout1 = new LinearLayout(1);
        TextView textView2 = new TextView(2);
        LinearLayout linearLayout3 = new LinearLayout(3);
        LinearLayout linearLayout4 = new LinearLayout(4);

        linearLayout0.left = linearLayout1;
        linearLayout0.right = textView2;
        linearLayout1.left = linearLayout3;
        linearLayout1.right = linearLayout4;

         //获取window窗口大小,假设是1080x1920
        int windowW = 1080;
        int windowH = 1920;
        linearLayout0.measure(windowW,windowH);

    }
}

class View {
    int id;
    int width;
    int height;

    public View(int id) {
        this.id = id;
    }

    final public void measure(int width, int height) {
        //1.具体如何测量交给子类决定
        onMeasure(width, height);
    }

    //设置测量值
    public void setMeasuredDimension(int w, int h) {
        width = w;
        height = h;
        System.out.println(String.format("%d的测量结果是w=%d,h=%d", id, width, height));
    }

    protected void onMeasure(int width, int height) {
        //默认实现为直接设置父类传递过来的参数
        setMeasuredDimension(width, height);
    }

}

class ViewGroup extends View {
    public ViewGroup(int id) {
        super(id);
    }

    //ViewGroup才有子View
    View left;
    View right;

    @Override
    protected void onMeasure(int width, int height) {
        //默认实现为把width,height减去50作为自己的参数
        int myW = width - 50;
        int myH = height - 50;
        setMeasuredDimension(myW, myH);
        //发起子节点的测量
        if (left != null) {
            left.measure(myW, myH);
        }
        if (right != null) {
            right.measure(myW, myH);
        }
    }
}

//View的子类没有子节点,只需要关心自己的测量
class TextView extends View {
    public TextView(int id) {
        super(id);
    }

    //实现自己的测量逻辑,把width,height减去10
    @Override
    protected void onMeasure(int width, int height) {
        setMeasuredDimension(width - 10, height - 10);
    }
}

//ViewGroup的子类有子节点,需要发起子节点的测量
class LinearLayout extends ViewGroup {
    public LinearLayout(int id) {
        super(id);
    }

    //把width,height减去30作为自己的参数
    @Override
    protected void onMeasure(int width, int height) {
        int myW = width - 30;
        int myH = height - 30;
        setMeasuredDimension(myW, myH);
        //负责发起子节点的测量,这里实现为先测量右节点再测量左节点
        if (right != null) {
            right.measure(myW, myH);
        }
        if (left != null) {
            left.measure(myW, myH);
        }
    }
}
结果为:
0的测量结果是w=1050,h=1890
2的测量结果是w=1040,h=1880
1的测量结果是w=1020,h=1860
4的测量结果是w=990,h=1830
3的测量结果是w=990,h=1830

重要的点都在代码上注释了。

可以看到,之前的遍历顺序是01342,现在是02143了,因为LinearLayout是先进行右节点的测量。

4总结

View的体系设计用到了许多设计模式,这里主要是责任链模式和模板方法模式,理解设计模式能更加容易读懂源码。

View的遍历是深度遍历,需要掌握Java版的实现。

layout以及draw流程的核心也差不多也是这样,大家跟着我说的去分析源码效果更好。注意子节点的draw流程直接由父类发起了,子类只需要在onDraw中绘制自己的内容即可。

文中关注的重点在于如何实现一颗View树的测量过程。还有很多细节没有涉及,例如MeasureSpec。实际上View最终测量结果是结合我们在xml自己定义的参数和父View自己的参数去决定的。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/OQHGCluehmE-auabnibj_g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
简化Android的UI开发 4年以前  |  520699次阅读
Android 深色模式适配原理分析 3年以前  |  28625次阅读
Android阴影实现的几种方案 1年以前  |  10783次阅读
Android 样式系统 | 主题背景覆盖 3年以前  |  9586次阅读
 目录