【问题标题】:Android: Multiple view children for custom view with existing layoutAndroid:具有现有布局的自定义视图的多个视图子视图
【发布时间】:2012-06-27 05:19:00
【问题描述】:

我必须在 Android 中构建一个更复杂的自定义视图。最终布局应如下所示:

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

但是,我只想在 XML 文件中定义这个(不定义 SomeView、SomeOtherView 等):

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

这在 Android 中是否可行,如果可以:最干净的方法是什么? 我想到的可能解决方案是“覆盖 addView() 方法”和“删除所有视图并稍后再添加”,但我不确定该走哪条路……

非常感谢您的帮助! :)

【问题讨论】:

    标签: java android xml android-layout android-custom-view


    【解决方案1】:

    创建自定义容器视图是绝对可行的,并且受到鼓励。这就是 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>
    

    使用&lt;merge&gt; 的原因是为了简化层次结构。所有子视图都将附加到您的自定义类,即RelativeLayout。如果您不使用&lt;merge&gt;,您最终会将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()
    

    【讨论】:

    • 不是解决方案,因为我说我想在其他地方定义孩子(不在自定义视图中)。更准确地说:我有布局文件 a.xml、b.xml、c.xml - 都使用相同的自定义视图,但在 LinearLayout 中有不同的子级。
    • 这也是完全可以接受的,我想说最简单的方法是按照您的描述覆盖addView()。我编辑了上面的例子。
    • 你确定这是对的吗?我正在尝试完成几乎相同的事情(创建了一个自定义 Card 扩展 LinearLayout ,其中包含一个 TextView 作为标题和另一个 LinearLayout 包含卡的子级。但是,同时膨胀 card.xml在构造函数中,Card.addView() 被调用,它试图将卡片本身添加到它的一个孩子中,从而产生 NullPointerException...
    • NullPointerException 是由在给定根视图(在本例中为 this)上调用 addView 的 inflate 方法引起的,并且在调用 addView 时 mContentView 为空。如果 mContentView 为 null,则可以通过调用 super.addView 来解决...
    • 在大多数情况下,这很好用,但我发现当MyCustomView 扩展RelativeLayout 并且添加孩子的容器是LinearLayout 时,在 XML 中为这些设置的边距孩子不受尊重。但是,将 MyCustomView 更改为扩展 LinearLayout 会按预期尊重子页边距。两个 ViewGroup 的 LayoutParams 之间是否需要进行任何转换以支持此功能?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-25
    • 1970-01-01
    • 2011-07-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多