通过填充布局创建自定义视图?

142

我试图创建一个自定义视图来替换我在多个地方使用的某个布局,但我无法做到这一点。

基本上,我想要替换这个:

<RelativeLayout
 android:id="@+id/dolphinLine"
 android:layout_width="fill_parent"
 android:layout_height="wrap_content"
    android:layout_centerInParent="true"
 android:background="@drawable/background_box_light_blue"
 android:padding="10dip"
 android:layout_margin="10dip">
  <TextView
   android:id="@+id/dolphinTitle"
   android:layout_width="200dip"
   android:layout_height="100dip"
   android:layout_alignParentLeft="true"
   android:layout_marginLeft="10dip"
   android:text="@string/my_title"
   android:textSize="30dip"
   android:textStyle="bold"
   android:textColor="#2E4C71"
   android:gravity="center"/>
  <Button
   android:id="@+id/dolphinMinusButton"
   android:layout_width="100dip"
   android:layout_height="100dip"
   android:layout_toRightOf="@+id/dolphinTitle"
   android:layout_marginLeft="30dip"
   android:text="@string/minus_button"
   android:textSize="70dip"
   android:textStyle="bold"
   android:gravity="center"
   android:layout_marginTop="1dip"
   android:background="@drawable/button_blue_square_selector"
   android:textColor="#FFFFFF"
   android:onClick="onClick"/>
  <TextView
   android:id="@+id/dolphinValue"
   android:layout_width="100dip"
   android:layout_height="100dip"
   android:layout_marginLeft="15dip"
   android:background="@android:drawable/editbox_background"
   android:layout_toRightOf="@+id/dolphinMinusButton"
   android:text="0"
   android:textColor="#2E4C71"
   android:textSize="50dip"
   android:gravity="center"
   android:textStyle="bold"
   android:inputType="none"/>
  <Button
   android:id="@+id/dolphinPlusButton"
   android:layout_width="100dip"
   android:layout_height="100dip"
   android:layout_toRightOf="@+id/dolphinValue"
   android:layout_marginLeft="15dip"
   android:text="@string/plus_button"
   android:textSize="70dip"
   android:textStyle="bold"
   android:gravity="center"
   android:layout_marginTop="1dip"
   android:background="@drawable/button_blue_square_selector"
   android:textColor="#FFFFFF"
   android:onClick="onClick"/>
</RelativeLayout>

通过这个:

<view class="com.example.MyQuantityBox"
    android:id="@+id/dolphinBox"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:myCustomAttribute="@string/my_title"/>

我不想要自定义布局,我想要一个自定义视图(此视图不应该有子视图)。

从一个 MyQuantityBox 的实例到另一个实例唯一可能改变的是标题。我很想能够在 XML 中指定它(就像我在最后一行所做的那样)。

我应该如何做?我应该将 RelativeLayout 放入 /res/layout 中的 XML 文件中,并在我的 MyBoxQuantity 类中进行膨胀吗?如果是这样,我该如何操作?

谢谢!


请参阅 Android 中的“复合控件”,以及此链接:https://dev59.com/9HM_5IYBdhLWcg3wNwMv - greg7gkb
7个回答

173

这篇文章比较旧,但我认为分享一下我的做法还是有益的,基于chubbsondubs的回答:

我使用FrameLayout(参见文档),因为它用于包含单个视图,并从xml中填充该视图。

代码如下:

public class MyView extends FrameLayout {
    public MyView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public MyView(Context context) {
        super(context);
        initView();
    }

    private void initView() {
        inflate(getContext(), R.layout.my_view_layout, this);
    }
}

20
既然View类有静态的inflate()方法,就不需要使用LayoutInflater.from()了。 - outlying
2
这不就是 Johannes 在这里提出的解决方案吗:https://dev59.com/VXTYa4cB1Zd3GeqPzdg7 然而,它仍然会在内部填充另一个布局。所以我想这并不是最好的解决方案。 - Tobias Reich
3
是的,但是 Johannes 的解决方案是从7.24.13的版本开始的,而我的解决方案是从7.1.13的版本开始的... 此外,我的解决方案使用的是 FrameLayout,这个布局只应包含一个 View(在所引用的文档中也是如此写的)。因此,它实际上是用作 View 的占位符。我不知道有任何不涉及使用占位符来膨胀 View 的解决方案。 - Fox
我不明白。那个方法(inflate)返回一个视图,但被忽略了。看起来你需要将它添加到当前视图中。 - Jeffrey Blattman
1
@Jeffrey Blattman 请查看 View.inflate 方法,我们使用这个方法(将根视图指定为 this,即第三个参数)。 - V1raNi

