Настраиваемая расширяемая карточка с дочерними представлениями

Я новичок в разработке 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 внутри тела карты? Я хочу взять любую дочернюю структуру, определенную в пользовательском представлении, и показать ее в теле карточки, когда она будет развернута. Надеюсь, я облегчил понимание.

3
0
1 134
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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"/>

Я объясню свое обоснование в конце, но давайте посмотрим на ответ на ваш вопрос, как указано.

То, что вы, вероятно, видите в своем коде, - это SomeView и expandable_card_view, которые одновременно появляются в макете. Это связано с тем, что SomeView неявно раздувается с помощью CardView, а затем expandable_card_view добавляется посредством явного расширения. Поскольку работа с XML-файлами макета напрямую затруднительна, мы позволим неявной инфляции произойти так, что пользовательский CardView будет содержать только SomeView.

Затем мы удалим SomeView из макета, спрячем его и вставим на его место expandable_card_view. Как только это будет сделано, SomeView будет повторно вставлен в LinearLayout с идентификатором card_body. Все это нужно делать после завершения первоначального макета. Чтобы получить контроль после завершения первоначального макета, мы будем использовать ViewTreeObserver.OnGlobalLayoutListener. Вот обновленный код. (Я удалил несколько вещей, чтобы упростить пример.)

[video]

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 может никогда не отображаться. Это будет дорого, если у вас, например, много нестандартных CardViews в RecyclerView. Используя настраиваемый атрибут для ссылки на внешний макет, вам нужно будет раздуть SomeView только тогда, когда он отображается, и код будет намного проще и понятнее. Только мои два цента, и это может не иметь большого значения в зависимости от того, как вы собираетесь использовать настраиваемый вид.

Благодарим вас за ответ. Только попробовал, работает как положено! Вскоре после того, как я спросил об этом, я создал общедоступный метод в ExpandableCardView, чтобы добавить дочернее представление в тело карты. Затем я просто объявил макет в отдельном файле, увеличил его в действии и вызвал этот метод. Я считаю, что это не идеально, потому что у меня много разных карточек с простым содержанием, для которых я не хочу объявлять отдельные файлы макета. Также моя анимация коллапса довольно странная - тело карточки быстро сворачивается, и вскоре после этого содержимое исчезает. Что я могу с этим поделать?

DoubleZ 21.10.2018 21:23

@DoubleZ Трудно сказать по описанию. Если проблема не исчезнет, ​​лучше всего задать еще один вопрос, подчеркнув проблему, с которой вы столкнулись.

Cheticamp 22.10.2018 13:47

Замена пользовательского корневого элемента представления androidx.cardview.widget.CardView с помощью тега слияния помогла с анимацией, хотя я не уверен, почему именно.

DoubleZ 22.10.2018 19:55

Другие вопросы по теме