Android:使用现有布局为自定义视图创建多个视图子项

41

我需要在Android中构建一个更复杂的自定义视图。最终布局应该像这样:

<RelativeLayout>
  <SomeView />
  <SomeOtherView />
  <!-- maybe more layout stuff here later -->
  <LinearLayout>
    <!-- the children -->
  </LinearLayout>
</RelativeLayout>

但是,在XML文件中,我只想定义它(而不定义SomeView、SomeOtherView等内容):

<MyCustomView>
  <!-- the children -->
</MyCustomView>

在Android中是否可能实现此功能,如果可以的话,最干净的方法是什么? 我想到了两种可能的解决方案:'覆盖addView()方法'和'删除所有视图然后稍后再添加它们',但我不确定应该选择哪种方法...

非常感谢您的帮助! :)

1个回答

70

完全可以创建自定义容器视图,并且这是可行的和被鼓励的。在Android中,这被称为复合控件。

public class MyCustomView extends RelativeLayout {
    private LinearLayout mContentView;

    public MyCustomView(Context context) {
        this(context, null);
    }

    public MyCustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        //Inflate and attach your child XML
        LayoutInflater.from(context).inflate(R.layout.custom_layout, this);
        //Get a reference to the layout where you want children to be placed
        mContentView = (LinearLayout) findViewById(R.id.content);

        //Do any more custom init you would like to access children and do setup
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if(mContentView == null){
            super.addView(child, index, params);
        } else {
            //Forward these calls to the content view
            mContentView.addView(child, index, params);
        }
    }
}
您可以覆盖尽可能多的addView()版本,但最终它们都会回调我放置在示例中的版本。仅覆盖此方法将使框架将其XML标记内找到的所有子项传递给特定的子容器。 然后按如下修改XML: res/layout/custom_layout.xml
<merge>
  <SomeView />
  <SomeOtherView />
  <!-- maybe more layout stuff here later -->
  <LinearLayout
      android:id="@+id/content" />
</merge>

使用 <merge> 的原因是为了简化布局层次结构。所有子视图都将附加到您的自定义类,即 RelativeLayout。如果不使用 <merge>,则最终会得到一个与所有子项关联的 RelativeLayout 附加到另一个 RelativeLayout 的情况,这可能会导致问题。


Kotlin 版本:


    private fun expand(view: View) {
        val parentWidth = (view.parent as View).width
        val matchParentMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidth, MeasureSpec.EXACTLY)
        val wrapContentMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)

        view.measure(matchParentMeasureSpec, wrapContentMeasureSpec)
        val targetHeight = view.measuredHeight
        view.isVisible = true
        val animation: Animation = getExpandAnimation(view, targetHeight)

        view.startAnimation(animation)
    }

    private fun getExpandAnimation(
        view: View,
        targetHeight: Int
    ): Animation = object : Animation() {
        override fun applyTransformation(
            interpolatedTime: Float,
            transformation: Transformation
        ) {
            view.layoutParams.height =
                if (interpolatedTime == 1f) {
                    LayoutParams.WRAP_CONTENT
                } else {
                    (targetHeight * interpolatedTime).toInt()
                }

            view.requestLayout()
        }

        override fun willChangeBounds(): Boolean {
            return true
        }
    }.apply {
        duration = getDuration(targetHeight, view)
    }

    private fun collapse(view: View) {
        val initialHeight = view.measuredHeight
        val animation: Animation = getCollapseAnimation(view, initialHeight)

        view.startAnimation(animation)
    }

    private fun getCollapseAnimation(
        view: View,
        initialHeight: Int
    ): Animation = object : Animation() {
        override fun applyTransformation(
            interpolatedTime: Float,
            transformation: Transformation
        ) {
            if (interpolatedTime == 1f) {
                view.isVisible = false
            } else {
                view.layoutParams.height =
                    initialHeight - (initialHeight * interpolatedTime).toInt()
                view.requestLayout()
            }
        }

        override fun willChangeBounds(): Boolean = true

    }.apply {
        duration = getDuration(initialHeight, view)
    }

    /**
     * Speed = 1dp/ms
     */
    private fun getDuration(initialHeight: Int, view: View) =
        (initialHeight / view.context.resources.displayMetrics.density).toLong()

谢谢!很高兴看到这些问题即使经过了这么长时间也得到了解决! :) - mreichelt
1
你确定这是对的吗?我试图完成基本相同的事情(创建一个自定义的Card,扩展LinearLayout,其中包含一个标题TextView和另一个包含卡片子项的LinearLayout)。然而,在构造函数中填充card.xml时,调用了Card.addView(),它试图将卡片本身添加到其子项之一,导致NullPointerException... - Sander
2
NullPointerException是由inflate方法在给定的根视图(在这种情况下为this)上调用addView并且mContentView在调用addView时为空引起的。如果mContentView为空,可以通过调用super.addView来解决它... - haze4real
@Devunwired 如果我理解得足够好的话,您只能有一个子项,对吗?但为什么是LinearLayout?如果有时是LinearLayout而有时是Recyclerview会发生什么? - Sotti
1
大多数情况下这很好用,但我发现当MyCustomView扩展RelativeLayout并且子项添加到的容器是LinearLayout时,为这些子项设置的XML边距不被尊重。然而,将MyCustomView更改为扩展LinearLayout可以按预期尊重子项边距。是否需要在这两个ViewGroup的LayoutParams之间进行任何转换以支持此功能? - Matthew
显示剩余3条评论

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