LayoutInflater学习(二)之创建布局View

news2024/11/25 4:26:00

这篇是在上篇的基础上继续学习LayoutInflater,上篇主要讲了LayoutInflater是怎么解析布局的,但是并没有去仔细地说明LayoutInflater创建View的过程,这篇就补上这部分。

LayoutInflater创建xml布局View是分开创建的:

1. 先创建xml布局最外层的View,也就是布局的根View

2. 递归创建所有的子View

本着先简后繁的原则,先不考虑 <merge> <include>等标签,直接从一般情况的流程看起

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
           ......
            View result = root;
            try {
               ......
                if (TAG_MERGE.equals(name)) {
                    ......
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    //首先创建xml布局最外层View:根View
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    //递归创建子View
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);
                    ......
                }
            } catch (XmlPullParserException e) {
            ......
            return result;
        }
    }

布局根View的创建

布局最外层根View的创建是通过 createViewFromTag() 来创建的,其实后面子View的创建依然会用到这个方法,在看这个方法之前先理清方法的参数.

第一个参数 root :这里注意:这个 root 是我们调用LayoutInflater的inflate方法加载布局时自己传入的一个父View,需要和要加载的xml布局根View区分开来

第二个参数 name:这个是对应我们要解析的xml布局树中对应的每个View的名称,就以上篇文章中 activity_main.xml 为例,name 的取值为

androidx.constraintlayout.widget.ConstraintLayout,当然如果是子View的创建取值将依次为:LinearLayout、TextView、FrameLayout

后面两个参数依次为上下文 Context 和保存要创建View的属性的 AttributeSet 对象,接下来开始看代码,还是只看主要流程


    private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
        return createViewFromTag(parent, name, context, attrs, false);
    }
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
       ......
        try {
            //这里可以通过设置工厂的形式拦截系统创建View
            View view = tryCreateView(parent, name, context, attrs);
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        //SDK View的创建,eg:LinearLayout TextView ...
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        //自定义View的创建,eg:androidx.constraintlayout.widget.ConstraintLayout
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
        } catch (InflateException e) {
        ......
    }

首先来看下这个 tryCreateView ,这个方法其实是系统创建View的一个Hook点

   private boolean mFactorySet;
   private Factory mFactory;
   private Factory2 mFactory2;
   private Factory2 mPrivateFactory;
   public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        ......
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }
        return view;
    }

 tryCreateView 中首先通过 mFactory2 来创建View, mFactory2 是LayoutInflater的一个成员变量,Factory2是一个接口,平时我们使用LayoutInflater来加载布局的时候 tryCreateView方法一般都是返回 null 的,我们可以自己实现这个接口,并通过对应的 setFactory2()方法设置到LayoutInflater中,这样可以拦截系统创建xml布局View的过程,不过这个setFactory2的方法只能设置一次,因为mFactorySet的控制,再次设置会抛出异常,这里也可以通过反射消除这个异常,后面的mFactory也是类似的,mPrivateFactory 其实Activity已经实现了 不过onCreateView方法直接返回null了,对于系统创建 xml 布局的Hook点就说到这里,对此感兴趣的朋友可以去看下对应源码

接下来看第二个try语句中的逻辑,也就是 if..else..语句,这才是系统创建View的核心逻辑,这里对要创建的View做了个分类,看看要创建的View是开发者自定义的View,还是SDK中的系统View,一般我们使用自定义View的时候 name 值往往是 com.xxx.xx.XxxView,所以这里通过判断 name 中是否包含 "." 来判断是不是自定义View,像LinearLayout、TextView、FrameLayout、RelativeLayout等这些都是SDK View,有人可能会问了,那上面的约束布局 ConstraintLayout 又该怎么算呢,ConstraintLayout 的 name 中包含了".",当然它也是自定义View,只不过是google工程师自定义的。

为什么要区分SDK View和自定义View,这是因为后面创建View对象都是通过反射,需要View的全类名,而SDK View在不同的包下,需要手动拼接全类名

 先来看SDK View 的创建流程,会通过多个重载的方法:onCreateView方法来创建View

   public View onCreateView(@NonNull Context viewContext, @Nullable View parent,
            @NonNull String name, @Nullable AttributeSet attrs)
            throws ClassNotFoundException {
        return onCreateView(parent, name, attrs);
    }
   protected View onCreateView(View parent, String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return onCreateView(name, attrs);
    }

