如何更好的通过Inflate layout的方式来实现自定义view

英文原文 http://trickyandroid.com/protip-inflating-layout-for-your-custom-view/  

今天要讲的是在通过组合的方式实现自定义view(custom compound view)的时候容易遇到的一些问题。

custom compound view:一种通过组合原有安卓控件或者布局而实现的自定义view的方法,与常规的自定义view方法相比,一般来说不需要实现onDraw方法,选择这种方式的场景一般是 有一种布局经常被使用,并且这个布局里面有的元素有一些逻辑需要处理,我们希望将他们封装起来使用,其实这种方式的重点不在于view的自定义这个概念,而是在于封装。-译者注


我们选取一个自定义的组合视图作为例子,尝试了解是如何创建的。


就如你所看到的,我们这里有一个相当典型的view - 一个简单的卡片似的控件。因为在这个卡片中我们有一些逻辑要处理,我决定创建一个自定义的view。这是网上经常看到的一种方法:继承自一个现有的布局控件然后在初始化期间inflate一个自定义的布局:


Card.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.trickyandroid.customview.app.view;
 
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
 
import com.trickyandroid.customview.app.R;
 
public class Card extends RelativeLayout {
     private TextView header;
     private TextView description;
     private ImageView thumbnail;
     private ImageView icon;
 
     public Card(Context context) {
         super (context);
         init();
     }
 
     public Card(Context context, AttributeSet attrs) {
         super (context, attrs);
         init();
     }
 
     public Card(Context context, AttributeSet attrs, int defStyle) {
         super (context, attrs, defStyle);
         init();
     }
 
     private void init() {
         inflate(getContext(), R.layout.card,  this );
         this .header = (TextView)findViewById(R.id.header);
         this .description = (TextView)findViewById(R.id.description);
         this .thumbnail = (ImageView)findViewById(R.id.thumbnail);
         this .icon = (ImageView)findViewById(R.id.icon);
     }
}

card.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version= "1.0"  encoding= "utf-8" ?>
 
<RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     android:layout_width= "match_parent"
     android:layout_height= "wrap_content"
     android:padding= "@dimen/card_padding"
     android:background= "@color/card_background" >
 
     <ImageView
         android:id= "@+id/thumbnail"
         android:src= "@drawable/thumbnail"
         android:layout_width= "72dip"
         android:layout_height= "72dip"
         android:scaleType= "centerCrop" />
 
     <TextView
         android:id= "@+id/title"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:text= "Card title"
         android:layout_toRightOf= "@+id/thumbnail"
         android:layout_toLeftOf= "@+id/icon"
         android:textAppearance= "@android:style/TextAppearance.Holo.Medium"
         android:layout_marginLeft= "?android:attr/listPreferredItemPaddingLeft" />
 
     <TextView
         android:id= "@+id/description"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:text= "Card description"
         android:layout_toRightOf= "@+id/thumbnail"
         android:layout_below= "@+id/title"
         android:layout_toLeftOf= "@+id/icon"
         android:layout_marginLeft= "?android:attr/listPreferredItemPaddingLeft"
         android:textAppearance= "@android:style/TextAppearance.Holo.Small" />
 
     <ImageView
         android:id= "@+id/icon"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:src= "@drawable/icon"
         android:layout_alignParentRight= "true"
         android:layout_centerVertical= "true" />
 
</RelativeLayout>

好了,当我们想要使用刚刚新建的自定义view的时候,我们只须将这个view像一般的控件那样添加到主布局中:

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     xmlns:tools= "http://schemas.android.com/tools"
     android:layout_width= "match_parent"
     android:layout_height= "match_parent"
     android:paddingLeft= "@dimen/activity_horizontal_margin"
     android:paddingRight= "@dimen/activity_horizontal_margin"
     android:paddingTop= "@dimen/activity_vertical_margin"
     android:paddingBottom= "@dimen/activity_vertical_margin" >
 
     <com.trickyandroid.customview.app.view.Card
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content" />
 
</FrameLayout>

