一、前言
上文【Android一杯冰美式的时间--去找setContentView】,最后因为我太困了而结束在LayoutInflater
。这篇文章还是要补足玩这一步的。
在 Android 应用中,界面是通过布局文件(通常是 XML 文件)来定义的。这些布局文件描述了界面的结构和外观,包括各种控件和它们的属性。但是,为了在应用运行时使用这些布局,我们需要将它们从 XML 文件转换成 Java 或 Kotlin 代码中的View对象。这就是 LayoutInflater
的作用所在。
如果你没用过LayoutInflater
....当我没说。
(说完了...)
如果您有任何疑问、对文章写的不满意、发现错误、想吐槽或者有更好的想法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏
二、使用
看看我们平常使用LayoutInflater
的方法:
-
通过系统服务获取布局加载器
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(resource,root,attachToRoot);
-
通过Activity中的getLayoutInflater()方法
View view = getLayoutInflater().inflate(resource,root,attachToRoot);
-
通过View的静态inflate()方法
View view = View.inflate(resource,root,attachToRoot);
-
通过LayoutInflater的from()方法
View view = LayoutInflater.from(this).inflate(resource,root,attachToRoot);
三、inflate
最后你会发现“二”中这些用法间接或直接的调用了LayoutInflater
中的静态方法:
public static LayoutInflater from(@UiContext Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
它们使用的都是:
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
PS:系统会初始化LAYOUT_INFLATER_SERVICE
服务,AssertionError("LayoutInflater not found.")
几乎不会出现(反正我没看到过)。
然后它们都会调用LayoutInflater.inflate
,而它有三个重载函数
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
//会创建一个XmlResourceParser对象
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
最后方法都会指向inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
。我将展开/省略该方法的部分代码:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
// ... [省略部分代码]
View result = root;
try {
//⬇️advanceToRootNode(parser)展开
//移动解析器到 XML 文档的根元素,找根布局
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//⬆️advanceToRootNode(parser)展开
final String name = parser.getName();
// <merge> 标签只能在 root 非 null 且 attachToRoot 为 true 的情况下使用
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can only be used with a valid ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 创建根视图
// 按下不表 1⃣️
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 如果 root 非 null,则生成与 root 匹配的布局参数
if (root != null) {
params = root.generateLayoutParams(attrs);
// 如果不附加到 root,则先设置布局参数
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
// 递归地填充所有子视图
rInflateChildren(parser, temp, attrs, true);
// 如果 root 非 null 且 attachToRoot 为 true,则将解析的视图附加到 root
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 根据 attachToRoot 决定返回哪个视图
if (root == null || !attachToRoot) {
result = temp;
}
}
}
// ... [省略部分代码]
return result;
}
}
总注释中,我们可以了解到
-
@Nullable ViewGroup root:
- 这个参数指定了布局文件中顶级视图的父容器。它可以是
null
,表示没有父容器。 - 当
root
非null
时,解析出的视图可以选择性地附加到这个root
。 - 当使用
<merge>
标签时,root
不能为null
,且attachToRoot
必须为true
。
- 这个参数指定了布局文件中顶级视图的父容器。它可以是
-
boolean attachToRoot:
- 这个参数决定了解析出的视图是否应该立即附加到
root
视图组。 - 当
attachToRoot
为true
且root
非null
时,解析出的视图会被添加到root
中。 - 当
attachToRoot
为false
时,即使root
非null
,解析出的视图也不会立即添加到root
中,而是返回这个顶级视图供后续操作。
- 这个参数决定了解析出的视图是否应该立即附加到
你可能还是有点迷糊,总结性的来说:在任何我们不负责将View
添加进ViewGroup
的情况下都应该将attachToRoot
设置为false。比如RecyclerView
的onCreateViewHolder
。比如Fragment
的onCreateView
,FragmentManager
负责将 Fragment 的视图插入到容器中。如果在 onCreateView
中已经将视图附加到 root
,那么当 FragmentManager
尝试再次执行这个操作时,就会引发 IllegalStateException
,因为一个视图不能有多个父视图。
反之你就可以使用true值。
四、Factory2和Factory
在inflate
代码1⃣️中,选择性的忽略了createViewFromTag
这个方法的细节。视图如何创建出来?让我们看看最终指向的方法:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// ... [省略部分代码]
try {
//tryCreateView
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
//onCreateView
view = onCreateView(context, parent, name, attrs);
} else {
//createView
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
// ... [省略部分代码]
return view;
}
}
-
tryCreateView
我们可以看到的是,
view
是否为空,直接影响着下面流程。那有必要看看tryCreateView
的具体内容: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; }
好了,对于不太熟悉的人来说,直接懵掉。mFactory2?mFactory?先来说mFactory:
private Factory mFactory; public interface Factory { //当从 LayoutInflater 加载布局时,你可以提供一个回调(hook),在布局加载过程中被调用。你可以使用这个回调来自定义你的 XML 布局文件中可用的标签名 View onCreateView(@NonNull String name, @NonNull Context context,@NonNull AttributeSet attrs); }
看起来
mFactory
允许开发者提供自定义的逻辑来替代或增强标准的视图创建过程。官方🪝钩子!像这样:
LayoutInflater inflater = LayoutInflater.from(context); inflater.setFactory(new LayoutInflater.Factory() { @Override public View onCreateView(String name, Context context, AttributeSet attrs) { // 根据 name 创建自定义视图 if (name.equals("HarmonyOSGreateAgain")) { return new HarmonyOSView(context, attrs); } // 对于非自定义视图,返回 null 以使用默认行为 return null; } });
当然你也可以这样:
LayoutInflater inflater = LayoutInflater.from(context); inflater.setFactory(new LayoutInflater.Factory() { @Override public View onCreateView(String name, Context context, AttributeSet attrs) { // 根据 name 创建自定义视图 if (name.equals("TextView")) { return new HarmonyOSTextView(context, attrs); } // 对于非自定义视图,返回 null 以使用默认行为 return null; } });
没错!你可以创建自定义 UI 组件或者改变标准组件的行为!
接着看看
mFactory2
:private Factory2 mFactory2; public interface Factory2 extends Factory { View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs); }
细心的你发现,这玩意继承自
Factory
,且多了一个parent
参数。是的,如你想的那样,它可以对创建 View 的 Parent 进行控制。这就是它的主要目的。你已经注意到
Factory
是通过setFactory
设置的,那Factory2
你也该猜到了。setFactory2
.....那么什么是标准组件的行为呢?
-
onCreateView
这就要看到
createViewFromTag
中的onCreateView
和createView
了。onCreateView
最终指向createView
,我们看看createView
://反射构造View public final View createView(@NonNull Context viewContext, @NonNull String name,@Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException { // 确保参数不为空 Objects.requireNonNull(viewContext); Objects.requireNonNull(name); // 尝试从缓存中获取视图的构造函数 Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { // 如果构造函数不在缓存中,尝试加载视图类 clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); // 应用过滤器(如果有)来决定是否允许加载类 if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, viewContext, attrs); } } // 获取并缓存构造函数 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 { //反射构造 final View view = constructor.newInstance(args); // 特殊处理 ViewStub // ... [省略 ViewStub 处理逻辑] return view; } finally { mConstructorArgs[0] = lastContext; } } catch (NoSuchMethodException | ClassCastException | ClassNotFoundException | Exception e) { // 处理各种异常 // ... [省略异常处理逻辑] } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
简单吧~总结下来就两个:
- 从缓存集合中获取当前View对应的构造方法,没有则创建,并存入缓存。
- 反射构造方法,创建对应的View对象
-
IllegalStateException
当我们兴致勃勃的去准备大改特改的时候,你会发现:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val inflater: LayoutInflater = LayoutInflater.from(this) //使用LayoutInflater.Factory inflater.factory = LayoutInflater.Factory { name, context, attrs -> null } //使用LayoutInflater.Factory2 inflater.factory2 = object : LayoutInflater.Factory2 { override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { return XXX } override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return XXXX } } } }
两个设置方法都会崩溃~
Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater at android.view.LayoutInflater.setFactory(LayoutInflater.java:317) //和 Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater at android.view.LayoutInflater.setFactory2(LayoutInflater.java:375)
错误来自
setFactory2/setFactory
:(PS:setFactory2/setFactory
基本一致)public void setFactory(Factory factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = factory; } else { mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); } } public void setFactory2(Factory2 factory) { if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = mFactory2 = factory; } else { mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2); } }
我们可以看出,
setFactory2/setFactory
均只能调用一次。但是明明我们只调用了一次?为什么会抛出异常呢? -
AppCompatDelegate.createView
我们四处寻找
setFactory2/setFactory
的使用者,找到了AppCompatDelegate
以及它的实现类AppCompatDelegateImpl
,眼熟吧!【Android一杯冰美式的时间--去找setContentView】提到的!最终我们可以在实现类中找到
@Override public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory2(layoutInflater, this); } else { if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) { Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed" + " so we can not install AppCompat's"); } } }
至于为什么没有
setFactory
的调用,你也找不到呢,因为被弃用了,而setFactory2
也会mFactory = mFactory2 = factory
。相信细心的你也发现~我们继续对setFactory2,进行跟踪,那么我们肯定需要寻找,它的实现类。最后我们可以定位到
AppCompatDelegateImpl
的onCreateView
->createView
方法,我们和LayoutInflater中的createView对比就知道@Override public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { // 检查是否已经有一个 AppCompatViewInflater 实例,如果没有,则创建一个 if (mAppCompatViewInflater == null) { TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); String viewInflaterClassName = a.getString(R.styleable.AppCompatTheme_viewInflaterClass); if (viewInflaterClassName == null) { // 如果在主题中没有指定自定义视图创建器,则使用默认的 AppCompatViewInflater mAppCompatViewInflater = new AppCompatViewInflater(); } else { try { // 尝试通过反射加载并实例化自定义的 AppCompatViewInflater Class<?> viewInflaterClass = mContext.getClassLoader().loadClass(viewInflaterClassName); mAppCompatViewInflater = (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor().newInstance(); } catch (Throwable t) { // 如果反射失败,回退到默认的 AppCompatViewInflater Log.i(TAG, "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", t); mAppCompatViewInflater = new AppCompatViewInflater(); } } } // 标记是否应该继承上下文 boolean inheritContext = false; if (IS_PRE_LOLLIPOP) { // 对于 Android Lollipop 之前的版本,检查是否需要继承上下文 if (mLayoutIncludeDetector == null) { mLayoutIncludeDetector = new LayoutIncludeDetector(); } if (mLayoutIncludeDetector.detect(attrs)) { inheritContext = true; } else { inheritContext = (attrs instanceof XmlPullParser) ? ((XmlPullParser) attrs).getDepth() > 1 : shouldInheritContext((ViewParent) parent); } } // 使用 AppCompatViewInflater 创建视图 return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, // 仅在 Lollipop 之前的版本中读取 android:theme true, // 始终读取 app:theme,用于遗留原因 VectorEnabledTintResources.shouldBeUsed() // 根据配置决定是否使用着色资源 ); }
显然核心代码在
AppCompatViewInflater.createView
中。 -
AppCompatViewInflater.createView
@Nullable public final View createView(@Nullable View parent, @NonNull final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { final Context originalContext = context; // 根据需要调整上下文 if (inheritContext && parent != null) { context = parent.getContext(); } if (readAndroidTheme || readAppTheme) { context = themifyContext(context, attrs, readAndroidTheme, readAppTheme); } if (wrapContext) { context = TintContextWrapper.wrap(context); } View view = null; // 根据标签名创建 AppCompat 支持的视图 switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; // ...其他视图创建逻辑 default: // 尝试使用自定义方法创建视图 view = createView(context, name, attrs); } // 如果原始上下文和调整后的上下文不同,尝试重新创建视图 if (view == null && originalContext != context) { view = createViewFromTag(context, name, attrs); } // 检查视图的 onClick 属性和无障碍属性 if (view != null) { checkOnClickListener(view, attrs); backportAccessibilityAttributes(context, view, attrs); } return view; }
在switch (name)中,返回的都是AppCompatXXX。因此,我们可以确认,默认用于确保在旧版
Android
系统上,应用也能够使用Material Design
样式的视图,同时保持向后兼容性。也就是统一Material Design
样式。而它最后指向了AppCompatViewInflater
好了现在你已经学会使用Factory了。
值得注意的是,一般而言你需要保留
AppCompatViewInflater
做出的兼容操作。所以你需要如此做: LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { // 调用 AppCompatDelegate 的createView方法 getDelegate().createView(parent, name, context, attrs); // 自由发挥 return XXX; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return XXX; } });
显然使用
setFactory
需要在加载布局前,也就是调用inflate方法之前。
五、用途
你可能已经意识到了,setFactory
的用途,通过 LayoutInflater 创建 View 时候的一个回调,可以通过 LayoutInflater.Factory 来改造或定制创建 View 的过程。比如样式替换,比如自定义的View等等等。这里我们展示接管View的背景绘制,你可以扩展成“无需自定义View,直接添加属性便可以实现shape、selector的效果”
以下算是一个通用操作了,也是模仿AppCompatViewInflater的流程:
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.LayoutInflaterCompat
object BackgroundLibrary {
fun inject(context: Context?): LayoutInflater? {
val inflater: LayoutInflater? = if (context is Activity) {
context.layoutInflater
} else {
LayoutInflater.from(context)
}
if (inflater == null) {
return null
}
if (inflater.factory2 == null) {
val factory = setDelegateFactory(context!!)
inflater.factory2 = factory
} else if (inflater.factory2 !is BackgroundFactory) {
forceSetFactory2(inflater)
}
return inflater
}
/**
* 注入自定义 LayoutInflater 工厂的主方法
* 如果因为其他库已经设置了factory,可以使用该方法去进行inject,在其他库的setFactory后面调用即可
*/
fun inject2(context: Context?): LayoutInflater? {
// 根据 Context 类型获取 LayoutInflater 实例
val inflater: LayoutInflater? = if (context is Activity) {
context.layoutInflater
} else {
LayoutInflater.from(context)
}
if (inflater == null) {
return null
}
// 强制设置自定义工厂
forceSetFactory2(inflater)
return inflater
}
// 创建并配置 BackgroundFactory 实例
private fun setDelegateFactory(context: Context): BackgroundFactory {
val factory = BackgroundFactory()
if (context is AppCompatActivity) {
// 如果是 AppCompatActivity 实例,使用其委托创建视图
val delegate = context.delegate
factory.setInterceptFactory { name, context, attrs ->
delegate.createView(null, name, context, attrs)
}
}
return factory
}
// 通过反射技术强制为 LayoutInflater 设置自定义工厂
@SuppressLint("DiscouragedPrivateApi")
private fun forceSetFactory2(inflater: LayoutInflater) {
val compatClass = LayoutInflaterCompat::class.java
val inflaterClass = LayoutInflater::class.java
try {
// 访问私有字段并修改其值,以便可以设置自定义工厂
val sCheckedField = compatClass.getDeclaredField("sCheckedField").apply {
isAccessible = true
setBoolean(compatClass, false)
}
val mFactory = inflaterClass.getDeclaredField("mFactory").apply {
isAccessible = true
}
val mFactory2 = inflaterClass.getDeclaredField("mFactory2").apply {
isAccessible = true
}
// 创建 BackgroundFactory 实例
val factory = BackgroundFactory()
if (inflater.factory2 != null) {
factory.setInterceptFactory2(inflater.factory2)
} else if (inflater.factory != null) {
factory.setInterceptFactory(inflater.factory)
}
// 设置工厂到 LayoutInflater 的 mFactory 和 mFactory2 字段
mFactory2[inflater] = factory
mFactory[inflater] = factory
} catch (e: IllegalAccessException) {
// 处理反射访问异常
e.printStackTrace()
} catch (e: NoSuchFieldException) {
// 处理反射访问异常
e.printStackTrace()
}
}
}
class BackgroundFactory : LayoutInflater.Factory2 {
// 已经存在的工厂和工厂2的引用
private var mViewCreateFactory: LayoutInflater.Factory? = null
private var mViewCreateFactory2: LayoutInflater.Factory2? = null
// 用于创建视图的方法
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
// 检查是否为特定前缀的视图,如果是,则不处理
if (name.startsWith("com.fuck.harmonyos.view")) {
return null
}
var view: View? = null
// 首先尝试使用已经存在的工厂创建视图
if (mViewCreateFactory2 != null) {
view = mViewCreateFactory2!!.onCreateView(name, context, attrs)
if (view == null) {
view = mViewCreateFactory2!!.onCreateView(null, name, context, attrs)
}
} else if (mViewCreateFactory != null) {
view = mViewCreateFactory!!.onCreateView(name, context, attrs)
}
// 对创建的视图应用自定义背景处理
return setViewBackground(name, context, attrs, view)
}
// 设置拦截的工厂
fun setInterceptFactory(factory: LayoutInflater.Factory) {
mViewCreateFactory = factory
}
fun setInterceptFactory2(factory: LayoutInflater.Factory2) {
mViewCreateFactory2 = factory
}
// Factory2 接口的另一个方法实现
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
return onCreateView(name, context, attrs)
}
// 伴生对象,包含静态方法和属性
companion object {
// ... [省略静态方法和属性的注释]
}
}
一个BackgroundFactory,BackgroundLibrary。就可以随意组合了,具体实现可以在BackgroundFactory。慢慢琢磨。
如果是一个懒狗,肯定不乐意在每个Activity中,去添加inject的操作。所以可以直接如此做:
class FuckApplication : Application() {
init {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
BackgroundLibrary.inject(activity)
//BackgroundLibrary.inject2(activity)
}
// ... [省略部分代码]
})
}
}
六、 结尾
部分代码可以在这里看到
感谢仓库BackgroundLibrary,该库基于LayoutInflater.Factory原理完成。
如果您有任何疑问、对文章写的不满意、发现错误、想吐槽或者有更好的想法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