流程走到这里需要注意下了:看过第一篇文章的朋友应该知道,我们加载布局最终获取到的是PhoneLayoutInflater对象,所以后面的流程不在LayoutInflater这个类里面,而是要到 PhoneLayoutInflater里去,虽说不是很难找到,但是也是比较容易忽略的一个点,稍微提下。

public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };   
    @Override
    protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
               ......
            }
        }
        return super.onCreateView(name, attrs);
    }
}

细心的朋友可能发现了,这个 sClassPrefixList 数组里好像少了一个 "android.view." 的包,这个参数其实是加在了超类LayoutInflater里,有兴趣的可以看下就不再贴源码了,这里循环遍历了SDK View的包名 prefix,之后作为参数传到了createView方法中,接下来又进入了LayoutInflater的方法中,其核心代码如下

   public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Context context = (Context) mConstructorArgs[0];
        if (context == null) {
            context = mContext;
        }
        return createView(context, name, prefix, attrs);
    }

    public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        ......
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        ......
        Class<? extends View> clazz = null;
        try {
            if (constructor == null) {
                //反射获取对应View的Class对象
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);
                ......
                //获取创建View的构造器
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
               ......
            }
            Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = viewContext;
            Object[] args = mConstructorArgs;
            args[1] = attrs;
            try {
                //通过View的构造器反射创建View对象
                final View view = constructor.newInstance(args);
                if (view instanceof ViewStub) {
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        } catch (NoSuchMethodException e) {
        ......
    }

最终SDK View的创建和自定义View的创建都是通过 createView(Context viewContext, String name, String prefix, AttributeSet attrs) 方法来创建的,只不过SDK View需要先获取到对应的包名,再拼接获取到全类名之后再调用该方法创建View

通过反射获取对应View的Class对象时,SDK View会对全类名做一个拼接, 而自定义View prefix为null,因为在xml布局获取到的 name 就是自定义View的全类名,当通过构造器的newInstance方法创建View时,会调用对应View的构造方法,由于传入的参数 args 是一个包含两个元素的数组,这两个元素分别是Context上下文和属性集合Attributeset对象,所以最终会调用每个View的两参构造函数,例如 TextView 将会调用如下构造方法,其他View也一样

    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.textViewStyle);
    }

这就是为什么我们写自定义View时必须要有这个两参的构造函数,否则就会报错,到此两种类型View的创建流程都走完了

递归创建子View

递归创建子View的方法是 rInflateChildren(),这个方法是在inflate()方法中创建完xml的根View后调用的,另外在rInflate()方法中也调用了一次,这一次是为了实现递归,还有就是在解析 include 标签的 parseInclude()方法中也调用了这个方法,我们先了解在 inflate()方法中的调用,在看这个方法之前还是要先了解下它的参数,重点关注下第二个参数,在 inflate()方法中传入的是 temp,而 temp 就是xml布局的根View,这里同样要和父容器root 区分开来。

一般递归流程

还是先从一般的简单流程看,不考虑xml中其他标签,这个之后再讨论

   final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            if (TAG_REQUEST_FOCUS.equals(name)) {
              ......
            } else if (TAG_TAG.equals(name)) {
              ......
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                //递归创建子View
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }
        ......
    }

首次调用 parent 参数接收的就是 temp,xml布局的根View,看rInflate()方法时先不考虑<merge> <include>等其他标签,先通过一个 while()循环找到temp下面的第一个子View标签的起始位置,之后将进入 else 语句中执行,依然是调用 createViewFromTag()来创建该子View,创建完子View之后又调用了rInflateChildren()形成递归调用,注意:这次第二个参数传的是 view,之后就是递归创建view下面的所有子View了.递归创建完所有子View之后,都是通过addView()的方法添加到对应的父View中去,这样一直到把整个子View树全部创建完成。

包含标签的递归流程

 在上篇文章中一开始将xml解析的时候就遇到了<merge>标签,我们在写xml布局文件的时候是可以加入各种标签的,有些标签我们经常用到,有些可能使用的频率比较低,这些标签在LayoutInflater中都有定义,下面来一一介绍(只讲LayoutInflater中定义的标签)

