Пользовательское управление Android с детьми

Я пытаюсь создать пользовательский элемент управления Android, который содержит LinearLayout. Вы можете думать об этом как о расширенном LinearLayout с причудливыми границами, фоном, изображением слева...

Я мог бы сделать все это в XML (отлично работает), но, поскольку в моем приложении есть десятки вхождений, его становится трудно поддерживать. Я подумал, что было бы лучше иметь что-то вроде этого:

/* Main.xml */
<MyFancyLayout>
    <TextView />   /* what goes inside my control's linear layout */
</MyfancyLayout>

Как бы вы подошли к этому? Я бы не хотел переписывать всю линейную компоновку методов onMeasure/onLayout. Вот что у меня есть на данный момент:

/* MyFancyLayout.xml */
<TableLayout>
    <ImageView />
    <LinearLayout id="container" />   /* where I want the real content to go */
</TableLayout>    

а также

/* MyFancyLayout.java */
public class MyFancyLayout extends LinearLayout
{
    public MyFancyLayout(Context context) {
        super(context);
        View.inflate(context, R.layout.my_fancy_layout, this);
    }
}

Как бы вы вставили указанный пользователем контент (TextView в main.xml) в нужное место (id=container)?

Ваше здоровье!

Роман

----- редактировать -------

Все еще не повезло с этим, поэтому я изменил свой дизайн, чтобы использовать более простой макет, и решил жить с небольшим количеством повторяющихся XML. Тем не менее очень интересно, кто знает, как это сделать!


person Romain    schedule 05.01.2011    source источник


Ответы (4)


Этот точный вопрос беспокоил меня уже некоторое время, но только сейчас я его решил.

На первый взгляд, проблема заключается в том, что декларативный контент (TextView в Вашем случае) создается где-то после ctor (где мы обычно раздуваем наши макеты), поэтому слишком рано иметь оба декларативный и шаблонный контент под рукой, чтобы подтолкнуть первое внутрь второго.

Я нашел одно такое место, где мы можем манипулировать обоими: это метод onFinishInflate(). Вот как это происходит в моем случае:

    @Override
    protected void onFinishInflate() {
        int index = getChildCount();
        // Collect children declared in XML.
        View[] children = new View[index];
        while(--index >= 0) {
            children[index] = getChildAt(index);
        }
        // Pressumably, wipe out existing content (still holding reference to it).
        this.detachAllViewsFromParent();
        // Inflate new "template".
        final View template = LayoutInflater.from(getContext())
            .inflate(R.layout.labeled_layout, this, true);
        // Obtain reference to a new container within "template".
        final ViewGroup vg = (ViewGroup)template.findViewById(R.id.layout);
        index = children.length;
        // Push declared children into new container.
        while(--index >= 0) {
            vg.addView(children[index]);
        }

        // They suggest to call it no matter what.
        super.onFinishInflate();
    }

Упомянутый выше файл labeled_layout.xml мало чем отличается от этого:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation             ="vertical"
    android:layout_width            ="fill_parent"
    android:layout_height           ="wrap_content"
    android:layout_marginLeft       ="8dip"
    android:layout_marginTop        ="3dip"
    android:layout_marginBottom     ="3dip"
    android:layout_weight           ="1"
    android:duplicateParentState    ="true">

    <TextView android:id            ="@+id/label"
        android:layout_width        ="fill_parent"
        android:layout_height       ="wrap_content"
        android:singleLine          ="true"
        android:textAppearance      ="?android:attr/textAppearanceMedium"
        android:fadingEdge          ="horizontal"
        android:duplicateParentState="true" />

    <LinearLayout
        android:id                  ="@+id/layout"
        android:layout_width        ="fill_parent"
        android:layout_height       ="wrap_content"
        android:layout_marginLeft   ="8dip"
        android:layout_marginTop    ="3dip" 
        android:duplicateParentState="true" />
</LinearLayout>

Теперь (по-прежнему опуская некоторые детали) в другом месте мы могли бы использовать его так:

        <com.example.widget.LabeledLayout
            android:layout_width    ="fill_parent"
            android:layout_height   ="wrap_content">
            <!-- example content -->
        </com.example.widget.LabeledLayout> 
person esteewhy    schedule 18.04.2011

Такой подход экономит мне много кода! :)

Как объясняет esteewhy, просто поменяйте содержимое, определенное в xml, туда, где вы хотите, внутри вашего собственного макета, в onFinishInflate(). Пример:

Я беру содержимое, которое указываю в xml:

<se.jog.custom.ui.Badge ... >
    <ImageView ... />
    <TextView ... />