看起来很简单,但是别慌,让我们看看view的层次结构:


如你所见,在包含卡片内容本身的RelativeLayout之外还有一层RelativeLayout(打问号的那个)。这是因为我们的Card类就是一个RelativeLayout(继承自RelativeLayout),当我们再inflate一个内容的时候,我们只是将内容添加到了这个RelativeLayout中。

当然,假如我们不对父RelativeLayout做任何事情,这并不是什么大问题。但是当我们的布局更复杂或者自定义view的数量增大的时候,你就会注意到性能方面的问题了。这是因为UI引擎要遍历,测量,摆放所有这些布局非常吃力。


简单说来就是 - 布局越深,越难遍历。因此,尽量使布局扁平化。


让我们看看该如何让布局更扁平化:

Merge

在本例中,减少布局数量的一种可取的方法就是让卡片本身的内容直接依附在父view中(即Card类)

card.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version= "1.0"  encoding= "utf-8" ?>
 
     android:layout_width= "match_parent"
     android:layout_height= "wrap_content"
     android:padding= "@dimen/card_padding"
     android:background= "@color/card_background" >
 
     <ImageView
         android:id= "@+id/thumbnail"
         android:src= "@drawable/thumbnail"
         android:layout_width= "72dip"
         android:layout_height= "72dip"
         android:scaleType= "centerCrop" />
 
     <TextView
         android:id= "@+id/title"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:text= "Card title"
         android:layout_toRightOf= "@+id/thumbnail"
         android:layout_toLeftOf= "@+id/icon"
         android:textAppearance= "@android:style/TextAppearance.Holo.Medium"
         android:layout_marginLeft= "?android:attr/listPreferredItemPaddingLeft" />
 
     <TextView
         android:id= "@+id/description"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:text= "Card description"
         android:layout_toRightOf= "@+id/thumbnail"
         android:layout_below= "@+id/title"
         android:layout_toLeftOf= "@+id/icon"
         android:layout_marginLeft= "?android:attr/listPreferredItemPaddingLeft"
         android:textAppearance= "@android:style/TextAppearance.Holo.Small" />
 
     <ImageView
         android:id= "@+id/icon"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:src= "@drawable/icon"
         android:layout_alignParentRight= "true"
         android:layout_centerVertical= "true" />
 
</merge>

下面是我们所得到的效果:

好极了!我们消除了冗余的RelativeLayout,但是也让最顶层原本有的那些属性也没了 - 白色背景和padding。这是因为<marge>标签可以融合其内容,但是不包括自身,因此顶层的属性都丢失了。

有三种办法可以将它们添加回来:

1)在代码中添加:

Card.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
     .....
     private void init() {
         inflate(getContext(), R.layout.card,  this );
         setBackgroundColor(getResources().getColor(R.color.card_background));
 
         //Add missing top level attributes    
         int padding = (int)getResources().getDimension(R.dimen.card_padding);
         setPadding(padding, padding, padding, padding);
 
         this .header = (TextView)findViewById(R.id.header);
         this .description = (TextView)findViewById(R.id.description);
         this .thumbnail = (ImageView)findViewById(R.id.thumbnail);
         this .icon = (ImageView)findViewById(R.id.icon);
     }

2) 在主布局中添加card的时候添加:

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     xmlns:tools= "http://schemas.android.com/tools"
     android:layout_width= "match_parent"
     android:layout_height= "match_parent"
     android:paddingLeft= "@dimen/activity_horizontal_margin"
     android:paddingRight= "@dimen/activity_horizontal_margin"
     android:paddingTop= "@dimen/activity_vertical_margin"
     android:paddingBottom= "@dimen/activity_vertical_margin" >
 
     <com.trickyandroid.customview.app.view.Card
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:background= "@color/card_background"
         android:padding= "@dimen/card_padding" />
 
</FrameLayout>

3)定义一个stylable 属性将这些值通过style提供给控件。感谢 @vovkab 为我指出该方法 :

我不喜欢这种方法,因为代码相互依赖的地方变多了,这个类复杂点还好说,如果类本身很简单,又有如此多的关联,不爽  - 译者注。

