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

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章