130
这是一个简单的演示,通过从xml中充气来创建自定义视图(复合视图)。

attrs.xml

<resources>
    
    <declare-styleable name="CustomView">
        <attr format="string" name="text"/>
        <attr format="reference" name="image"/>
    </declare-styleable>
</resources>

CustomView.kt

class CustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
        ConstraintLayout(context, attrs, defStyleAttr) {

    init {
        init(attrs)
    }

    private fun init(attrs: AttributeSet?) {
        View.inflate(context, R.layout.custom_layout, this)

        val image_thumb = findViewById<ImageView>(R.id.image_thumb)
        val text_title = findViewById<TextView>(R.id.text_title)

        val ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
        try {
            val text = ta.getString(R.styleable.CustomView_text)
            val drawableId = ta.getResourceId(R.styleable.CustomView_image, 0)
            if (drawableId != 0) {
                val drawable = AppCompatResources.getDrawable(context, drawableId)
                image_thumb.setImageDrawable(drawable)
            }
            text_title.text = text
        } finally {
            ta.recycle()
        }
    }
}

custom_layout.xml

我们应该在这里使用“合并”而不是“ConstraintLayout”,因为
如果我们在这里使用“ConstraintLayout”,布局层次结构将是“ConstraintLayout” - >“ConstraintLayout” - >“ImageView” + “TextView”= >我们有1个冗余的“ConstraintLayout”=>性能不是很好。
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:parentTag="android.support.constraint.ConstraintLayout">

    <ImageView
        android:id="@+id/image_thumb"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="ContentDescription"
        tools:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/text_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="@id/image_thumb"
        app:layout_constraintStart_toStartOf="@id/image_thumb"
        app:layout_constraintTop_toBottomOf="@id/image_thumb"
        tools:text="Text" />

</merge>

Using activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <your_package.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#f00"
        app:image="@drawable/ic_android"
        app:text="Android" />

    <your_package.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#0f0"
        app:image="@drawable/ic_adb"
        app:text="ADB" />

</LinearLayout>

Result

enter image description here

在GitHub上查看完整代码:
Github


19
这应该成为本帖中被接受或投票最多的答案,因为它提到了不必要的布局层次结构。 - Farid
有没有一种方法可以在自定义视图上引用所有可用于文本的属性,并且同时从图像获取它们并将它们挂钩到文本视图和图像视图上,而无需手动进行此操作? - uberchilly
1
经过两个小时的寻找实用例子,我发现了你的例子。这就是为什么你需要往下滚动来看更多内容。非常有帮助,谢谢! - Mykola Yakovliev
5
感谢提到 tools:parentTag,即使在进行了9年的Android开发后我也不知道这个 ❤️ - Billda
4
非常好的回答,谢谢。对于那些喜欢使用视图绑定的人: CustomViewBinding.bind(View.inflate(context, R.layout.custom_layout, this)) - giorgos.nl

28

是的,你可以这样做。RelativeLayout、LinearLayout等都是视图(Views),所以自定义布局就是自定义视图。需要考虑的一点是,如果你想创建自定义布局,你可以这样做。

你要做的是创建一个复合控件(Compound Control)。你将创建一个RelativeLayout子类,在代码中添加所有组件(TextView等),在构造函数中,你可以读取从XML传递过来的属性。然后将该属性传递给你的标题TextView。

http://developer.android.com/guide/topics/ui/custom-components.html


18

使用如下所示的LayoutInflater。

public View myView() {
    View v; // Creating an instance for View Object
    LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    v = inflater.inflate(R.layout.myview, null);

    TextView text1 = v.findViewById(R.id.dolphinTitle);
    Button btn1 = v.findViewById(R.id.dolphinMinusButton);
    TextView text2 = v.findViewById(R.id.dolphinValue);
    Button btn2 = v.findViewById(R.id.dolphinPlusButton);

    return v;
}

我已经尝试过了,它运行良好。但是,当我点击btn1时,它将调用Web服务,并在从服务器接收到响应后,我想更新特定位置text2中的一些文本..请帮忙吗? - harikrishnan