attr.xml

1
2
3
4
5
6
<?xml version= "1.0"  encoding= "utf-8" ?>
<resources>
     <declare-styleable name= "Card" >
         <attr name= "cardStyle"  format= "reference" />
     </declare-styleable>
</resources>

style.xml

1
2
3
4
5
6
7
8
9
10
11
12
     <!-- Base application theme. -->
     <style name= "AppTheme"  parent= "android:Theme.Holo.Light.DarkActionBar" >
         <item name= "android:windowBackground" >@color/main_background</item>
         <item name= "cardStyle" >@style/CardStyle</item>
     </style>
 
     <style name= "CardStyle"  parent= "android:Widget.Holo.Light" >
         <item name= "android:padding" >@dimen/card_padding</item>
         <item name= "android:background" >@color/card_background</item>
     </style>
 
</resources>

Card.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Card extends RelativeLayout {
     private TextView header;
     private TextView description;
     private ImageView thumbnail;
     private ImageView icon;
 
     public Card(Context context) {
         super (context,  null , R.attr.cardStyle);
         init();
     }
 
     public Card(Context context, AttributeSet attrs) {
         super (context, attrs, R.attr.cardStyle);
         init();
     }
     ..........

注意在view的构造函数中指定的是我们的stylable。   

Include

另一种减少布局数量的方法是使用<include>标签。因为Card.java是一个RelativeLayout,我们可以让他作为内容的跟布局,然后使用include将它包含在main activity的布局中:

card.xml:    

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<com.trickyandroid.views.Card xmlns:android= "http://schemas.android.com/apk/res/android"
     android:layout_width= "match_parent"
     android:layout_height= "wrap_content"
     android:padding= "@dimen/card_padding"
     android:background= "@color/card_background" >
 
     <ImageView
         android:id= "@+id/thumbnail"
         android:src= "@drawable/thumbnail"
         android:layout_width= "72dip"
         android:layout_height= "72dip"
         android:scaleType= "centerCrop" />
 
     <TextView
         android:id= "@+id/title"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:text= "Card title"
         android:layout_toRightOf= "@+id/thumbnail"
         android:layout_toLeftOf= "@+id/icon"
         android:textAppearance= "@android:style/TextAppearance.Holo.Medium"
         android:layout_marginLeft= "?android:attr/listPreferredItemPaddingLeft" />
 
     <TextView
         android:id= "@+id/description"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:text= "Card description"
         android:layout_toRightOf= "@+id/thumbnail"
         android:layout_below= "@+id/title"
         android:layout_toLeftOf= "@+id/icon"
         android:layout_marginLeft= "?android:attr/listPreferredItemPaddingLeft"
         android:textAppearance= "@android:style/TextAppearance.Holo.Small" />
 
     <ImageView
         android:id= "@+id/icon"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:src= "@drawable/icon"
         android:layout_alignParentRight= "true"
         android:layout_centerVertical= "true" />
 
</com.trickyandroid.views.Card>

Card.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Card extends RelativeLayout {
     private TextView header;
     private TextView description;
     private ImageView thumbnail;
     private ImageView icon;
 
     public Card(Context context) {
         super (context);
     }
 
     public Card(Context context, AttributeSet attrs) {
         super (context, attrs);
     }
 
     public Card(Context context, AttributeSet attrs, int defStyle) {
         super (context, attrs, defStyle);
     }
 
     @Override
     protected void onFinishInflate() {
         super .onFinishInflate();
         this .header = (TextView)findViewById(R.id.title);
         this .description = (TextView)findViewById(R.id.description);
         this .thumbnail = (ImageView)findViewById(R.id.thumbnail);
         this .icon = (ImageView)findViewById(R.id.icon);
     }
}

这种情况下,我们不需要主动inflate内容 - 因为它本身就在那里(activity的setContentView会做这个事情)。因此不再需呀init()了,所有view的初始化都在onFinishInflate()回调方法中了。

 

现在的问题是如何将这个自定义view添加到主布局中。使用<include>:

  其实我觉得这段应该和上面的那段顺序颠倒下- 译者注。

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     android:layout_width= "match_parent"
     android:layout_height= "match_parent"
     android:paddingBottom= "@dimen/activity_vertical_margin"
     android:paddingLeft= "@dimen/activity_horizontal_margin"
     android:paddingRight= "@dimen/activity_horizontal_margin"
     android:paddingTop= "@dimen/activity_vertical_margin" >
 
     <include
         layout= "@layout/card"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"  />
 
</FrameLayout>

 

我个人偏爱的是使用merge + stylable的方法,因为<include>让你失去了对view的控制权。

有些人问我为什么不直接使用<include>,而使用自定义view,这是因为这个view中我还有其他的逻辑。ps 做过的都知道咋回事了,我遇到的一种场景就是,不想使用Fragment,但是又要像Fragment那样是一个整体 -  译者注。

Official Android docs 
Layout optimization tricks by Romain Guy


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的Android通讯录的代码实现,供你参考: 1. 在activity_main.xml布局文件中添加ListView和FloatingActionButton控件: ``` <ListView android:id="@+id/contact_list" android:layout_width="match_parent" android:layout_height="match_parent"/> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/add_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:src="@drawable/ic_add"/> ``` 2. 在MainActivity.java文件中定义Contact类,并初始化ListView和FloatingActionButton控件: ``` public class Contact { private String name; private String phone; public Contact(String name, String phone) { this.name = name; this.phone = phone; } public String getName() { return name; } public String getPhone() { return phone; } } private ArrayList<Contact> contacts = new ArrayList<>(); private ListView listView; private FloatingActionButton addButton; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = findViewById(R.id.contact_list); addButton = findViewById(R.id.add_button); addButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 弹出对话框添加联系人信息 // ... } }); // 初始化联系人列表 contacts.add(new Contact("张三", "13811111111")); contacts.add(new Contact("李四", "13822222222")); contacts.add(new Contact("王五", "13833333333")); // 初始化联系人列表适配器 ContactAdapter adapter = new ContactAdapter(this, contacts); listView.setAdapter(adapter); // 设置长按列表项删除联系人功能 listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { // 删除联系人 contacts.remove(position); adapter.notifyDataSetChanged(); return true; } }); } ``` 3. 创建自定义适配器ContactAdapter,并在getView方法中显示联系人信息: ``` public class ContactAdapter extends BaseAdapter { private Context context; private ArrayList<Contact> contacts; public ContactAdapter(Context context, ArrayList<Contact> contacts) { this.context = context; this.contacts = contacts; } @Override public int getCount() { return contacts.size(); } @Override public Object getItem(int position) { return contacts.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(context).inflate(R.layout.contact_item, parent, false); } TextView nameView = convertView.findViewById(R.id.name_view); TextView phoneView = convertView.findViewById(R.id.phone_view); Contact contact = contacts.get(position); nameView.setText(contact.getName()); phoneView.setText(contact.getPhone()); return convertView; } } ``` 4. 在弹出的添加联系人对话框中,使用AlertDialog.Builder构建对话框,添加EditText等UI控件用于输入联系人信息,并在点击“确定”按钮时,将联系人信息添加到contacts列表中,并刷新ListView控件: ``` AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("添加联系人"); View view = LayoutInflater.from(this).inflate(R.layout.contact_dialog, null); builder.setView(view); final EditText nameEdit = view.findViewById(R.id.name_edit); final EditText phoneEdit = view.findViewById(R.id.phone_edit); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String name = nameEdit.getText().toString(); String phone = phoneEdit.getText().toString(); Contact contact = new Contact(name, phone); contacts.add(contact); adapter.notifyDataSetChanged(); } }); builder.setNegativeButton("取消", null); builder.show(); ``` 以上就是一个简单的Android通讯录的代码实现,其中包括联系人信息的添加、删除和显示功能。实际开发中还需要考虑更多的细节问题,比如数据验证、异常处理、用户体验等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值