具有子视图的自定义展开式卡片

时间:2018-09-28 17:46:39

标签: android android-custom-view

我是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>

最终,我希望能够像在活动中那样使用它,通常每个活动有多个实例:

<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 是任何文本,图像,布局或其他自定义视图,通常带有活动绑定的数据。

我如何在卡体内渲染 SomeView ?我想采用自定义视图中定义的任何子结构,并在展开时在卡主体中显示它。希望我使它变得易于理解。

1 个答案:

答案 0 :(得分:1)

我认为更好的方法是在单独的文件中定义将插入到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"/>

我将在最后解释我的基本原理,但让我们看看对您所陈述问题的回答。

您可能会在代码中看到SomeViewexpandable_card_view一次出现在布局中。这是因为SomeViewCardView隐式地膨胀,然后通过显式膨胀来添加expandable_card_view。由于直接处理布局XML文件很困难,因此我们将使隐式膨胀发生,以使自定义CardView仅包含SomeView

然后,我们将从布局中删除SomeView,将其隐藏,然后在其位置插入expandable_card_view。完成此操作后,SomeView将重新插入ID为LinearLayout的{​​{1}}中。所有这些都必须在完成初始布局后完成。为了在初始布局完成后获得控制权,我们将使用ViewTreeObserver.OnGlobalLayoutListener。这是更新的代码。 (为了简化示例,我删除了一些内容。)

[video]

ExpandableCardView

card_body

expandable_card_view.java
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); } } } 标记更改为CardView,以避免将merge直接嵌套在CardView中。

CardView

activity_main.xml

<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>

那么,为什么我建议您使用一个自定义属性在我一开始就确定的布局中包含<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中有很多自定义CardViews,这将很昂贵。通过使用自定义属性引用外部布局,您只需在显示RecyclerView时对其进行充气,并且代码将变得更加简单易懂。只是我的两分钱,可能并不重要,这取决于您打算如何使用自定义视图。