平常开发中,我们避免不了会自定义 view,自定义 view 的时候可以通过 AttributeSet 来获取自定义的相关属性。而怎么样不通过自定义 view,就能实现自定义相关属性呢,那就要使用自定义的 LayoutInflater 了。
我们先看看原生的 LayoutInflater 是怎么使用的。
View view = LayoutInflater.from(context).inflate(R.layout.activity_main,null);
这样我们就拿到了解析出来的 view。
所以我们只需要思考在什么时候 hock 一下,解析出我们需要的自定义属性即可。
通过上面的使用方法为入口,我们来看看 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。
找到这里是不就有那么点思路了,话不多说,盘就完了。
我们来实现一个,给 ImageView 添加一个移动速度的自定义属性,下面为相关的自定义 attr 和相关的布局文件。都是非常简单的,没有什么需要解释的。
<resources>
<attr name="moveSpeed" format="float"/>
<item name="moveSpeedTag" type="id"/>
resources>
<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 都有自己的移动速度。