自定义Android控件及其子控件

12

我试图创建一个自定义的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);
    }
}

如何将用户指定的内容(main.xml中的TextView)插入到正确的位置(id = container)?

谢谢!

Romain

----- 编辑 -------

仍然没有找到解决方法,所以我改变了我的设计,使用更简单的布局,并决定接受一些重复的XML。不过仍然对任何知道如何实现这个功能感兴趣!

4个回答

11

这个问题困扰我已经有一段时间了,但直到现在我才解决了它。

乍一看,问题在于一个声明式内容(比如您的TextView)是在构造函数之后实例化的,所以在我们通常膨胀布局的构造函数中既有声明式内容又有模板内容时,要将前者推入后者还为时过早。

我找到了一个可以操作这两者的地方: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> 

7
这种方法让我的代码量大大减少了! :)
正如esteewhy所解释的那样,在onFinishInflate()中将xml定义的内容替换为您自己布局内部想要的位置即可。例如:
我将在xml中指定的内容拿出来:
<se.jog.custom.ui.Badge ... >
    <ImageView ... />
    <TextView ... />
</se.jog.custom.ui.Badge>

我想把它们移到我的名为contents的内部LinearLayout中:

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属性,事情变得更易维护。


谢谢更好的解释)) 的确,使用addView()是个好主意! - esteewhy

1

您可以通过扩展LinearLayout来创建自己的MyFancyLayout类。添加三个构造函数,这些构造函数调用一个方法(在本例中为“initialize”)来设置其余的视图:

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();
}

在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()来节省一些开销。

谢谢!这应该会帮很大忙。所以在这种情况下,我猜测在main.xml中提供的子项已经被附加了。你有没有成功地将它们放到其他地方?在我的情况下,我想重新将它们附加到代码创建的一个子项(即“容器”)上。 - Romain
没错,它们都附加在一起了。如果您想将它们移动到另一个容器中,可以通过循环来实现(使用getChildCount查看数量,使用getChildAt(index)获取每个视图),并将它们添加到其他ViewGroup中。添加到其他视图后,您可以使用removeViewAt(index)将其从MyFancyLayout的直接子项中删除。 - Ian G. Clifton
其实,现在我想起来了,孩子们可能要到那之后才会被填充,所以你可能不能立即循环遍历它们。至于这一点我不确定。 - Ian G. Clifton
今晚我得试验一下,谢谢。如果行的话,我会发布修改后的代码! - Romain

0

谢谢。是的,我确实需要完整的范围,我只是简化了一下。但我的问题是所有这些示例都没有使用任何子元素,例如<ToggleButton ... />。在我的情况下,我想要里面有内容,就像普通布局一样。 - Romain
嗯...例如<org.myprogram.MyFancyLayout android:layout_width="fill_parent">...</org.myprogram.MyFancyLayout>不起作用? - Igor
我的意思是你可以像标准布局一样在<org.myprogram.MyFancyLayout>中放置参数,它应该能正常工作。 - Igor
我编辑了原始帖子以包含更多细节。您建议的方法非常好,但我似乎无法将用户提供的“真实”内容与MyFancyLayout的内容(额外的边框...)结合起来。 - Romain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接