</se.jog.custom.ui.Badge>

... и переместите их в мой внутренний LinearLayout под названием contents, где я хочу, чтобы они были:

public class Badge extends LinearLayout {
    //...
    private LinearLayout badge;
    private LinearLayout contents;

    // This way children can be added from xml.
    @Override
    protected void onFinishInflate() {      
        View[] children = detachChildren(); // gets and removes children from parent
        //...
        badge = (LinearLayout) layoutInflater.inflate(R.layout.badge, this);
        contents = (LinearLayout) badge.findViewById(R.id.badge_contents);
        for (int i = 0; i < children.length; i++)
            addView(children[i]); //overridden, se below.
        //...
        super.onFinishInflate();
    }

    // This way children can be added from other code as well.
    @Override
    public void addView(View child) {
        contents.addView(child);
}

В сочетании с настраиваемыми XML-атрибутами все становится очень удобным в сопровождении.

person JOG    schedule 18.05.2011
comment
Спасибо за лучшее объяснение)) Действительно, хорошая идея с addView()! - person esteewhy; 03.01.2013

Вы можете создать свой класс MyFancyLayout, расширив LinearLayout. Добавьте три конструктора, которые вызывают метод (в данном случае «инициализировать») для настройки остальных представлений:

public MyFancyLayout(Context context) {
    super(context);
    initialize();
}

public MyFancyLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    initialize();
}

public MyFancyLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initialize();
}

При инициализации вы делаете все, что вам нужно, чтобы добавить дополнительные представления. Вы можете получить LayoutInflater и раздуть другой макет:

final LayoutInflater inflator = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflator.inflate(R.layout.somecommonlayout, this);

Или вы можете создать представления в коде и добавить их:

        ImageView someImageView = new ImageView(getContext());
        someImageView.setImageDrawable(myDrawable);
        someImageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
        addView(someImageView);

Если вы собираетесь часто использовать контекст, вы можете сохранить ссылку на него в своих конструкторах и использовать его, а не getContext(), чтобы сэкономить немного накладных расходов.

person Ian G. Clifton    schedule 11.01.2011
comment
Спасибо! Это должно очень помочь. Итак, в этом случае я предполагаю, что дочерние элементы, представленные в main.xml, уже подключены. Вам когда-нибудь удавалось размещать их в другом месте? В моем случае я хотел бы снова прикрепить их к одному из дочерних элементов, созданных в коде (контейнер). - person Romain; 11.01.2011
comment
Это правильно; они все привязаны. Если вы хотите переместить их в другой контейнер, вы будете перебирать их (используйте getChildCount, чтобы узнать, сколько у вас есть, и getChildAt(index), чтобы получить каждый) и добавить их в другую ViewGroup. После добавления в другое представление вы можете выполнить команду removeViewAt(index), чтобы удалить его из прямого дочернего элемента MyFancyLayout. - person Ian G. Clifton; 11.01.2011
comment
На самом деле, теперь, когда я думаю об этом, дети могут не быть надуты до тех пор, пока это не произойдет, так что вы не сможете сразу пройти через них. Я не уверен в этом. - person Ian G. Clifton; 11.01.2011
comment
Надо будет вечером поэкспериментировать, спасибо. Я опубликую отредактированный код, если он работает! - person Romain; 11.01.2011

просто используйте что-то вроде этого:

<org.myprogram.MyFancyLayout>
 ...
</org.myprogram.MyFancyLayout>

Полезная ссылка - http://www.anddev.org/creating_custom_views_-_the_togglebutton-t310.html< /а>

person Igor    schedule 05.01.2011
comment
Спасибо. Да, мне действительно нужен полный объем, я упрощал. Но моя проблема в том, что во всех этих примерах не используются дочерние элементы, например ‹ToggleButton ... /›. В моем случае я хотел бы иметь содержимое внутри, как обычный макет. - person Romain; 05.01.2011
comment
хм... например ‹org.myprogram.MyFancyLayout android:layout_width=fill_parent› ...‹/org.myprogram.MyFancyLayout› не работает? - person Igor; 05.01.2011
comment
Я имею в виду, что вы можете поместить параметры в ‹org.myprogram.MyFancyLayout›, как в стандартном макете, и он должен работать нормально. - person Igor; 05.01.2011
comment
Я отредактировал исходный пост, чтобы включить больше деталей. То, что вы предлагаете, отлично работает, но я не могу объединить реальный контент (предоставленный пользователем) и контент MyFancyLayout (дополнительные границы...) - person Romain; 06.01.2011