You could combine a group of View components into a new single component, perhaps to make something like a ComboBox (a combination of popup list and free entry text field), a dual-pane selector control (a left and right pane with a list in each where you can re-assign which item is in which list), and so on.
背景:
android开发中对于一些重复的布局。通常我们的处理方式不外乎以下三种
1.直接复制粘贴已有布局代码
2.将重复xml布局抽离出来,再利用include和merge标签来使用该布局(merge主要用于优化性能)
3.自定义组合控件
优缺点:
第一种方式优缺点很明显,虽然简单,快捷。但是当重复的布局比较复杂且重复的次数比较多的时候会使得布局文件很臃肿,不利于阅读和修改。
第二种方式可以应付大部分布局重用情况,但也有局限性,比如要实现下图一效果,在同一个布局文件中包含多个复用布局(暂且叫做卡片布局),使用include标签显然不行,无法设置每个卡片上的图片和文字。这个时候第三种方式组合控件就派上用场了。
自定义组合控件:
step1:自定义布局文件(卡片布局)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:scaleType="centerInside"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="卡片文字" />
</LinearLayout>
注释:
这个很简单,就是一个上面图片下面文字的简单布局
step2: 自定义属性
<!-- CardLinearLayout属性 -->
<declare-styleable name="CardLinearLayout">
<attr name="card_text" format="string" /> //文字内容
<attr name="card_text_size" format="dimension" /> //文字尺寸
<attr name="card_text_color" format="color" /> //文字颜色
<attr name="card_text_margin_top" format="dimension" /> //文字顶部margin
<attr name="card_image_src" format="reference" /> //图片资源
<attr name="card_image_width" format="dimension" /> //imageView的宽度
<attr name="card_image_height" format="dimension" /> //imageView的高度
</declare-styleable>
注释:
自定义属性的多少取决于控件的复杂程度以及你想这个控件具备的扩展性强弱,比如这个卡片布局,如果你认为只有这一个页面会用到的话,只定义图片资源和文字内容两个属性,其它的属性在上面的step1中写死,也能达到图一的效果,但这就跟我们布局重用的初衷相违背了。
这里总共定义了7种属性,对于一般情况应该是够用了。当然你也可以继续扩展,比如添加图片是否圆角显示,圆角半径等其它属性来扩展其功能。
对于不同类型的属性,值的类型也不一样,比如文字内容是String类型,文字尺寸这个属性应该是dimension。Android提供了丰富的值类型供我们选择:reference,color,boolean,dimension,float,integer,string,fraction,enum,flag
具体用法可参考这里:Android自定义属性,format详解
step3:代码实现
public class CardLinearLayout extends LinearLayout {
private ImageView mImage;
private TextView mTextView;
private String mText = ""; //文字内容
private int mTextColor; //文字颜色
private int mTextSize ; //文字尺寸
private int mImageWidth ; //imageView的宽度
private int mImageHeight ; //imageView的高度
private Drawable mDrawable; //图片资源
private int mTextMarginTop; //文字顶部margin
public CardLinearLayout(Context context) {
this(context, null);
}
public CardLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
if (!isInEditMode()) {//解决可视化编辑器无法识别自定义控件的问题
// 在构造函数中将Xml中定义的布局解析出来。
LayoutInflater.from(context).inflate(R.layout.card_linealayout, this, true);
mImage = (ImageView) findViewById(R.id.imageview);
mTextView = (TextView) findViewById(R.id.text);
}
//获取在下面step4引用布局时在XML文件中定义的属性值
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardLinearLayout);
mText = a.getString(R.styleable.CardLinearLayout_card_text);
mTextColor = a.getColor(R.styleable.CardLinearLayout_card_text_color, Color.WHITE);
mTextSize = a.getDimensionPixelOffset(R.styleable.CardLinearLayout_card_text_size, 12);
mTextMarginTop = a.getDimensionPixelOffset(R.styleable.CardLinearLayout_card_text_margin_top, 0);
mImageWidth = a.getDimensionPixelOffset(R.styleable.CardLinearLayout_card_image_width, 100);
mImageHeight = a.getDimensionPixelOffset(R.styleable.CardLinearLayout_card_image_height, 100);
mDrawable = a.getDrawable(R.styleable.CardLinearLayout_card_image_src);
//将获取到的值设置到相应位置
if (!isInEditMode()) { //解决可视化编辑器无法识别自定义控件的问题
mTextView.setText(mText);
mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
Log.i("CardLinearLayout", "====mTextSize=" + mTextSize);
mTextView.setTextColor(mTextColor);
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(0, mTextMarginTop, 0, 0);
mTextView.setLayoutParams(layoutParams);
mImage.setImageDrawable(mDrawable);
LayoutParams imageLayoutParams = new LayoutParams(mImageWidth, mImageHeight);
imageLayoutParams.gravity = Gravity.CENTER;
mImage.setLayoutParams(imageLayoutParams);
}
a.recycle(); //这个别忘了
}
/**
* 在代码中设置卡片的图片
* @param imageId 图片id
*/
public void setImageResource(int imageId){
if (mImage !=null) {
mImage.setImageResource(imageId);
}
}
}
注释:
1.关于isInEditMode
上面代码如果不加isInEditMode,在可视化界面会报错,无法预览我们的卡片布局。虽然编译运行后能正常显示,但不方便我们调整界面。加上isInEditMode后step3 中的布局的预览效果见下面图二,因为当前正处于编辑模式,所以部分代码被忽略,无法看到完整效果。
关于isInEditMode,官方的解释是:
Indicates whether this View is currently in edit mode. A View is usually in edit mode when displayed within a developer tool. For instance, if this View is being drawn by a visual user interface builder, this method should return true. Subclasses should check the return value of this method to provide different behaviors if their normal behavior might interfere with the host environment. For instance: the class spawns a thread in its constructor, the drawing code relies on device-specific features, etc. This method is usually checked in the drawing code of custom widgets.
来源
2.关于getDimension(),getDimensionPixelOffset(),getDimensionPixelOffset()的区别
简单粗暴的解释就是:
这三个函数返回的都是绝对尺寸,而不是相对尺寸(dp\sp等)。如果getDimension()返回结果是20.5f,那么getDimensionPixelSize()返回结果就是21,getDimensionPixelOffset()返回结果就是20。
具体可看这里和这里
step4: 引用布局实现图一
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 顶部图片-->
<ImageView
android:id="@+id/topPic"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:src="@drawable/image2" />
<!-- 第一行 -->
...
<!-- 第二行 -->
...
<!-- 第三行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="4dp"
android:gravity="center"
android:orientation="horizontal">
<com.example.ziv.myapplication.CardLinearLayout
android:id="@+id/homeGuide"
android:layout_width="152dp"
android:layout_height="80dp"
android:background="#FFAA3D"
android:gravity="center"
card:card_image_height="48dp"
card:card_image_src="@mipmap/ic_launcher"
card:card_image_width="48dp"
card:card_text="Ruby"
card:card_text_margin_top="2dp"
card:card_text_size="12sp" />
<com.example.ziv.myapplication.CardLinearLayout
android:id="@+id/classSetting"
android:layout_width="152dp"
android:layout_height="80dp"
android:layout_marginLeft="4dp"
android:background="#3398CC"
android:gravity="center"
card:card_image_height="48dp"
card:card_image_src="@mipmap/ic_launcher"
card:card_image_width="48dp"
card:card_text="Swift"
card:card_text_margin_top="2dp"
card:card_text_size="12sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>
注释:
前面3步已经实现了一个简单的组合控件,剩下的就是使用这个自定义控件了,用法和使用系统自带的控件类似。
要注意的是这里命名空间有两种方式可选(本例中用的是第二种)
- xmlns:名称="http://schemas.android.com/apk/res/包路径"
- xmlns:名称="http://schemas.android.com/apk/res-auto"
如果当前工程是作为lib使用时,使用第一种方式会出现找不到自定义属性的错误。
以下是官方解释
Added support for custom views with custom attributes in libraries. Layouts using custom attributes must use the namespace URI http://schemas.android.com/apk/res-auto instead of the URI that includes the app package name. This URI is replaced with the app specific one at build time
来源点这里