Android自定义View的分类
前言
上一篇文章我们知道,自定义View共分为五种,分别是自定义组合View、继承系统View(TextView等)、直接继承View、继承系统ViewGroup控件(LinearLayout等)、直接继承ViewGroup。
一.自定义组合View
自定义组合View就是将多个控件组合成一个新的控件,主要是为了复用。常见的组合控件有标题栏、loading加载框等。
举例说明
1. 编写布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:id="@+id/header_root_layout"
android:layout_height="45dp"
android:background="#827192">
<ImageView
android:id="@+id/iv_back"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentLeft="true"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:src="@drawable/back"
android:scaleType="fitCenter"/>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:lines="1"
android:maxLines="20"
android:ellipsize="end"
android:text="title"
android:textStyle="bold"
android:textColor="#ffffff"/>
<ImageView
android:id="@+id/iv_home"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentRight="true"
android:src="@drawable/home"
android:scaleType="fitCenter"
android:paddingRight="12dp"
android:paddingLeft="12dp"/>
</RelativeLayout>
很简单的布局,左边是一个返回按钮,中间是标题,右边是home按钮
2. 实现构造方法
//因为我们的布局采用RelativeLayout,所以这里继承RelativeLayout。
//关于各个构造方法的介绍可以参考前面的内容
public class CommonHeadView extends RelativeLayout {
public CommonHeadView(Context context) {
super(context);
}
public CommonHeadView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CommonHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
3. 提供对外的方法
//设置标题文字的方法
private void setTitle(String title) {
if (!TextUtils.isEmpty(title)) {
tv_title.setText(title);
}
}
//对左边按钮设置事件的方法
private void setLeftListener(OnClickListener onClickListener) {
iv_back.setOnClickListener(onClickListener);
}
//对右边按钮设置事件的方法
private void setRightListener(OnClickListener onClickListener) {
img_right.setOnClickListener(onClickListener);
}
4. 在布局当中引用该控件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.demo.view.CommonHeadView
android:layout_width="match_parent"
android:layout_height="45dp">
</com.example.example.view.CommonHeadView>
</LinearLayout>
到此基本的功能已经实现了,除了这些基础的功能外,我们还可以做一些功能扩展,比如可以在布局时设置我的View显示的元素,因为可能有些需求并不需要右边的按钮。这时就需要用到自定义属性来解决了。
前面已经简单介绍过自定义属性的相关知识,我们之间看代码
4.1.首先在values目录下创建attrs.xml
内容如下
<resources>
<declare-styleable name="CommonHeadViewStyle">
<attr name="title_text_clolor" format="color" />
<attr name="title_text" format="string" />
<attr name="left_img" format="integer" />
<attr name="center_text" format="integer" />
<attr name="right_text" format="integer" />
</declare-styleable>
</resources>
这里我们定义了三个属性,文字内容、颜色以及要显示的元素。
4.2.在java代码中进行设置
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);
String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
showLeft = mTypedArray.getInt(R.styleable.HeaderBar_left_img, 1);
showCenter = mTypedArray.getInt(R.styleable.HeaderBar_center_text, 1);
showRight = mTypedArray.getInt(R.styleable.HeaderBar_right_text, 1);
text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));
mTypedArray.recycle();
showView();
}
private void showView() {
img_left.setVisibility(showLeft == 1 ? VISIBLE : GONE);
text_center.setVisibility(showCenter == 1 ? VISIBLE : GONE);
tv_next.setVisibility(showRight == 1 ? VISIBLE : GONE);
}
}
4.3 在布局文件中进行设置
<com.example.demo.view.CommonHeadView
android:layout_width="match_parent"
android:layout_height="80dp"
app:title_text="标题"
app:title_text_clolor="#FF03DAC5"
app:left_img="1"
app:center_text="1"
app:right_text="1"
tools:ignore="MissingConstraints">
</com.example.demo.view.CommonHeadView>
OK,到这里整个View基本定义完成。整个CommonHeadView 的代码如下
public class CommonHeadView extends RelativeLayout {
private ImageView img_left;
private TextView text_center;
private TextView tv_next;
private RelativeLayout layout_root;
private Context context;
String element;
private int showLeft;
private int showCenter;
private int showRight;
public CommonHeadView(Context context) {
super(context);
this.context = context;
initView(context);
}
public CommonHeadView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
initView(context);
initAttrs(context, attrs);
}
public CommonHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
initView(context);
initAttrs(context, attrs);
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.CommonHeadViewStyle);
String title = mTypedArray.getString(R.styleable.CommonHeadViewStyle_title_text);
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
showLeft = mTypedArray.getInt(R.styleable.CommonHeadViewStyle_left_img, 1);
showCenter = mTypedArray.getInt(R.styleable.CommonHeadViewStyle_center_text, 1);
showRight = mTypedArray.getInt(R.styleable.CommonHeadViewStyle_right_text, 1);
text_center.setTextColor(mTypedArray.getColor(R.styleable.CommonHeadViewStyle_title_text_clolor, Color.WHITE));
mTypedArray.recycle();
showView();
}
private void showView() {
img_left.setVisibility(showLeft == 1 ? VISIBLE : GONE);
text_center.setVisibility(showCenter == 1 ? VISIBLE : GONE);
tv_next.setVisibility(showRight == 1 ? VISIBLE : GONE);
}
private void initView(final Context context) {
LayoutInflater.from(context).inflate(R.layout.base_title, this, true);
img_left = (ImageView) findViewById(R.id.iv_back);
tv_next = (TextView) findViewById(R.id.tv_next);
text_center = (TextView) findViewById(R.id.tv_title);
layout_root = (RelativeLayout) findViewById(R.id.layout_titlebar);
layout_root.setBackgroundColor(Color.BLACK);
img_left.setOnClickListener(view -> Toast.makeText(context, element + "", Toast.LENGTH_SHORT).show());
}
private void setTitle(String title) {
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
}
private void setLeftListener(OnClickListener onClickListener) {
img_left.setOnClickListener(onClickListener);
}
private void setRightListener(OnClickListener onClickListener) {
tv_next.setOnClickListener(onClickListener);
}
}
二.继承系统控件
继承系统的控件可以分为继承View子类(如TextVIew等)和继承ViewGroup子类(如LinearLayout等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承子View的实现方式。
业务需求:实现一个带边框圆角的TextView。
因为这种实现方式会复用系统的逻辑,大多数情况下我们希望复用系统的onMeaseur和onLayout流程,所以我们只需要重写onDraw方法 。实现非常简单,话不多说,直接上代码。
1.代码
public class MyTextView extends AppCompatTextView {
Paint mPaint;
RectF mRect;
int mColor;
float mSize;
float mDensity;
int mThickness = 2;
DisplayMetrics mMetric = new DisplayMetrics();
public MyTextView(Context context) {
super(context);
init(context, null);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
void init(Context context, AttributeSet attrs){
mPaint = new Paint();
mRect = new RectF();
if(attrs != null){
//获取自定义属性(注意属性的命名方式)
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
mColor = a.getColor(R.styleable.MyTextView_rect_color, Color.BLUE);
mSize = a.getDimension(R.styleable.MyTextView_round_size, 5);
a.recycle();
}
}
@Override
protected void onDraw(Canvas canvas) {
//获取当前手机设备的像素密度
getDisplay().getMetrics(mMetric);
mDensity = mMetric.density;
//计算边框厚度值
float thickness = mThickness*mDensity;
//获取控件的宽高
int w = getWidth();
int h = getHeight();
//设置抗锯齿,否则锯齿会很明显
mPaint.setAntiAlias(true);
//绘制外圆角矩形
mPaint.setColor(mColor);
mRect.left = 0;
mRect.top = 0;
mRect.right = w;
mRect.bottom = h;
canvas.drawRoundRect(mRect, mSize* mDensity,mSize* mDensity,mPaint);
//绘制内圆角矩形(白色)
mPaint.setColor(Color.WHITE);
mRect.left = thickness;
mRect.top = thickness;
mRect.right = w-thickness;
mRect.bottom = h-thickness;
//计算圆角值
float r = (mSize-1>0 ? mSize-1 : 0)* mDensity;
canvas.drawRoundRect(mRect, r, r, mPaint);
//绘制文本文字,还走原来的逻辑
super.onDraw(canvas);
}
}
2.attrs文件定义了一个颜色属性、一个圆角值属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyTextView">
<attr name="rect_color" format="color"/>
<attr name="round_size" format="dimension"/>
</declare-styleable>
</resources>
3、在布局文件中引用自定义控件
<com.example.demo.view.MyTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:paddingLeft="2dp"
android:paddingRight="2dp"
app:rect_color="@android:color/holo_red_light"
app:round_size="2dp"
android:textSize="20sp"
android:text="Hello World!"/>
4.效果图
一个简单的自定义TextView的控件就实现了。
三. 直接继承View
直接继承View会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写onDraw外还需要对onMeasure方法进行重写。
我们用自定义View来绘制一个正方形
1.首先定义构造方法,以及做一些初始化操作
public class RectView extends View{
//定义画笔
private Paint mPaint = new Paint();
/**
* 实现构造方法
* @param context
*/
public RectView(Context context) {
super(context);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(Color.BLUE);
}
}
2.重写draw方法,绘制正方形,注意对padding属性进行设置
/**
* 重写draw方法
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取各个编剧的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//获取绘制的View的宽度
int width = getWidth()-paddingLeft-paddingRight;
//获取绘制的View的高度
int height = getHeight()-paddingTop-paddingBottom;
//绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);
}
之前我们讲到过View的measure过程,再看一下源码对这一步的处理
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
在View的源码当中并没有对AT_MOST和EXACTLY两个模式做出区分,也就是说View在wrap_content和match_parent两个模式下是完全相同的,都会是match_parent,显然这与我们平时用的View不同,所以我们要重写onMeasure方法。
3.重写onMeasure方法
/**
* 重写onMeasure方法
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//处理wrap_contentde情况
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 300);
}
}
整个自定义View的代码如下:
public class RectView extends View {
//定义画笔
private Paint mPaint = new Paint();
/**
* 实现构造方法
*
* @param context
*/
public RectView(Context context) {
super(context);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(Color.BLUE);
}
/**
* 重写onMeasure方法
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 300);
}
}
/**
* 重写draw方法
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取各个编剧的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//获取绘制的View的宽度
int width = getWidth() - paddingLeft - paddingRight;
//获取绘制的View的高度
int height = getHeight() - paddingTop - paddingBottom;
//绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
canvas.drawRect(0 + paddingLeft, 0 + paddingTop, width + paddingLeft, height + paddingTop, mPaint);
}
}
整个过程大致如下,直接继承View时需要有几点注意:
1、在onDraw当中对padding属性进行处理。
2、在onMeasure过程中对wrap_content属性进行处理。
3、至少要有一个构造方法。
四.继承ViewGroup
自定义ViewGroup的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子View的测量参数负责。
需求实例
实现一个类似于Viewpager的可左右滑动的布局。
代码比较多,我们结合注释分析。
public class HorizontaiView extends ViewGroup {
private int lastX;
private int lastY;
private int currentIndex = 0;
private int childWidth = 0;
private Scroller scroller;
private VelocityTracker tracker;
/**
* 1.创建View类,实现构造函数
* 实现构造方法
* @param context
*/
public HorizontaiView(Context context) {
super(context);
init(context);
}
public HorizontaiView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
scroller = new Scroller(context);
tracker = VelocityTracker.obtain();
}
/**
* 2、根据自定义View的绘制流程,重写`onMeasure`方法,注意对wrap_content的处理
* 重写onMeasure方法
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@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);
//测量所有子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//如果没有子View,则View大小为0,0
if (getChildCount() == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
int childHeight = childOne.getMeasuredHeight();
//View的宽度=单个子View宽度*子View个数,View的高度=子View高度
setMeasuredDimension(getChildCount() * childWidth, childHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
//View的宽度=单个子View宽度*子View个数,View的高度=xml当中设置的高度
setMeasuredDimension(getChildCount() * childWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childHeight = childOne.getMeasuredHeight();
//View的宽度=xml当中设置的宽度,View的高度=子View高度
setMeasuredDimension(widthSize, childHeight);
}
}
/**
* 3、接下来重写`onLayout`方法,对各个子View设置位置。
* 设置子View位置
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int left = 0;
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
childWidth = child.getMeasuredWidth();
child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
left += childWidth;
}
}
}
}
到这里我们的View布局就已经基本结束了。但是要实现Viewpager的效果,还需要添加对事件的处理。
/**
* 4、因为我们定义的是ViewGroup,从onInterceptTouchEvent开始。
* 重写onInterceptTouchEvent,对横向滑动事件进行拦截
* @param event
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercrpt = false;
//记录当前点击的坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int delatY = y - lastY;
//当X轴移动的绝对值大于Y轴移动的绝对值时,表示用户进行了横向滑动,对事件进行拦截
if (Math.abs(deltaX) > Math.abs(delatY)) {
intercrpt = true;
}
break;
}
lastX = x;
lastY = y;
//intercrpt = true表示对事件进行拦截
return intercrpt;
}
/**
* 5、当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。
* 重写onTouchEvent方法
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
tracker.addMovement(event);
//获取事件坐标(x,y)
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int delatY = y - lastY;
//scrollBy方法将对我们当前View的位置进行偏移
scrollBy(-deltaX, 0);
break;
//当产生ACTION_UP事件时,也就是我们抬起手指
case MotionEvent.ACTION_UP:
//getScrollX()为在X轴方向发生的便宜,childWidth * currentIndex表示当前View在滑动开始之前的X坐标
//distance存储的就是此次滑动的距离
int distance = getScrollX() - childWidth * currentIndex;
//当本次滑动距离>View宽度的1/2时,切换View
if (Math.abs(distance) > childWidth / 2) {
if (distance > 0) {
currentIndex++;
} else {
currentIndex--;
}
} else {
//获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点
tracker.computeCurrentVelocity(1000);
float xV = tracker.getXVelocity();
//当X轴加速度>50时,也就是产生了快速滑动,也会切换View
if (Math.abs(xV) > 50) {
if (xV < 0) {
currentIndex++;
} else {
currentIndex--;
}
}
}
//对currentIndex做出限制其范围为【0,getChildCount() - 1】
currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
//滑动到下一个View
smoothScrollTo(currentIndex * childWidth, 0);
tracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}
private void smoothScrollTo(int destX, int destY) {
//startScroll方法将产生一系列偏移量,从(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离
scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
//invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法
invalidate();
}
//重写computeScroll方法
@Override
public void computeScroll() {
super.computeScroll();
//当scroller.computeScrollOffset()=true时表示滑动没有结束
if (scroller.computeScrollOffset()) {
//调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//没有滑动结束,继续刷新View
postInvalidate();
}
}
这部分代码比较多,为了方便阅读,在代码当中进行了注释。
之后就是在XML代码当中引入自定义View
<com.example.demo.view.HorizontaiView
android:id="@+id/test_layout"
android:layout_width="match_parent"
android:layout_height="400dp">
<ListView
android:id="@+id/list1"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
<ListView
android:id="@+id/list2"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
<ListView
android:id="@+id/list3"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</com.example.demo.view.HorizontaiView>
总结
本篇文章对常用的自定义View的方式进行了总结,并简单分析了View的绘制流程。对各种实现方式写了简单的实现。
参考文档