View 自定义属性之 LayoutInflater

Posted xyTianZhao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View 自定义属性之 LayoutInflater相关的知识,希望对你有一定的参考价值。

View 自定义属性之 LayoutInflater

平常开发中,我们避免不了会自定义 view,自定义 view 的时候可以通过 AttributeSet 来获取自定义的相关属性。而怎么样不通过自定义 view,就能实现自定义相关属性呢,那就要使用自定义的 LayoutInflater 了。

原生 LayoutInflater 使用

我们先看看原生的 LayoutInflater 是怎么使用的。

View view = LayoutInflater.from(context).inflate(R.layout.activity_main,null);

这样我们就拿到了解析出来的 view。

所以我们只需要思考在什么时候 hock 一下,解析出我们需要的自定义属性即可。

原生 LayoutInflater 解析 xml 布局文件

通过上面的使用方法为入口,我们来看看 LayoutInflater 这个类是怎么将一个 xml 布局文件解析为一个 view 的。

public abstract class LayoutInflater 

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) 
        return inflate(resource, root, root != null);
    

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 
        final Resources res = getContext().getResources();
        //通过 XmlResourceParser 将 xml布局文件解析为一个对象
        final XmlResourceParser parser = res.getLayout(resource);
        try 
            return inflate(parser, root, attachToRoot);
         finally 
            parser.close();
        
    

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 
        // Temp is the root view that was found in the xml
        // temp view 是从这个 xml 文件中找出的根view
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        // Inflate all children under temp against its context.
        // 从根 view 开始,解析所有的子 view
        rInflateChildren(parser, temp, attrs, true);
    

    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;
        //循环遍历所有的子 view 并解析。递归调用 rInflateChildren 方法
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) 
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        
    

    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) 
        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);
        

        if (view == null) 
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try 
                if (-1 == name.indexOf('.')) 
                    view = onCreateView(parent, name, attrs);
                 else 
                    view = createView(name, null, attrs);
                
             finally 
                mConstructorArgs[0] = lastContext;
            
        

        return view;
    

上面就是 LayoutInflater 将 xml 布局文件解析成 view 的全过程,相应的注释已经标明,这里就不过多解释。

我们重点关注 createViewFromTag 这个方法里面的两个 Factory。

public abstract class LayoutInflater 
    public interface Factory 
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         *
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    

    public interface Factory2 extends Factory 
        /**
         * Version of @link #onCreateView(String, Context, AttributeSet)
         * that also supplies the parent that the view created view will be
         * placed in.
         */
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    

注释中的大概意思就是,你可以提供一个回调,来 hook 这个方法,从而返回你的 view。

找到这里是不就有那么点思路了,话不多说,盘就完了。

自定义 LayoutInflater 解析自定义属性

我们来实现一个,给 ImageView 添加一个移动速度的自定义属性,下面为相关的自定义 attr 和相关的布局文件。都是非常简单的,没有什么需要解释的。

<!-- attr -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 移动速度 -->
    <attr name="moveSpeed" format="float"/>
    <!-- 保存移动速度的tag,如果多的话,可以考虑创建一个对象来保存相关属性 -->
    <item name="moveSpeedTag" type="id"/>
</resources>

<!-- xml 布局文件 -->
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:moveSpeed="10"
        android:background="@color/colorAccent"/>

</RelativeLayout>

我们主要看实现自定义 LayoutInflater 这个抽象类都需要实现哪些方法。
代码不多,我就全部贴上了

/**
 * Author silence.
 * Time:2020-02-28.
 * Desc:自定义 LayoutInflater ,解析自定义属性
 * 给 ImageView 添加自定义速度
 * 当点击 View 的时候,X 和 Y 方向个移动 moveSpeed
 */
public class CustomLayoutInflaterView extends RelativeLayout 

    private View moveView;

    public CustomLayoutInflaterView(@NonNull Context context) 
        super(context);
        CustomLayoutInflater layoutInflater = new CustomLayoutInflater(context);
        addView(layoutInflater.inflate(R.layout.activity_main,null),new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
        setOnClickListener(new OnClickListener() 
            @Override
            public void onClick(View v) 
                if (moveView != null)
                    float moveSpeed = (float) moveView.getTag(R.id.moveSpeedTag);
                    ViewHelper.setTranslationX(moveView,moveView.getTranslationX()+moveSpeed);
                    ViewHelper.setTranslationY(moveView,moveView.getTranslationY()+moveSpeed);
                
            
        );
    

    private class CustomLayoutInflater extends LayoutInflater

        private CustomLayoutInflater(Context newContext) 
            super(newContext);
            setFactory(new CustomLayoutInflaterFactory(cloneInContext(newContext)));
        

        @Override
        public LayoutInflater cloneInContext(Context newContext) 
            return LayoutInflater.from(newContext);
        
    

    private class CustomLayoutInflaterFactory implements LayoutInflater.Factory

        private String[] sClassPrefix = "android.widget.","android.view.";

        private LayoutInflater inflater;

        private CustomLayoutInflaterFactory(LayoutInflater inflater)
            this.inflater = inflater;
        

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) 
            View view = null;
            for (String classPrefix : sClassPrefix) 
                try 
                    //使用系统的 inflater 创建 view
                    view = inflater.createView(name,classPrefix,attrs);
                 catch (Exception e) 
                    e.printStackTrace();
                
                if (view != null)
                    //解析自定义属性
                    TypedArray a = context.obtainStyledAttributes(attrs,new int[]R.attr.moveSpeed);
                    if (a != null && a.length() > 0)
                        float moveSpeed = a.getFloat(0,0);
                        //保存自定义属性
                        view.setTag(R.id.moveSpeedTag,moveSpeed);
                        moveView = view;
                        a.recycle();
                    
                    break;
                
            
            return view;
        
    


代码量不多,主要是思路:

1、设置自定义 CustomLayoutInflaterFactory 工厂,实现 onCreateView 方法,创建自定义 view。
2、从 传递的 AttributeSet 中解析出自定义的属性并保存在 tag 中。
3、从 tag 中取出相应的属性并使用。

实现效果

拓展

类似小红书这种欢迎页,是不就可以使用这种方式去实现,每个 view 都有自己的移动速度。

以上是关于View 自定义属性之 LayoutInflater的主要内容,如果未能解决你的问题,请参考以下文章

android 自定义view之 TypeArray

LayoutInflate: Avoid passing null as the view root

Android进阶技术分享之——自定义 View 系列实战篇-ViewGroup(内含自定义View 宝藏图)

Android 使用Kotlin来实现自定义View之雷达图

Android 使用Kotlin来实现自定义View之雷达图

Android 使用Kotlin来实现自定义View之雷达图