public abstract class LayoutInflater {
    private static final String TAG_MERGE = "merge";
    private static final String TAG_INCLUDE = "include";
    private static final String TAG_1995 = "blink";
    private static final String TAG_REQUEST_FOCUS = "requestFocus";
    private static final String TAG_TAG = "tag";

"merge标签"

讲 <merge>标签之前先来看一个关于<merge>标签的事例,还是对第一篇的例子做稍微的改动,原来的activity_mainxml的布局内容保持不变,通过LayoutInflater向其中LinearLayout(id为ll) 添加 layout_merge.xml

<?xml version="1.0" encoding="utf-8"?>
<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    >
    <TextView
        android:text="测试布局merge------merge"
        android:textColor="@color/red_200"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
    <!--"merge"必须放在布局根View的位置否则报错:
    layout/layout_merge: <merge /> must be the root element-->
    <!--<merge>
        <TextView
            android:text="测试布局merge&#45;&#45;&#45;&#45;&#45;&#45;merge"
            android:textColor="@color/red_200"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />
    </merge>-->

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        >
        <TextView
            android:text="测试布局merge------merge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />

        <TextView
            android:text="测试布局merge------merge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />
        <TextView
            android:text="测试布局merge------merge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            />
    </LinearLayout>
</merge>
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //ConstraintLayout cs = findViewById(R.id.cs);
        LinearLayout ll = findViewById(R.id.ll);
        LayoutInflater.from(this).inflate(R.layout.layout_merge, ll,true); 
    }

最后的效果如下:

 通过上面的事例来总结下 <merge>标签的用法及作用:

1. <merge>标签只能作为xml的根标签,其他位置会报错(见layout_merge.xml中注释)

2. 使用LayoutInflater加载包含<merge>标签的布局时,必须同时满足 root 不为空且 attachToRoot为true

<merge>标签的作用通过源码就可以看得出来,由于第一篇讲过,这里只截取 inflate()方法的包含 "merge" 的代码

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
           ......
            try {
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                  ......

上面提到的第2点,看抛异常的代码就很清楚了,当解析到布局中包含 <merge>标签后,直接调用了 rInflate()方法去递归创建所有<merge>标签下的子View,再看下参数,第二个参数传的是 root ,这里的root是我们调用LayoutInflater的inflate方法时自己传入的root父容器,这样以来后面递归创建的<merge>标签之下的所有子View的容器将是 root父容器,以上面的事例来时 root 就是 id 为 ll 的LinearLayout,这里<merge>的效果就好比是直接把 layout_merge.xml的布局放到 id 为 ll 的LinearLayout里面,如果不加<merge>标签,那就需要在最外层再添加一个父容器

所以<merge>标签的作用:可以减少一层嵌套,至于为什么只能是根标签后面会有对应源码

"include"标签

<include>标签也是属于这几个标签中比较常用的一个,由于源码较为枯燥,所以先通过一个事例说下 include 的具体用法,有兴趣的可以往后看源码

 由于activity_main.xml的布局和第一篇一样,这里只贴部分,在父容器 ll 中加了一个 include布局(注意:include标签自身也设置了id)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/cs"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:id="@+id/ll"
        android:background="@color/purple_200"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.21">
        <include
            android:id="@+id/include"
            layout="@layout/layout_include"
            />
    </LinearLayout>
    ......
</androidx.constraintlayout.widget.ConstraintLayout>

layout_include.xml (注意点1:最外层父容器设置了 id,下文会讲;注意点2:如果上面的<include>标签本身设置了宽、高,将会覆盖 id 为 linear_include在xml中的宽、高,也就是如果<include>本身设置了layout_width、layout_height,下面xml布局中的对应的宽高属性将会失效!)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linear_include"
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:text="测试include布局"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

 代码很简单,直接通过setContentView()把activity_main.xml显示出来,效果如下:

通过以上事例总结下<include>用法,<include>的用法也比较简单,它恰好和 <merge>的用法相反,使用<include>需要注意的点:

1.  <include> 标签不能作为布局根标签,必须放在某个父容器内

2.  如果<include>标签本身设置了 id,这个id将会覆盖它所引用的layout的最外层View的id

3. 如果<include>标签本身设置了layout_width layout_height,和id的作用效果一样会覆盖它引用的layout的最外层View的宽、高

上面第 2 点有点绕,直接通过上面的例子就好理解了,activity_main.xml布局中 id为ll的LinearLayout里嵌套了一个<include>标签,这个<include>标签本身有一个id:"include",而这个<include>所引用的布局 layout_include.xml 的最外层View是个Linear Layout也设置了一个id: linear_include,此时layout_include.xml中的LinearLayout 的 id 将会被 <include>标签的id "include"覆盖,也就是如果我们要通过findViewById()找到它,传的id值必须是 R.id.include,否则报错

 接下来看下解析 <include>的源码,比较枯燥,不想了解的这里可以跳过,其实处理tag标签的逻辑都在rInflate()方法中,这次只看处理标签的逻辑

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }//解析 <include> 标签
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
               ......
            }
        }
        ......
    }

