带有子视图的自定义可扩展卡片
Custom expandable card with child views
我是Android开发的新手,感觉这真的是一个微不足道的问题,但是我不能很好地表达出来,无法在网上找到解决方案,所以我不妨在这里问一下。
我的目标是创建一个可重用的组件,它本质上是一个可扩展的卡片,如下所述:https://material.io/design/components/cards.html#behavior。
为此,我创建了一个扩展 CardView 的自定义视图:
public class ExpandableCardView extends CardView {
public ExpandableCardView(Context context) {
super(context);
}
public ExpandableCardView(Context context, AttributeSet attrs) {
super(context, attrs);
// get custom attributes
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExpandableCardView, 0, 0);
String heading = array.getString(R.styleable.ExpandableCardView_heading);
array.recycle();
// inflate the layout
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.expandable_card_view, this, true);
// set values
TextView headingTextView = findViewById(R.id.card_heading);
headingTextView.setText(heading.toUpperCase());
// set collapse/expand click listener
ImageView collapseExpandButton = findViewById(R.id.collapse_expand_card_button);
collapseExpandButton.setOnClickListener((View v) -> toggleCardBodyVisibility());
}
private void toggleCardBodyVisibility() {
LinearLayout description = findViewById(R.id.card_body);
ImageView imageButton = findViewById(R.id.collapse_expand_card_button);
if (description.getVisibility() == View.GONE) {
description.setVisibility(View.VISIBLE);
imageButton.setImageResource(R.drawable.ic_arrow_up);
} else {
description.setVisibility(View.GONE);
imageButton.setImageResource(R.drawable.ic_arrow_down);
}
}
}
布局:
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/expandable_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="16dp"
android:animateLayoutChanges="true"
app:cardCornerRadius="4dp">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_header"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/card_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/colorPrimary"
android:layout_alignParentLeft="true"
android:text="HEADING"/>
<ImageView
android:id="@+id/collapse_expand_card_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
app:srcCompat="@drawable/ic_arrow_down"/>
</RelativeLayout>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_body"
android:padding="12dp"
android:layout_marginTop="28dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:visibility="gone" >
</LinearLayout>
</androidx.cardview.widget.CardView>
最终我希望能够在我的活动中像这样使用它,通常每个 activity:
多个实例
<xx.xyz.yy.customviews.ExpandableCardView
android:id="@+id/card_xyz"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom_xxx:heading="SOME HEADING" >
<SomeView></SomeView>
</xx.xyz.yy.customviews.ExpandableCardView>
其中 SomeView 是任何文本、图像、布局或其他自定义视图,通常具有从 activity.
绑定的数据
如何让它在卡体内呈现 SomeView?我想采用自定义视图中定义的任何子结构,并在展开时将其显示在卡体中。希望我说得容易理解。
我认为更好的方法是在单独的文件中定义将插入 CardView
("SomeView") 的布局,并使用如下自定义属性引用它:
<xx.xyz.yy.customviews.ExpandableCardView
android:id="@+id/card_xyz"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom_xxx:heading="SOME HEADING"
custom_xxx:expandedView="@layout/some_view"/>
我会在最后解释我的理由,但让我们看看对你的问题的回答。
您在代码中看到的可能是 SomeView
和 expandable_card_view
同时出现在布局中。这是因为 SomeView
被 CardView
隐式膨胀,然后 expandable_card_view
通过显式 inflation 添加。由于直接使用布局 XML 文件很困难,我们将让隐式 inflation 发生,以便自定义 CardView
仅包含 SomeView
.
然后我们将从布局中删除 SomeView
,将其隐藏,然后在其位置插入 expandable_card_view
。完成后,SomeView
将被重新插入到 ID 为 card_body
的 LinearLayout
中。所有这些都必须在初始布局完成后完成。为了在初始布局完成后获得控制权,我们将使用 ViewTreeObserver.OnGlobalLayoutListener
。这是更新的代码。 (为了简化示例,我删除了一些内容。)
ExpandableCardView
public class ExpandableCardView extends CardView {
public ExpandableCardView(Context context) {
super(context);
}
public ExpandableCardView(Context context, AttributeSet attrs) {
super(context, attrs);
// Get control after layout is complete.
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// Remove listener so it won't be called again
getViewTreeObserver().removeOnGlobalLayoutListener(this);
// Get the view we want to insert into the LinearLayut called "card_body" and
// remove it from the custom CardView.
View childView = getChildAt(0);
removeAllViews();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.expandable_card_view, ExpandableCardView.this, true);
// Insert the view into the LinearLayout.
((LinearLayout) findViewById(R.id.card_body)).addView(childView);
// And the rest of the stuff...
TextView headingTextView = findViewById(R.id.card_heading);
headingTextView.setText("THE HEADING");
// set collapse/expand click listener
ImageView collapseExpandButton = findViewById(R.id.collapse_expand_card_button);
collapseExpandButton.setOnClickListener((View v) -> toggleCardBodyVisibility());
}
});
}
private void toggleCardBodyVisibility() {
LinearLayout description = findViewById(R.id.card_body);
ImageView imageButton = findViewById(R.id.collapse_expand_card_button);
if (description.getVisibility() == View.GONE) {
description.setVisibility(View.VISIBLE);
imageButton.setImageResource(R.drawable.ic_arrow_up);
} else {
description.setVisibility(View.GONE);
imageButton.setImageResource(R.drawable.ic_arrow_down);
}
}
}
expandable_card_view.java
CardView
标签更改为 merge
以避免 CardView
直接嵌套在 CardView
.
中
<merge
android:id="@+id/expandable_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="16dp"
android:animateLayoutChanges="true"
app:cardCornerRadius="4dp">
<RelativeLayout
android:id="@+id/card_header"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/card_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/colorPrimary"
android:layout_alignParentLeft="true"
android:text="HEADING"/>
<ImageView
android:id="@+id/collapse_expand_card_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
app:srcCompat="@drawable/ic_arrow_down"/>
</RelativeLayout>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_body"
android:padding="12dp"
android:layout_marginTop="28dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:visibility="gone" >
</LinearLayout>
</merge>
activity_main.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.customcardview.ExpandableCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_android" />
<TextView
android:id="@+id/childView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Say my name."
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.example.customcardview.ExpandableCardView>
</LinearLayout>
那么,为什么我建议您使用自定义属性将 SomeView
包含在布局中,正如我在开头指出的那样?在上面概述的方式中,SomeView
将始终被夸大并且需要一些努力来切换布局,尽管 SomeView
可能永远不会显示。例如,如果您在 RecyclerView
中有很多自定义 CardViews
,这将是昂贵的。通过使用自定义属性来引用外部布局,您只需要在显示时膨胀 SomeView
并且代码会更加简单易懂。只是我的两分钱,这可能并不重要,具体取决于您打算如何使用自定义视图。
我是Android开发的新手,感觉这真的是一个微不足道的问题,但是我不能很好地表达出来,无法在网上找到解决方案,所以我不妨在这里问一下。
我的目标是创建一个可重用的组件,它本质上是一个可扩展的卡片,如下所述:https://material.io/design/components/cards.html#behavior。
为此,我创建了一个扩展 CardView 的自定义视图:
public class ExpandableCardView extends CardView {
public ExpandableCardView(Context context) {
super(context);
}
public ExpandableCardView(Context context, AttributeSet attrs) {
super(context, attrs);
// get custom attributes
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExpandableCardView, 0, 0);
String heading = array.getString(R.styleable.ExpandableCardView_heading);
array.recycle();
// inflate the layout
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.expandable_card_view, this, true);
// set values
TextView headingTextView = findViewById(R.id.card_heading);
headingTextView.setText(heading.toUpperCase());
// set collapse/expand click listener
ImageView collapseExpandButton = findViewById(R.id.collapse_expand_card_button);
collapseExpandButton.setOnClickListener((View v) -> toggleCardBodyVisibility());
}
private void toggleCardBodyVisibility() {
LinearLayout description = findViewById(R.id.card_body);
ImageView imageButton = findViewById(R.id.collapse_expand_card_button);
if (description.getVisibility() == View.GONE) {
description.setVisibility(View.VISIBLE);
imageButton.setImageResource(R.drawable.ic_arrow_up);
} else {
description.setVisibility(View.GONE);
imageButton.setImageResource(R.drawable.ic_arrow_down);
}
}
}
布局:
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/expandable_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="16dp"
android:animateLayoutChanges="true"
app:cardCornerRadius="4dp">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_header"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/card_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/colorPrimary"
android:layout_alignParentLeft="true"
android:text="HEADING"/>
<ImageView
android:id="@+id/collapse_expand_card_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
app:srcCompat="@drawable/ic_arrow_down"/>
</RelativeLayout>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_body"
android:padding="12dp"
android:layout_marginTop="28dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:visibility="gone" >
</LinearLayout>
</androidx.cardview.widget.CardView>
最终我希望能够在我的活动中像这样使用它,通常每个 activity:
多个实例<xx.xyz.yy.customviews.ExpandableCardView
android:id="@+id/card_xyz"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom_xxx:heading="SOME HEADING" >
<SomeView></SomeView>
</xx.xyz.yy.customviews.ExpandableCardView>
其中 SomeView 是任何文本、图像、布局或其他自定义视图,通常具有从 activity.
绑定的数据如何让它在卡体内呈现 SomeView?我想采用自定义视图中定义的任何子结构,并在展开时将其显示在卡体中。希望我说得容易理解。
我认为更好的方法是在单独的文件中定义将插入 CardView
("SomeView") 的布局,并使用如下自定义属性引用它:
<xx.xyz.yy.customviews.ExpandableCardView
android:id="@+id/card_xyz"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom_xxx:heading="SOME HEADING"
custom_xxx:expandedView="@layout/some_view"/>
我会在最后解释我的理由,但让我们看看对你的问题的回答。
您在代码中看到的可能是 SomeView
和 expandable_card_view
同时出现在布局中。这是因为 SomeView
被 CardView
隐式膨胀,然后 expandable_card_view
通过显式 inflation 添加。由于直接使用布局 XML 文件很困难,我们将让隐式 inflation 发生,以便自定义 CardView
仅包含 SomeView
.
然后我们将从布局中删除 SomeView
,将其隐藏,然后在其位置插入 expandable_card_view
。完成后,SomeView
将被重新插入到 ID 为 card_body
的 LinearLayout
中。所有这些都必须在初始布局完成后完成。为了在初始布局完成后获得控制权,我们将使用 ViewTreeObserver.OnGlobalLayoutListener
。这是更新的代码。 (为了简化示例,我删除了一些内容。)
ExpandableCardView
public class ExpandableCardView extends CardView {
public ExpandableCardView(Context context) {
super(context);
}
public ExpandableCardView(Context context, AttributeSet attrs) {
super(context, attrs);
// Get control after layout is complete.
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// Remove listener so it won't be called again
getViewTreeObserver().removeOnGlobalLayoutListener(this);
// Get the view we want to insert into the LinearLayut called "card_body" and
// remove it from the custom CardView.
View childView = getChildAt(0);
removeAllViews();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.expandable_card_view, ExpandableCardView.this, true);
// Insert the view into the LinearLayout.
((LinearLayout) findViewById(R.id.card_body)).addView(childView);
// And the rest of the stuff...
TextView headingTextView = findViewById(R.id.card_heading);
headingTextView.setText("THE HEADING");
// set collapse/expand click listener
ImageView collapseExpandButton = findViewById(R.id.collapse_expand_card_button);
collapseExpandButton.setOnClickListener((View v) -> toggleCardBodyVisibility());
}
});
}
private void toggleCardBodyVisibility() {
LinearLayout description = findViewById(R.id.card_body);
ImageView imageButton = findViewById(R.id.collapse_expand_card_button);
if (description.getVisibility() == View.GONE) {
description.setVisibility(View.VISIBLE);
imageButton.setImageResource(R.drawable.ic_arrow_up);
} else {
description.setVisibility(View.GONE);
imageButton.setImageResource(R.drawable.ic_arrow_down);
}
}
}
expandable_card_view.java
CardView
标签更改为 merge
以避免 CardView
直接嵌套在 CardView
.
<merge
android:id="@+id/expandable_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="16dp"
android:animateLayoutChanges="true"
app:cardCornerRadius="4dp">
<RelativeLayout
android:id="@+id/card_header"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal" >
<TextView
android:id="@+id/card_heading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/colorPrimary"
android:layout_alignParentLeft="true"
android:text="HEADING"/>
<ImageView
android:id="@+id/collapse_expand_card_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
app:srcCompat="@drawable/ic_arrow_down"/>
</RelativeLayout>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_body"
android:padding="12dp"
android:layout_marginTop="28dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:visibility="gone" >
</LinearLayout>
</merge>
activity_main.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.customcardview.ExpandableCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_android" />
<TextView
android:id="@+id/childView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Say my name."
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.example.customcardview.ExpandableCardView>
</LinearLayout>
那么,为什么我建议您使用自定义属性将 SomeView
包含在布局中,正如我在开头指出的那样?在上面概述的方式中,SomeView
将始终被夸大并且需要一些努力来切换布局,尽管 SomeView
可能永远不会显示。例如,如果您在 RecyclerView
中有很多自定义 CardViews
,这将是昂贵的。通过使用自定义属性来引用外部布局,您只需要在显示时膨胀 SomeView
并且代码会更加简单易懂。只是我的两分钱,这可能并不重要,具体取决于您打算如何使用自定义视图。