尽管 Android 系统提供了很多控件,可是有不少酷炫效果仍然是系统原生控件没法实现的。好在 Android 容许自定义控件,来弥补原生控件的不足。可是在不少初学者看来,自定义 View 彷佛很难掌握。其中有很大一部分缘由是咱们平时看到的自定义 View 使用中,有多种形式,有的寥寥数笔,有的逻辑很复杂,有的直接继承 View 或 ViewGroup,有的却直接继承系统的原生控件,有的能够直接使用系统定义的属性,而有的却自定义了本身的属性。
实际上实现自定义 View 的方式,从总体上看,只分为三种:组合控件,继承控件,自绘控件。而后就是根据须要来添加自定义的属性,就这么简单。android
组合控件,顾名思义,就是将系统原有的控件进行组合,构成一个新的控件。这种方式下,不须要开发者本身去绘制图上显示的内容,也不须要开发者重写 onMeasure,onLayout,onDraw 方法来实现测量、布局以及 draw 流程。因此,在实现自定义 view 的三种方式中,这一种相对比较简单。
实际开发中,标题栏就是一个比较常见的例子。由于在一个 app 的各个界面中,标题栏基本上是大同小异,复用率很高。因此常常会将标题栏单独作成一个自定义 view,在不一样的界面直接引入便可,而不用每次都把标题栏布局一遍。本节就自定义一个标题栏,包含标题和返回按钮两个控件,来介绍这种组合控件的实现方式。canvas
定义标题栏的布局文件 custom_title_view.xml,将返回按钮和标题文本进行组合。这一步用于肯定标题栏的样子,代码以下所示:app
<?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:background="@android:color/holo_orange_light"> <Button android:id="@+id/btn_left" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="5dp" android:text="Back" android:textColor="@android:color/white" /> <TextView android:id="@+id/title_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Title" android:textColor="@android:color/white" android:textSize="20sp" /> </RelativeLayout>
public class CustomTitleView extends FrameLayout implements View.OnClickListener { private View.OnClickListener mLeftOnClickListener; private Button mBackBtn; private TextView mTittleView; public CustomTitleView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.custom_title_view, this); mBackBtn = findViewById(R.id.btn_left); mBackBtn.setOnClickListener(this); mTittleView = findViewById(R.id.title_tv); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_left: if (mLeftOnClickListener != null) { mLeftOnClickListener.onClick(v); } break; } } public void setLeftOnClickListener(View.OnClickListener leftOnClickListener) { mLeftOnClickListener = leftOnClickListener; } public void setTittle(String title){ mTittleView.setText(title); } }
为了编译理解和记忆,这里对该部分作一点说明:ide
在 Activity 的布局文件 activity_custom_view_compose_demo.xml 中,像使用系统控件同样使用 CustomTitleView 便可。前说了,CustomTitleView 本身就是继承的现成的系统布局,因此它们拥有的属性特性,CustomTitleView 同样拥有。函数
<?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="match_parent"> <com.example.demos.customviewdemo.CustomTitleView android:id="@+id/customview_title" android:layout_width="match_parent" android:layout_height="wrap_content"> </com.example.demos.customviewdemo.CustomTitleView> </RelativeLayout>
public class CustomViewComposeDemoActivity extends AppCompatActivity { private CustomTitleView mCustomTitleView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_custom_view_compose_demo); mCustomTitleView = findViewById(R.id.customview_title); mCustomTitleView.setTittle("This is Title"); mCustomTitleView.setLeftOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); } }
获取到 CustomTitleView 实例,设置标题文字,而后自定义"Back"按钮点击事件 setLeftOnClickListener()。工具
按照如上的 4 步,就经过组合控件完成了一个比较简单的自定义标题栏。可见,这种方式是很是简单的。布局
经过继承系统控件(View 子类控件或 ViewGroup 子类控件)来完成自定义 View,通常是但愿在原有系统控件基础上作一些修饰性的修改,而不会作大幅度的改动,如在 TextView 的文字下方添加下划线,在 LinearLayout 布局中加一个蒙板等。这种方式每每都会复用系统控件的 onMeasure 和 onLayout 方法,而只须要重写 onDraw 方法,在其中绘制一些须要的内容。下面会分别继承 View 类控件和 ViewGroup 类控件来举例说明。测试
以下示例为在 TextView 文字下方显示红色下划线,其基本步骤以下:
(1)继承 View 控件,并重写 onDraw 方法this
@SuppressLint("AppCompatCustomView") public class UnderlineTextView extends TextView{ public UnderlineTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setColor(Color.RED); paint.setStrokeWidth(5); int width = getWidth(); int height = getBaseline(); canvas.drawLine(0,height,width,height,paint); } }
(2)在布局文件中调用
就像使用一个普通 TextView 同样使用 UnderlineTextView。spa
<?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="match_parent"> <com.example.demos.customviewdemo.UnderlineTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="50dp" android:layout_centerInParent="true" android:text="Hello World!"/> </RelativeLayout>
(3)效果图
以下示例演示,在 layout 布局上添加一个浅红色的半透明蒙板,这种需求在工做中也是很是常见的。
(1)继承 ViewGroup 类系统控件
public class ForegroundLinearLayout extends LinearLayout{ public ForegroundLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); canvas.drawColor(Color.parseColor("#50FF0000")); } }
(2)在布局文件中调用
对 ForegroundLinearLayout 的使用,就和使用其父类 LinearLayout 同样。
<?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="match_parent"> <com.example.demos.customviewdemo.ForegroundLinearLayout android:layout_width="match_parent" android:layout_height="200dp" android:layout_centerInParent="true" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Hello World!" android:textColor="@android:color/black" android:textSize="50dp" /> </com.example.demos.customviewdemo.ForegroundLinearLayout> </RelativeLayout>
(3)效果图
在宽为全屏宽度,高为 200dp 的布局范围内,绘制完子其子控件 TextView 后,在上面覆盖了一层浅红色的半透明蒙板。
从上面两个例子可见,继承系统原有的控件来实现自定义 View,步骤很是简单,比组合控件简单多了。可是这一节须要对 Canvas,paint 等绘制方面的知识有必定的了解,且还须要对 ViewGroup 的中内容的绘制顺序有必定的了解,才能在原生控件的基础上作出想要的效果来。
这三种方法中,自绘控件是最复杂的,由于全部的绘制逻辑和流程都须要本身完成。采用自绘控件这种方式时,若是自定义 View 为最终的叶子控件,那么须要直接继承 View;而不过自定义 View 为容器类控件,则须要直接继承 ViewGroup。这里依然针对直接继承 View 和 ViewGroup 分别举例进行说明。
一、自绘叶子 View 控件
这里经过画一个直方图来展现自绘 View 控件的实现。
自绘叶子 View 控件时,最主要工做就是绘制出丰富的内容,这一过程是在重写的 onDraw 方法中实现的。因为是叶子 view,它没有子控件了,因此重写 onLayout 没有意义。onMeasure 的方法能够根据本身的须要来决定是否须要重写,不少状况下,不重写该方法并不影响正常的绘制。
public class HistogramView extends View{ private Paint mPaint; private Path mPath; public HistogramView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPath = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制坐标轴 mPaint.reset(); mPath.reset(); mPaint.setColor(Color.BLACK); mPaint.setStyle(Paint.Style.STROKE); mPath.moveTo(100,100); mPath.rLineTo(0,402); mPath.rLineTo(800,0); canvas.drawPath(mPath,mPaint); //绘制文字 mPaint.reset(); mPaint.setTextSize(30); mPaint.setStyle(Paint.Style.FILL); canvas.drawText("Froyo",160,540,mPaint); canvas.drawText("CB",280,540,mPaint); canvas.drawText("ICS",380,540,mPaint); canvas.drawText("J",480,540,mPaint); canvas.drawText("KitKat",560,540,mPaint); canvas.drawText("L",690,540,mPaint); canvas.drawText("M",790,540,mPaint); //绘制直方图,柱形图是用较粗的直线来实现的 mPaint.reset(); mPaint.setColor(Color.GREEN); mPaint.setStrokeWidth(80); float[] lines3={ 200,500,200,495, 300,500,300,480, 400,500,400,480, 500,500,500,300, 600,500,600,200, 700,500,700,150, 800,500,800,350, }; canvas.drawLines(lines3,mPaint); } }
(2)在 Activity 界面的布局文件中引入
和其它自定义控件同样,直接在布局文件中引入便可。
<?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="match_parent"> <com.example.demos.customviewdemo.HistogramView android:layout_width="match_parent" android:layout_height="wrap_content"/> </RelativeLayout>
(3)效果图
这里经过自定义一个父布局控件,并添加一个子 view 来做为例子讲解该方法的实现。
(1)直接继承 ViewGroup 类
自绘 ViewGroup 控件,须要直接继承 ViewGroup,在该系列第一篇文章中将绘制流程的时候就讲过,onLayout 是 ViewGroup 中的抽象方法,其直接继承者必须实现该方法。因此这里,onLayout 方法必需要实现的,若是这里面的方法体为空,那该控件的子 view 就没法显示了。要想准确测量,onMeasure 方法也是要重写的。下面例子中,只演示了第一个子 view 的测量和布局,onLayout 方法中的 child.layout,就完成了对子 view 的布局。
public class CustomLayout extends ViewGroup { public CustomLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() > 0) { //只测量第一个child View child = getChildAt(0); measureChild(child, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { //只布局第一个child View child = getChildAt(0); child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); } } }
(2)在布局文件中和普通父布局同样被引入
<?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="match_parent"> <com.example.demos.customviewdemo.CustomLayout android:layout_width="match_parent" android:layout_centerInParent="true" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="50dp"/> </com.example.demos.customviewdemo.CustomLayout> </RelativeLayout>
(3)效果图
上述代码中 android:layout_centerInParent="true"没有起效,从布局上看 TextView 应该是处于屏幕的正中央,可是实际结果却仍是在左上方显示。这是由于 CustomLayout 控件,并无实现 android:layout_centerInParent 这个属性,因此是无效的。关于属性的问题,正是下一节要介绍的内容。
咱们在使用 Android 原生控件的时候,常常能够看到在布局文件中能够设置不少的属性值,如:
<TextView android:id="@+id/title_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Title" android:textColor="@android:color/white" android:textSize="20sp" />
这里能够根据须要随时设置 TextView 要显示的文字,文字颜色,文字大小等各类属性,给使用者带来了极大的方便。咱们在使用自定义 View 的时候,也很是但愿可以像 TextView 等系统原生控件同样经过设置属性值来个性化自定义 View。本节我们在上一节自定义直方图的基础上,来介绍自定义属性的基本使用流程。
在 res/values/下新建资源文件,这里我们命名为 attrs.xml,在其中编写所须要的属性
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="HistogramView"> <attr name="textColor" format="color"/> <attr name="histogramColor" format="color"/> </declare-styleable> </resources>
这里"declare-styleable"中的 name 是自行命名的,能够理解为这个自定义属性集合的名称。代码中包含了两个自定义属性,名称分别为"textColor"和"histogramColor",这里用来设置直方图中文字的颜色和直方图的颜色。format 表示的是属性的格式,这里均设置为"color",表示对应的属性是用来设置颜色值的。对于"format",后面还会详细讲到。其它的就是固定的格式了,直接套用就行。
public class HistogramView extends View{ private Paint mPaint; private Path mPath; private int mTextColor,mHistogramColor; public HistogramView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPath = new Path(); initAttrs(context,attrs); } private void initAttrs(Context context, AttributeSet attrs){ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HistogramView); mTextColor = typedArray.getColor(R.styleable.HistogramView_textColor,Color.BLACK); mHistogramColor = typedArray.getColor(R.styleable.HistogramView_histogramColor,Color.GREEN); typedArray.recycle(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制坐标轴 mPaint.reset(); mPath.reset(); mPaint.setColor(Color.BLACK); mPaint.setStyle(Paint.Style.STROKE); mPath.moveTo(100,100); mPath.rLineTo(0,402); mPath.rLineTo(800,0); canvas.drawPath(mPath,mPaint); //绘制文字 mPaint.reset(); mPaint.setTextSize(30); mPaint.setColor(mTextColor); mPaint.setStyle(Paint.Style.FILL); canvas.drawText("Froyo",160,540,mPaint); canvas.drawText("CB",280,540,mPaint); canvas.drawText("ICS",380,540,mPaint); canvas.drawText("J",480,540,mPaint); canvas.drawText("KitKat",560,540,mPaint); canvas.drawText("L",690,540,mPaint); canvas.drawText("M",790,540,mPaint); //绘制直方图,柱形图是用较粗的直线来实现的 mPaint.reset(); mPaint.setColor(mHistogramColor); mPaint.setStrokeWidth(80); float[] lines3={ 200,500,200,495, 300,500,300,480, 400,500,400,480, 500,500,500,300, 600,500,600,200, 700,500,700,150, 800,500,800,350, }; canvas.drawLines(lines3,mPaint); } }
将上述代码和前面第三节中自绘直方图代码对比,红色部分是修改或新增的代码。初始化属性的地方,这个过程须要在构造函数中完成。其中,和自定义属性集创建联系,获取开发者在布局文件中使用时设置的相应属性值,若是没有设置,则会使用默认设置的颜色,分别为 Color.BLACK 和 Color.GREEN,用完后必定要回收资源。这样就初始化了文字颜色 mTextColor 值和 mHistogramColor 值,在后面 onDraw 中就使用该值来绘制对应的部分。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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"> <com.example.demos.customviewdemo.HistogramView android:layout_width="match_parent" android:layout_height="wrap_content" app:textColor="@android:color/holo_red_light" app:histogramColor="@android:color/holo_blue_bright"/> </RelativeLayout>
这段代码中,第 3,10,11 行和以往的布局文件有些不同,这是使用自定义属性时的固定格式。第 3 行中,若是布局文件中没有这一句,必定要加上,这句是声明命名空间,只有声明了命名空间才能使用自定义属性。"app"是该命名空间的名称,这里是自行命名的,不必定非要用"app"。第 10 行和 11 行,"app:attrName"表示用的是自定义的属性,固定用法,前面 mTextColor 和 mHistogramColor 值就是从这里获取的。
在上面一节中,仅仅只是对文字颜色和直方图颜色的属性值作了设置,是为了演示自定义属性的使用步骤。在实际开发中,彻底能够定义更多类型的属性,如显示文字的内容,文字的大小,直方图的宽度等。format 也不仅限定于"color",还有"String","Integer"等,多种多样。本节就汇总一下平时比较经常使用的一些属性 format。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SelfAttr"> <!--1.reference:参考某一资源ID--> <attr name="background" format="reference" /> <!--2. color:颜色值--> <attr name = "textColor" format = "color" /> <!--3.boolean:布尔值--> <attr name = "focusable" format = "boolean" /> <!--4.dimension:尺寸值--> <attr name = "layout_width" format = "dimension" /> <!--5. float:浮点值--> <attr name = "fromAlpha" format = "float" /> <!--6.integer:整型值--> <attr name = "lines" format="integer" /> <!--7.string:字符串--> <attr name = "text" format = "string" /> <!--8.fraction:百分数--> <attr name = "pivotX" format = "fraction" /> <!--9.enum:枚举值。属性值只能选择枚举值中的一个--> <attr name="orientation"> <enum name="horizontal" value="0" /> <enum name="vertical" value="1" /> </attr> <!--10.flag:位或运算。属性值能够选择其中多个值--> <attr name="gravity"> <flag name="top" value="0x01" /> <flag name="bottom" value="0x02" /> <flag name="left" value="0x04" /> <flag name="right" value="0x08" /> <flag name="center_vertical" value="0x16" /> ... </attr> <!--11.混合类型:属性定义时能够指定多种类型值--> <attr name = "background_2" format = "reference|color" /> </declare-styleable> </resources>
如上列出了平时工做中在常见的 11 种类型的格式,说是 11 种,但最后一种是前面 10 种的组合而已。看到上述的属性名称应该很熟悉吧,都是系统原生控件的属性名称。
以下对上述属性的使用一一举例演示,能够对照着来理解,都是平时经常使用的系统控件。
<!--1.reference:参考某一资源ID--> <ImageView android:background = "@drawable/图片ID"/> <!--2. color:颜色值--> <TextView android:textColor = "#00FF00"/> <!--3.boolean:布尔值--> <Button android:focusable = "true"/> <!--4.dimension:尺寸值--> <Button android:layout_width = "42dp"/> <!--5. float:浮点值--> <alpha android:fromAlpha = "1.0"/> <!--6.integer:整型值--> <TextView android:lines="1"/> <!--7.string:字符串--> <TextView android:text = "我是文本"/> <!--8.fraction:百分数--> <rotate android:pivotX = "200%"/> <!--9.enum:枚举值--> <LinearLayout android:orientation = "vertical"> </LinearLayout> <!--10.flag:位或运算--> <TextView android:gravity="bottom|left"/> <!--11.混合类型:属性定义时能够指定多种类型值--> <ImageView android:background = "@drawable/图片ID" /> <!--或者--> <ImageView android:background = "#00FF00" />
关于自定义 View 的 3 中实现方式以及自定义属性的使用,这里就讲完了。读完后,是否是发现基本的实现流程其实很是简单。固然,本文为了说明实现流程,因此举的例子都比较简单,但不是说绘制内容也同样简单。就好像办理入学手续很简单,但读书这件事却不那么容易同样。要完成一些酷炫的自定义 View,还须要好好地掌握 Canvas,Paint,Path 等工具的使用,以及 View 的绘制流程原理。
Android 自定义View篇(一)View绘制流程
Android 自定义View篇(二)Canvas详解
Android 自定义View篇(三)Paint详解