<include>标签的解析都在 parseInclude() 方法中

private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;
        if (!(parent instanceof ViewGroup)) {//<include>必须放在一个父容器里
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }
        ......
        if (precompiled == null) {//注意:childParser是<include>标签引用的layoutId所对应的xml解析器
            final XmlResourceParser childParser = context.getResources().getLayout(layout);
            try {//注意:childAttrs是是<include>标签引用的layoutId所对应的属性集合
                final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
                while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                    // Empty.
                }
                ......
                final String childName = childParser.getName();
                if (TAG_MERGE.equals(childName)) {
                    rInflate(childParser, parent, context, childAttrs, false);
                } else {
                    final View view = createViewFromTag(parent, childName,
                        context, childAttrs, hasThemeOverride);
                    final ViewGroup group = (ViewGroup) parent;

                    final TypedArray a = context.obtainStyledAttributes(
                        attrs, R.styleable.Include);//注意这里获取的是<include>标签的id
                    final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                    final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                    a.recycle();
                    ViewGroup.LayoutParams params = null;
                    try {//如果<include>标签本身没有设置宽、高这里会抛出异常
                        params = group.generateLayoutParams(attrs);
                    } catch (RuntimeException e) {
                        // Ignore, just fail over to child attrs.
                    }
                    if (params == null) {//如果<include>标签本身没有设置宽、高,将会使用View在xml布局中的宽、高值
                        params = group.generateLayoutParams(childAttrs);
                    }
                    view.setLayoutParams(params);
                    rInflateChildren(childParser, view, childAttrs, true);
                    if (id != View.NO_ID) {
                        view.setId(id);//把<include>标签的id覆盖View原有的id
                    }
                    ......
                    group.addView(view);
                }
            } finally {
                childParser.close();
            }
        }
        LayoutInflater.consumeChildElements(parser);
    }

解析 <include> 标签时,<include>标签所引用的布局文件最外层View布局参数的设置需要注意下:

1.LayoutParams的设置    2. id值的设置  这些在源码中已经有相应的注释

"blink"标签

这个标签可能我们开发中用的少一些,看它的命名就能大概知道它的效果,"blink"是眨眼、闪烁的意思,是的"blink"标签就是实现了这个一个效果,源码实现也比较简单,直接创建了一个BlinkLayout布局,其实就是一个自定义的FrameLayout

public final View tryCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs) {
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }
        ......
    }

这里的TAG_1995 就是"blink",废话不多说了,直接看下实现代码及效果,直接在Activity中通过setContentView显示就可以了,

layout_tag_blink.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <blink
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="测试blink"
            android:textSize="30sp"
            android:textColor="@color/red_200"
            />
    </blink>

</LinearLayout>

 "requestFocus"标签

这个标签实现也比较简单,是用来获取焦点的,一般用到EditText上,我们可以通过加<requestFocus/>标签来指定哪个View优先获取到焦点

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:hint="第一个"
        android:textSize="20sp"
        android:layout_height="50dp"
        android:layout_width="match_parent"/>

    <EditText
        android:hint="第二个"
        android:layout_marginTop="15dp"
        android:textSize="20sp"
        android:layout_height="50dp"
        android:layout_width="match_parent"/>

    <EditText
        android:hint="第三个"
        android:textSize="20sp"
        android:layout_height="50dp"
        android:layout_width="match_parent">
        <requestFocus/>
    </EditText>

</LinearLayout>

上面布局页面有多个EditText,第三个EditText添加了 <requestFocus/>标签,所以它会优先获取到焦点

"tag"标签

这个标签的作用就是setTag,getTag的作用,只不过我们平时用的多的是在代码中直接使用setTag方法,它的作用就是把数据和一个View绑定起来,当我们要绑定的数据比较多的时候,可以直接在xml中通过添加 <tag>标签的形式实现,"tag"标签的源码也比较简单,在parseViewTag中,不再贴了直接看用法

首先创建 ids.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="linear_tag1" type="id"/>
    <item name="linear_tag2" type="id"/>
    <item name="linear_tag3" type="id"/>

    <item name="textView_tag1" type="id"/>
    <item name="textView_tag2" type="id"/>
    <item name="textView_tag3" type="id"/>
</resources>

