自定义CommonLayout继承ConstraintLayout的空指针问题及多态实现疑问
我想通过继承ConstraintLayout并加载指定XML实现可点击的自定义控件,后续用来实现多态特性。具体思路是:把包含两个TextView的ConstraintLayout封装成CommonLayout类,通过类方法动态设置内部TextView的文本,再把CommonLayout实例加载到LinearLayout中构建动态主菜单。
我写的CommonLayout代码如下:
public class CommonLayout extends ConstraintLayout { private TextView myTitle; private TextView myDesc; public CommonLayout(@NonNull Context context) { super(context); loadWidgets(); } public CommonLayout(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); loadWidgets(); } public CommonLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); loadWidgets(); } public CommonLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); loadWidgets(); } private void loadWidgets() { myTitle = this.findViewById(R.id.layoutTitle); myDesc = this.findViewById(R.id.layoutDesc); } public void setMyTitle(String input) { myTitle.setText(input); } public void setMyDescription(String input) { myDesc.setText(input); } }
对应的view_commonlayout.xml布局:
<com.mydomain.myapp.views.custom_widgets.CommonLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginVertical="10dp" android:layout_marginHorizontal="10dp" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> <TextView android:id="@+id/layoutTitle" android:textColor="#FFFFFF" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Here goes THE TITLE" tools:layout_editor_absoluteX="15dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" android:textSize="24sp"/> <TextView android:id="@+id/layoutDesc" android:textColor="#FFFFFF" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Here goes the description" tools:layout_editor_absoluteX="15dp" app:layout_constraintTop_toBottomOf="@id/layoutTitle" app:layout_constraintStart_toStartOf="parent" android:textSize="12sp"/> </com.mydomain.myapp.views.custom_widgets.CommonLayout>
在Fragment中使用时,调用setMyTitle和setMyDescription会触发NullPointerException,栈信息显示myTitle为null,this.findViewById()查找失败。我临时在setter方法里重新执行findViewById解决了问题,但不确定这个方案是否优雅。
另外还有几个疑问:
- 这个自定义控件能不能设置点击事件?
- 如何通过继承ConstraintLayout并实现通用接口强制子类遵循规范,从而实现多态?
- 后续能不能扩展不同布局结构的子类控件,满足多样化需求?
1. 解决findViewById返回Null的问题
核心问题是自定义View的构造方法中没有加载XML布局,导致布局树里还没有layoutTitle和layoutDesc这两个TextView,所以findViewById返回null。临时在setter里重新查找的方案不优雅,正确做法是先加载布局再初始化控件:
public class CommonLayout extends ConstraintLayout { private TextView myTitle; private TextView myDesc; public CommonLayout(@NonNull Context context) { super(context); initLayout(); } public CommonLayout(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); initLayout(); } public CommonLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initLayout(); } public CommonLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initLayout(); } private void initLayout() { // 加载XML布局到当前CommonLayout实例中,第三个参数为true表示将布局添加为当前View的子节点 LayoutInflater.from(getContext()).inflate(R.layout.view_commonlayout, this, true); // 此时可以正确找到控件 myTitle = findViewById(R.id.layoutTitle); myDesc = findViewById(R.id.layoutDesc); } public void setMyTitle(String input) { myTitle.setText(input); } public void setMyDescription(String input) { myDesc.setText(input); } }
2. 给自定义控件设置点击事件
完全可以设置点击事件,因为ConstraintLayout继承自View,直接使用View的点击事件API即可:
代码中设置
CommonLayout commonLayout = new CommonLayout(getContext()); commonLayout.setOnClickListener(v -> { // 自定义点击逻辑 });
XML中设置
如果在布局文件中使用CommonLayout,可以直接添加属性:
<com.mydomain.myapp.views.custom_widgets.CommonLayout ... android:clickable="true" android:onClick="onMenuClicked"/>
然后在对应的Activity/Fragment中实现方法:
public void onMenuClicked(View view) { // 点击逻辑 }
也可以封装自定义的点击方法:
public void setOnMenuClickListener(OnClickListener listener) { setOnClickListener(listener); }
3. 用接口实现多态规范
定义通用接口强制子类遵循统一行为,从而实现多态:
步骤1:定义接口
public interface MenuWidget { void setTitle(String title); void setDescription(String desc); void setOnMenuClickListener(OnClickListener listener); }
步骤2:让CommonLayout实现接口
public class CommonLayout extends ConstraintLayout implements MenuWidget { // ... 原有代码 ... @Override public void setTitle(String title) { setMyTitle(title); } @Override public void setDescription(String desc) { setMyDescription(desc); } @Override public void setOnMenuClickListener(OnClickListener listener) { setOnClickListener(listener); } }
步骤3:多态使用
后续所有菜单控件都实现MenuWidget接口,使用时用接口类型引用:
MenuWidget menuItem = new CommonLayout(getContext()); menuItem.setTitle("菜单标题"); menuItem.setDescription("菜单描述"); menuItem.setOnMenuClickListener(v -> { // 统一处理点击逻辑 });
不管后续扩展多少子类,只要实现MenuWidget接口,就能用统一方式调用,实现多态。
4. 扩展不同布局结构的子类
完全可以扩展不同布局的子类,有两种常见方式:
方式1:继承CommonLayout,替换布局
如果子类布局不同但核心逻辑一致,可继承CommonLayout并重写initLayout加载自定义XML:
public class IconMenuLayout extends CommonLayout { private ImageView menuIcon; public IconMenuLayout(@NonNull Context context) { super(context); } public IconMenuLayout(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void initLayout() { LayoutInflater.from(getContext()).inflate(R.layout.view_icon_menu, this, true); myTitle = findViewById(R.id.iconMenuTitle); myDesc = findViewById(R.id.iconMenuDesc); menuIcon = findViewById(R.id.menuIcon); } public void setIcon(int resId) { menuIcon.setImageResource(resId); } }
方式2:直接继承ConstraintLayout并实现MenuWidget接口
如果子类逻辑和布局差异较大,可直接继承ConstraintLayout并实现接口:
public class GridMenuLayout extends ConstraintLayout implements MenuWidget { private TextView title; private LinearLayout actionButtons; public GridMenuLayout(@NonNull Context context) { super(context); initLayout(); } private void initLayout() { LayoutInflater.from(getContext()).inflate(R.layout.view_grid_menu, this, true); title = findViewById(R.id.gridMenuTitle); actionButtons = findViewById(R.id.actionButtons); } @Override public void setTitle(String title) { this.title.setText(title); } @Override public void setDescription(String desc) { // 该布局无描述,可空实现或按需处理 } @Override public void setOnMenuClickListener(OnClickListener listener) { setOnClickListener(listener); } // 自定义方法 public void addActionButton(String text, OnClickListener listener) { Button button = new Button(getContext()); button.setText(text); button.setOnClickListener(listener); actionButtons.addView(button); } }
两种方式可根据子类与父类的逻辑关联程度选择,满足多样化布局需求。
内容的提问来源于stack exchange,提问作者SebasSBM