8
在实践中,我发现需要特别小心,尤其是如果您重复使用一些XML。例如,假设您有一个表格,希望为列表中的每个条目创建一个表格行。您已经设置了一些XML:
在 `my_table_row.xml` 中:
<?xml version="1.0" encoding="utf-8"?>
<TableRow xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" android:id="@+id/myTableRow">
    <ImageButton android:src="@android:drawable/ic_menu_delete" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/rowButton"/>
    <TextView android:layout_height="wrap_content" android:layout_width="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="TextView" android:id="@+id/rowText"></TextView>
</TableRow>

接着,您需要用一些代码每行创建一次它。假设您已经定义了一个父TableLayout myTable以将行附加到其中。

for (int i=0; i<numRows; i++) {
    /*
    * 1. Make the row and attach it to myTable. For some reason this doesn't seem
    * to return the TableRow as you might expect from the xml, so you need to
    * receive the View it returns and then find the TableRow and other items, as
    * per step 2.
    */
    LayoutInflater inflater = (LayoutInflater)getBaseContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v =  inflater.inflate(R.layout.my_table_row, myTable, true);

    // 2. Get all the things that we need to refer to to alter in any way.
    TableRow    tr        = (TableRow)    v.findViewById(R.id.profileTableRow);
    ImageButton rowButton = (ImageButton) v.findViewById(R.id.rowButton);
    TextView    rowText   = (TextView)    v.findViewById(R.id.rowText);
                            
    // 3. Configure them out as you need to
    rowText.setText("Text for this row");
    rowButton.setId(i); // So that when it is clicked we know which one has been clicked!
    rowButton.setOnClickListener(this); // See note below ...           

    /*
    * To ensure that when finding views by id on the next time round this
    * loop (or later) gie lots of spurious, unique, ids.
    */
    rowText.setId(1000+i);
    tr.setId(3000+i);
}

关于处理rowButton.setOnClickListener(this)的简单明了的例子,请参见通过程序创建按钮的Onclicklistener


嗨Neil,我已经尝试过了。它运行得很好。但是,当我点击rowButton时,它将调用Web服务,并在从服务器接收到响应后,我想更新特定位置的rowText中的一些文本..请帮忙吗? - harikrishnan
请参见我的问题:https://stackoverflow.com/questions/48719970/android-adding-view-from-inflate-layout-and-how-to-update-particular-textview-va - harikrishnan

1

有多个答案以不同的方式指向同一个方向,我相信以下是最简单的方法之一,而且不使用任何第三方库,甚至可以在Java中使用它。

在Kotlin中:

创建values/attr.xml

<resources>
    <declare-styleable name="DetailsView">
        <attr format="string" name="text"/>
        <attr format="string" name="value"/>
    </declare-styleable>
</resources>

创建 layout/details_view.xml 文件以供您的视图使用。
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/text_label"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            tools:text="Label" />

        <TextView
            android:id="@+id/text_value"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            tools:text="Value" />

    </LinearLayout>

</merge>

创建自定义视图小部件 DetailsView.kt
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import com.payable.taponphone.R

class DetailsView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    private val attributes: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.DetailsView)
    private val view: View = View.inflate(context, R.layout.details_view, this)

    init {
        view.findViewById<TextView>(R.id.text_label).text = attributes.getString(R.styleable.DetailsView_text)
        view.findViewById<TextView>(R.id.text_value).text = attributes.getString(R.styleable.DetailsView_value)
    }
}

现在你可以在应用程序的任何地方调用小部件,如下所示

<com.yourapp.widget.DetailsView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:text="Welcome"
    app:value="Feb" />

-1

使用 Kotlin 创建简单的自定义视图

将 FrameLayout 替换为您想要扩展的任何视图

/**
 * Simple Custom view
 */
class CustomView@JvmOverloads
constructor(context: Context,
            attrs: AttributeSet? = null,
            defStyleAttr: Int = 0)
    : FrameLayout(context, attrs, defStyleAttr) {

    init {
        // Init View
        val rootView = (getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
                .inflate(R.layout.custom_layout, this, true)
        val titleView= rootView.findViewById(id.txtTitle)

        // Load Values from XML
        val attrsArray = getContext().obtainStyledAttributes(attrs, R.styleable.CutsomView, defStyleAttr, 0)
        val titleString = attrsArray.getString(R.styleable.cutomAttrsText)
        attrsArray.recycle()
    }
}

R.styleable.CutsomView和R.styleable.cutomAttrsText是什么? - Andrew Bekhtold

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