实现代码,其中activity_main.xml还是用的之前的例子

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tag);
        LinearLayout ll = findViewById(R.id.ll);
        final LayoutInflater inflater = LayoutInflater.from(this);
        final View view = inflater.inflate(R.layout.layout_tag, ll, false);
        tagTest(view);
        ll.addView(view);
    }
    void tagTest(View view) {
        String linearTag1 = (String) view.getTag(R.id.linear_tag1);
        TextView tv = view.findViewById(R.id.textView);
        String textViewTag3 = (String) tv.getTag(R.id.textView_tag3);
        Log.d("TagActivity_Test", "Tag1: "+linearTag1+"  Tag3: "+textViewTag3);
        //TagActivity_Test  com.github.app_tag   D  Tag1: LinearLayout tag1  Tag3: TextView tag3
    }

layout_tag.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <tag
        android:id="@id/linear_tag1"
        android:value="LinearLayout tag1"
        >
    </tag>

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="测试tag标签"
        android:textSize="30sp"
        android:textColor="@color/teal_200"
        android:gravity="center"
        >
        <tag
            android:id="@+id/textView_tag1"
            android:value="TextView tag1"/>
        <tag
            android:id="@+id/textView_tag2"
            android:value="TextView tag2"/>
        <tag
            android:id="@+id/textView_tag3"
            android:value="TextView tag3"/>
    </TextView>

    <tag
        android:id="@id/linear_tag2"
        android:value="LinearLayout tag2"
        />

    <tag
        android:id="@id/linear_tag3"
        android:value="LinearLayout tag3"
        />

    <include layout="@layout/layout_tag_blink"/>
</LinearLayout>

日志打印结果已经在代码中贴出,实现也比较简单,上面的示例都可以直接自己复制粘贴实现,好了到此所有标签都已记录完毕了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/154350.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

package.json和package-lock.json的区别

前言 今天正在写代码&#xff0c;同学突然问我&#xff0c;package.json和package-lock.json有什么区别&#xff0c;这两个文件有什么用&#xff1f;我愣住了… 模块化开发 经过这么多年的发展&#xff0c;现在的前端开发基本上都是模块化开发了。而node和npm则可以很方便的…

电网调频及一次调频、二次调频

电网调频的基本概念电力系统运行的主要任务之一是对电网频率进行控制—控制电网频率在50Hz附近的一个允许范围内。电网频率偏离额定值50Hz的原因是能源侧&#xff08;水电、火电、核电……&#xff09;的供电功率与负荷侧的用电功率之间的平衡被破坏而引起的。负荷的用电功率是…

Sentinel热点参数限流

何为热点&#xff1f; 何为热点&#xff1f;热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K数据&#xff0c;并对其访问进行限制。比如&#xff1a; 1&#xff09;商品 ID 为参数&#xff0c;统计一段时间内最常购买的商品 ID 并进行限制&am…

正确的清理内存方式,才能让你的空间更加充裕

我们的手机用了一段时间后&#xff0c;有没有感觉卡变了&#xff0c;运行速度也很慢&#xff1f;估计是手机内存不足造成的&#xff0c;今天就来教教大家如何快速清理空间。方法一&#xff1a;清除微信缓存文件 我们每天都用微信。 其实它最占内存&#xff0c;我们还需要定时打…

原子化带来化学反应,视频号万粉创作者增加3倍,点赞10w+的爆款内容增长186%

1月10日&#xff0c;以“在场”为主题的2023微信公开课PRO正式开讲。本年度微信公开课重点从视频号内容生态发展、微信生态内各类产品助力实体转型以及数字生活服务升级三个方面&#xff0c;展示微信生态各产品的新能力、新计划。在2022年的微信公开课Pro上&#xff0c;“视频号…

广告业务系统 之 数据中转站 —— “日志中心-实时服务监控”

文章目录广告业务系统 之 数据中转站 —— “日志中心-实时服务监控”日志中心实时服务监控 —— 前链路日志分析日志收敛手段 —— “手术开口”基于 metrics 的日志分析 —— Prometheus & Graphite监控服务是怎么监控自身 & 比常规服务更坚强高扩展、高性能的架构设…

[L1 - 5分合集]心理阴影面积

L1-060 心理阴影面积 分数 5 作者 陈越 单位 浙江大学 题目&#xff1a; 这是一幅心理阴影面积图。我们都以为自己可以匀速前进&#xff08;图中蓝色直线&#xff09;&#xff0c;而拖延症晚期的我们往往执行的是最后时刻的疯狂赶工&#xff08;图中的红色折线&#xff09;。由…

