自定义组合控件实现布局重用

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标签显然不行,无法设置每个卡片上的图片和文字。这个时候第三种方式组合控件就派上用场了。

图一.png

自定义组合控件:

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。
具体可看这里这里

图二.png

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步已经实现了一个简单的组合控件,剩下的就是使用这个自定义控件了,用法和使用系统自带的控件类似。
要注意的是这里命名空间有两种方式可选(本例中用的是第二种)

  1. xmlns:名称="http://schemas.android.com/apk/res/包路径"
  2. 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
来源点这里

以上就是一个自定义组合控件的完整步骤了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 在A君又一次放我鸽子后,我决心下次再也不约他了。他的理由简直荒唐可笑,仅仅因为室友一个电话就轻易爽约。好朋友也不是...
    慢小小阅读 502评论 6 5
  • 非常感谢张飞清老师的精心准备,将强化第一次理解的较为透彻。 强化分为正强化、负强化、惩罚和消退。强化就是为了...
    娟子_7e81阅读 191评论 0 1
  • 一般会有要求指定长度的昵称合法性验证,比如要求6-18位字符、数字和下划线。通常的做法会是使用正则表达式,但是英文...
    Francis_Rose阅读 1,576评论 0 0
  • 石火光阴,二十几年过去,有些事儿已经随时远逝,有些记忆却愈久弥新,越大对老家的眷恋越发深沉,越珍惜每次回家的机会。...
    木本秋阅读 260评论 0 1