View 自定义属性之 LayoutInflater

本文介绍如何使用自定义LayoutInflater解析自定义属性,实现为ImageView添加移动速度的功能。通过重写onCreateView方法,从AttributeSet中解析自定义属性并保存在tag中,实现在点击View时,X和Y方向移动指定速度。

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 都有自己的移动速度。

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值