前言
对Android开发者而言,学习到一定时候,就会发现Android自带的控件诸如TextView,Button,ImageView,配合上布局如LinearLayout, RelativeLayout,TableLayout,FrameLayout等,做不出自己想要的效果。这个时候就要用到自定义控件。
- 自定义空间的有点有很多,包括:
- 用户交互体验的优化;
- 在大数据量情况下自定义控件比写布局效率高
- 布局上更加符合需求;
- 能配合App整体UI风格的设计;
- 等等
现在网上有许多开源的自定义控件共享,供开发者使用。而我们在实际情况中也会遇到需要自定义控件的时候,今天我们就来一起学习如何自定义控件。
(PS:本文内容基于官方教程进行学习解析,若想查看原文请点击阅读。
参考博客:guolin博文&博文,大家可以看看,受益匪浅)
View
View是我们平时所见到的所有视图的父类,每个布局,控件,都是直接或者间接继承自View。一个View在屏幕上显示出来要涉及到三个函数measure(),layout()和draw()。
-
它们的作用分别是:
- measure()测量视图布局大小
- layout()设置视图在屏幕中的位置
- draw()将视图显示在屏幕上
-
Measure:
- 调用measure()测量
- measure()中调用onMeasure(),onMeasure()是实际测量视图大小的函数
- onMeasure()中调用setMeasureDimension()保存测量结果;
-
Layout:
- layout调用setFrame(l,t,r,b),l,t,r,b即left, top, right, bottom,是与父View的距离。setFrame()会判断这个视图大小有没有改变来决定是否重绘。然后调用onLayout()
- onLayout()是个空方法由子类去实现,如TextView继承自View里面就对这个方法进行了重写。
- 特别一提,onLayout()在ViewGroup中是个抽象方法,没有进行实现。意思是在如果你创建了一个View继承自ViewGroup,则必须重写onLayout()方法。
-
Draw:
- draw会在一个canvas对象上进行绘制,然后显示在屏幕上
- 和上面两个类似,draw()中也有实际上负责绘制逻辑的函数onDraw(),诸如TextView,ImageView中有自己重写的onDraw()函数。
- onDraw()通过对传入的canvas对象进行逻辑处理,得到所要显示的样子。
自定义控件
知道了View的绘制流程之后,我们开始学习怎么去自定义一个控件。
- 自定义控件按照对View及其子类的依赖程度,可以分为:
- 组合型控件。利用现有控件的组合实现更加复杂的布局。
- 继承型控件。继承现有的View或者View的子类,如TextView,ImageView等,并在此基础上增加一点内容。
- 自创型控件。利用上面说的View绘制原理,自行设计绘制出来的全部内容并加以实现。
组合型控件:
先是在layout中创建一个布局文件,如例子中是一个Button和一个TextView,view_layout.xml,实现类似标题栏的效果:
<?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="50dp"
android:background="#ff0000" >
<Button
android:background="#00ff00"
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:textSize="18sp"
android:text="Button"
android:textColor="#0000ff" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="TextView"
android:textColor="#0000ff"
android:textSize="22sp" />
</RelativeLayout>
然后是控件对应的类,在里面实现逻辑处理,CombView.java:
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.example.wujiayi.testapplication.R;
public class CombView extends RelativeLayout{
private TextView textView;
private Button button;
private int count;
public CombView(Context context, AttributeSet attrs) {
super(context, attrs);
//加载视图的布局
LayoutInflater.from(context).inflate(R.layout.view_layout, this);
textView = findViewById(R.id.text);
button = findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
textView.setText(Integer.toString(++count));
}
});
}
}
例子比较简单,我就不多做介绍了。在写完布局文件和逻辑实现之后,就能开始使用了,activity_main.xml:
<LinearLayout 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:orientation="vertical">
<com.example.testapplication.views.CombView
android:layout_width="match_parent"
android:layout_height="60dp"/>
</LinearLayout>
最后得到的效果图如下:
在点击后TextView字样会变成数字并从1开始计数:
继承型控件:
继承型控件我们要做的是一个MyListview,滑动时在上面显示删除按钮,点击删除按钮时删除该条目。老规矩先创建一个布局文件,delete_btn.xml:
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/delete_button"
android:text="删除"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</Button>
然后是自定义的View的代码,监听手势,逻辑如下所示,也比较简单就不进行解释了,MyListView.java:
//继承自ListView,同时实现OnTouchListener和OnGestureListener接口
public class MyListView extends ListView implements View.OnTouchListener,
GestureDetector.OnGestureListener {
private View delete_btn;
private ViewGroup listView;
private int seletedItem;
private boolean isDeleteshown;
private GestureDetector gestureDetector;
private OnDeleteListener listener;
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
gestureDetector = new GestureDetector(getContext(), this);
setOnTouchListener(this);
}
public void setOnDeleteListener(OnDeleteListener _listener) {
listener = _listener;
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (isDeleteshown) {
listView.removeView(delete_btn);
delete_btn = null;
isDeleteshown = false;
return false;
}else {
return gestureDetector.onTouchEvent(motionEvent);
}
}
@Override
public boolean onDown(MotionEvent motionEvent) {
if (!isDeleteshown) {
seletedItem = pointToPosition((int) motionEvent.getX(), (int) motionEvent.getY());
}
return false;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
return false;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
return false;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
if (!isDeleteshown && Math.abs(velocityX) > Math.abs(velocityY)) {
delete_btn = LayoutInflater.from(getContext()).inflate(
R.layout.delete_btn, null);
delete_btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listView.removeView(delete_btn);
delete_btn = null;
isDeleteshown = false;
listener.onDelete(seletedItem);
}
});
listView = (ViewGroup) getChildAt(seletedItem
- getFirstVisiblePosition());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.addRule(RelativeLayout.CENTER_VERTICAL);
listView.addView(delete_btn, params);
isDeleteshown = true;
}
return false;
}
public interface OnDeleteListener {
void onDelete(int index);
}
到此为止,我们继承自ListVeiw的自定义控件就完成了。下面是使用这个自定义控件的示例,先是创建一个ListView的子项布局文件listview_item.xml:
<?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"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical" >
<TextView
android:id="@+id/listview"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:gravity="start|center_vertical"
android:textColor="#000" />
</RelativeLayout>
布局完之后,用一个adapter去加载刚刚写的的布局,MyAdapter.java:
public class MyAdapter extends ArrayAdapter<String>{
public MyAdapter(Context context, int textViewResourceId, List<String> objects) {
super(context, textViewResourceId, objects);
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.listview_item, null);
} else{
view = convertView;
}
TextView textView = view.findViewById(R.id.listview);
textView.setText(getItem(position));
return view;
}
}
子项布局和加载都完成了以后,就可以在activity_main.xml中引入这个布局了:
<LinearLayout 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:orientation="vertical">
<com.example.testapplication.views.MyListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/my_listview"/>
</LinearLayout>
布局完成,在MainActivity.java中进行点击删除按钮的逻辑实现:
public class MainActivity extends AppCompatActivity {
private MyListView listView;
private MyAdapter adapter;
private List<String> contentList = new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initList();
listView = (MyListView) findViewById(R.id.my_listview);
listView.setOnDeleteListener(new MyListView.OnDeleteListener() {
@Override
public void onDelete(int index) {
contentList.remove(index);
adapter.notifyDataSetChanged();
}
});
adapter = new MyAdapter(this, 0, contentList);
listView.setAdapter(adapter);
}
private void initList() {
for (int i = 0; i < 10; i++) {
contentList.add("Item" + Integer.toString(i));
}
}
}
一起来看看效果吧:
子项中滑动,出现删除按钮
点击删除后
自创型控件:
自创型控件我们做的是一个圆,类似于统计图中的饼状图。由于是自绘的,所以不存在已有控件布局问题,我们直接从measure,layout,draw三步入手,下面给出TestView.java:
public class TestView extends View {
private Paint mPaint;
private RectF oval;
public TestView(Context context) {
super(context);
initView();
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView(){
mPaint = new Paint();
mPaint.setAntiAlias(true);
oval=new RectF();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.EXACTLY:
break;
case MeasureSpec.AT_MOST:
break;
case MeasureSpec.UNSPECIFIED:
break;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(Color.GRAY);
// FILL填充, STROKE描边,FILL_AND_STROKE填充和描边
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
int with = getWidth();
int height = getHeight();
float radius = with / 4;
canvas.drawCircle(with / 2, with / 2, radius, mPaint);
mPaint.setColor(Color.BLUE);
oval.set(with / 2 - radius, with / 2 - radius, with / 2
+ radius, with / 2 + radius);//用于定义的圆弧的形状和大小的界限
canvas.drawArc(oval, 270, 120, true, mPaint); //根据进度画圆弧
}
}
主要实现的步骤是在canvas上画了一个圆形,需要重写onDraw()函数。有时候会需要重写onMeasure()和onLayout(),视实际情况而定。
在写完一个自创型控件之后,我们需要在布局中引用,activity_main.xml:
<LinearLayout 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:orientation="vertical">
<com.example.testapplication.views.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</LinearLayout>
我们一起来看看效果图:
总结
好了,这篇文章就先写到这里。我们来回顾一下全文内容:
我们首先分析了View在屏幕上显示出来需要的步骤包括measure,layout和draw。
然后一起学习了三种类型的控件,包括组合型,继承型和自创型,并在其中运用了上面所学的View显示的知识。
最后,自定义控件中还要考虑自定义属性的问题,我们会在以后的博文中再一起学习这一点。
文章是自己的一点浅陋的理解,如有不当之处欢迎指出,谢谢观看。