外贸邮件营销的优势

邮件营销相对于其他营销方式&#xff0c;历史更悠久。邮件营销具有成本低廉、快速、精准的特点。那么邮件营销有哪些优势&#xff0c;才能获得如此的关注。接下来&#xff0c;米贸搜和大家分享一下邮件营销的优势。1.节约成本的考虑:当前&#xff0c;世界经济复苏乏力&#xff…

Kafka消息队列使用及原理

消息队列作用&#xff1a;异步、削峰、解耦 1、kafka简介 ​ Apache Kafka 是一个分布式的流平台&#xff0c;有三个关键的功能&#xff1a; 能够发布&#xff08;写入&#xff09;和订阅&#xff08;读取&#xff09;事件流持续可靠的存储事件流在事件发生时回顾性的处理事件…

IB生物笔记:Structure and function of organelles

国际学校生物老师解读IB生物&#xff0c;感兴趣的同学记得收藏哦~IB生物分为SL(standard level)和HL(higher level)SL有6个topic∶细胞生物&#xff0c;分子生物&#xff0c;遗传学&#xff0c;生态学&#xff0c;物种进化以及多样性和人体生理。HL除了上述6个topic外还要加上∶…

C++模板类

目录 前言 类模板 模板类继承 前言 随着c发展&#xff0c;有一部分代码就会出现这样的情况&#xff1a;实现的内容相同&#xff0c;但是参数不同。模板类就是为解决这类情况来的&#xff0c;是一种泛型编码。即与数据类型无关的通用程序设计技术。 模板类本身不占空间&…

C语言模块化

&#x1f31e;欢迎来到C语言的世界 &#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f64f;作者水平很有限&#xff0c;如果发现错误&#xff0c;…

MySQL的行锁总结

文章目录前言一、行锁的介绍二、行锁的使用三、使用行锁所带来的问题四、死锁和死锁检测前言 上篇文章已经学习了MySQL的全局锁和表锁&#xff0c;今天这篇文章我们对行锁进行以下学习 一、行锁的介绍 行锁就是针对数据表中行记录的锁&#xff0c;比如事务A更新了一行&#x…

切面AOP

1.2 AOP体系与概念 简单地去理解&#xff0c;其实AOP要做三类事&#xff1a; 在哪里切入&#xff0c;也就是权限校验等非业务操作在哪些业务代码中执行。 在什么时候切入&#xff0c;是业务代码执行前还是执行后。 切入后做什么事&#xff0c;比如做权限校验、日志记录等。 因…

ES索引切分

提示&#xff1a;对于一些日志类的数据&#xff0c;我们常用到es作为存储&#xff0c;数据量过大时&#xff0c;可能会用到索引切分&#xff0c;这里可以参考 ES索引切分前言方案一&#xff1a;ES索引切分验证&#xff08;policy策略&#xff09;配置ilm策略 &#xff08;max_d…

照片如何修复清晰度?这些修复方法值得你收藏

我们都知道以前的拍照技术落后&#xff0c;拍摄出来的照片像素都比较低&#xff0c;从而导致照片有些模糊不清&#xff0c;再加上我们保存不当&#xff0c;很多旧照片都变得模糊破损&#xff0c;因此很多人为了不让这些旧照片消失&#xff0c;都会选择找人来修复这些旧照片&…

爬虫 大规模数据 采集心得和示例

本篇主要介绍网站数据很是大的采集心得数据库1. 什么样的数据才能称为数据量大&#xff1a;编程我以为这个可能会由于每一个人的理解不太同样&#xff0c;给出的定义 也不相同。我认为定义一个采集网站的数据大小&#xff0c;不单单要看这个网站包括的数据量的大小&#xff0c;…

MySQL高级【锁】

1&#xff1a;锁的概述锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中&#xff0c;除传统的计算资源&#xff08;CPU、 RAM、I/O&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有 效性是所有数据库必…

基于java springmvc+mybatis酒水商城管理系统设计和实现

基于java springmvcmybatis酒水商城管理系统设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取…

5.7、TCP 可靠传输的实现

1、TCP基于以字节为单位的滑动窗口来实现可靠传输 TCP 基于以字节为单位的滑动窗口\color{red}以字节为单位的滑动窗口以字节为单位的滑动窗口来实现可靠传输 如下所示&#xff0c;假定数据传输只在一个方向进行 这是发送方待发送字节的序号 假设发送方收到了来自一个接收方的…