Я новичок в разработке 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 внутри тела карты? Я хочу взять любую дочернюю структуру, определенную в пользовательском представлении, и показать ее в теле карточки, когда она будет развернута. Надеюсь, я облегчил понимание.
Я думаю, что лучшим подходом было бы определить макет, который будет вставлен в 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. Вот обновленный код. (Я удалил несколько вещей, чтобы упростить пример.)
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 только тогда, когда он отображается, и код будет намного проще и понятнее. Только мои два цента, и это может не иметь большого значения в зависимости от того, как вы собираетесь использовать настраиваемый вид.
@DoubleZ Трудно сказать по описанию. Если проблема не исчезнет, лучше всего задать еще один вопрос, подчеркнув проблему, с которой вы столкнулись.
Замена пользовательского корневого элемента представления androidx.cardview.widget.CardView с помощью тега слияния помогла с анимацией, хотя я не уверен, почему именно.
Благодарим вас за ответ. Только попробовал, работает как положено! Вскоре после того, как я спросил об этом, я создал общедоступный метод в ExpandableCardView, чтобы добавить дочернее представление в тело карты. Затем я просто объявил макет в отдельном файле, увеличил его в действии и вызвал этот метод. Я считаю, что это не идеально, потому что у меня много разных карточек с простым содержанием, для которых я не хочу объявлять отдельные файлы макета. Также моя анимация коллапса довольно странная - тело карточки быстро сворачивается, и вскоре после этого содержимое исчезает. Что я могу с этим поделать?