没有人不爱惜他的生命,但很少人珍视他的时间。 — 梁实秋
写在前面
在之前的的文章介绍了《View的绘制流程》,《View的measure流程》,《View的layout流程》,《View的draw流程》。本章就来实现一个继承自ViewGroup的布局容器,通过实例加深理解,并且会用到触摸事件进行滑动,如果不是很了解触摸事件,建议先看《触摸事件的处理流程》。本篇文章之所以没有实现继承自View的自定义控件,因为在《音乐相关自定义View》一篇中介绍了几个自定义控件,有直接继承自View的,也有继承自系统控件的,感兴趣的童鞋也可以去看下加深理解。
如何实现
我要写一个自定义布局容器HorizontalLayout,继承自ViewGroup,这是一个水平布局容器,支持左右滑动和快速滑动。
1.继承ViewGroup
public class HorizontalLayout extends ViewGroup {
public HorizontalLayout(Context context) {
super(context, null);
}
public HorizontalLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public HorizontalLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
从代码中可以看出继承自ViewGroup,实现构造函数,onLayout是ViewGroup的抽象方法,用来确定View的位置,构造函数和onLayout函数是必须要实现的。
2.实现LayoutParams
public class HorizontalLayout extends ViewGroup {
/**
* 以下三个函数需要重写
* 根据不同参数创建容器自身的布局参数对象并返回
*/
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (p instanceof LayoutParams) {
return new LayoutParams((LayoutParams) p);
} else if (p instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) p);
}
return new LayoutParams(p);
}
/**
* 添加容器自身的布局参数对象
* 继承自ViewGroup.MarginLayoutParams
*/
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
因为继承自ViewGroup的容器需要考虑子View的外边距,所以需要创建一个静态类LayoutParams并继承自ViewGroup.MarginLayoutParams,实现构造函数,这是必须要实现的。然后就要重写generateDefaultLayoutParams(),generateLayoutParams(attrs),generateLayoutParams(p)三个函数,返回值则是根据对应的参数创建容器自身的LayoutParams,后面会讲到怎么得到子View的外边距。
3.实现onMeasure()
public class HorizontalLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 计算出ViewGroup需要的宽和高
int maxWidth = 0;
int maxHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 子元素的宽需要考虑子元素的左右外边距
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
maxWidth += childWidth;
// 子元素的高需要考虑子元素的上下外边距
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
maxHeight = Math.max(maxHeight, childHeight);
}
// 容器的宽需要考虑容器左右内边距
maxWidth += (getPaddingLeft() + getPaddingRight());
// 容器的高需要考虑容器上下内边距
maxHeight += (getPaddingTop() + getPaddingBottom());
// 设置测量尺寸
setMeasuredDimension(computeMeasuredDimension(maxWidth, widthMeasureSpec),
computeMeasuredDimension(maxHeight, heightMeasureSpec));
// 测量子元素
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
/**
* 计算大小
*
* @param defaultSize
* @param measureSpec
* @return
*/
private int computeMeasuredDimension(int defaultSize, int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// AT_MOST对应wrap_content
case MeasureSpec.AT_MOST:
result = defaultSize;
break;
// EXACTLY对应match_parent
case MeasureSpec.EXACTLY:
result = specSize;
break;
// 未知模式
case MeasureSpec.UNSPECIFIED:
result = 0;
break;
default:
break;
}
return result;
}
}
先来看onMeasure()函数,该函数用来测量View大小,该函数中先遍历子View,只要子View不是GONE,会先拿到子View的LayoutParams,这个LayoutParams默认是ViewGroup.LayoutParams,它不能够得到子View的外边距信息,如果将它强转为ViewGroup.MarginLayoutParams则会抛出ClassCastException异常,但将它强转为容器自身的LayoutParams就不会抛出异常,因为容器自身的LayoutParams继承自ViewGroup.MarginLayoutParams,并重写了与LayoutParams相关三个函数,所以可以轻而易举的得到子View的外边距信息,这也再次证明了为什么要实现容器自身的LayoutParams,子View的外边距得到了,就可以计算出子View真正需要的宽高了,当子View全部遍历完,在将容器的内边距加上就可以了。然后调用setMeasuredDimension(measuredWidth, measuredHeight)设置测量尺寸就可以,最后调用measureChildren(widthMeasureSpec, heightMeasureSpec)测量子View。现在让我们将目光回到到computeMeasuredDimension(defaultSize, measureSpec)函数,该函数用来计算测量尺寸的,参数分别是默认大小和MesureSpec,函数内部会根据specMode返回相应的结果,比如specMode是AT_MOST,AT_MOST对应的是wrap_content包裹内容,那么就会将默认大小作为返回值。
4.实现onLayout
public class HorizontalLayout extends ViewGroup {
private int mFinalScrollX;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 需要考虑容器容器的左内边距
int left = getPaddingLeft();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 需要考虑子元素的左外边距
left += lp.leftMargin;
// 确定子元素的位置,需要考虑子元素的上下外边距
child.layout(left, getPaddingTop() + lp.topMargin,
left + child.getMeasuredWidth(),
getPaddingTop() + child.getMeasuredHeight() - lp.bottomMargin);
// 下一个子元素开始的位置,需要考虑子元素的右外边距
left += (child.getMeasuredWidth() + lp.rightMargin);
}
// 计算出水平方向上可以滑动的最大距离
mFinalScrollX = left - getMeasuredWidth();
}
}
onLayout()函数的作用是确定子View的位置,由于容器自身可能会设置内边距和子View可能会设置外边距,所以在确定子View的位置时需要将这两种因素考虑进来,否则容器自身的内边距和子View的外边距会失效。最终计算出水平方向上可以滑动的最大距离mFinalScrollX,之后会讲到它的用处。
5.实现滑动
public class HorizontalLayout extends ViewGroup {
private float mInterceptX;
private float mInterceptY;
private float mLastTouchMoveX;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mInterceptX = ev.getX();
mInterceptY = ev.getY();
// ACTION_DOWN事件没有被拦截,所以要在这记录一下
mLastTouchMoveX = ev.getX();
break;
case MotionEvent.ACTION_MOVE:
// 计算是否水平滑动,若水平滑动则拦截事件
float offsetX = ev.getX() - mInterceptX;
float offsetY = ev.getY() - mInterceptY;
if (Math.abs(offsetX) - Math.abs(offsetY) > 0) { // 1
intercept = true;
isIntercepted = true;
}
mInterceptX = ev.getX();
mInterceptY = ev.getY();
break;
default:
break;
}
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = (int) (event.getX() - mLastTouchMoveX);
int x;
// 向右滑动
if (offsetX > 0) {
// 如果向右滑动时,偏移量大于scrollX,则会超出View内容的左边,需要强制让它拉回来
x = Math.abs(offsetX) > getScrollX() ? getScrollX() : offsetX;
// 向左滑动
} else {
// 如果向左滑动时,偏移量与scrollX的和大于水平方向上可以滑动的最大距离,
// 则会超出View内容的右边,需要强制将它拉回来
x = Math.abs(offsetX) + getScrollX() > mFinalScrollX ? mFinalScrollX - getScrollX() : offsetX;
}
scrollBy(-x, 0);
mLastTouchMoveX = event.getX();
break;
default:
break;
}
return true;
}
}
众所周知,onInterceptTouchEvent()函数的作用是拦截事件,如果拦截则会调用容器自身的onTouchEvent()函数进行处理。
-
onInterceptTouchEvent()函数中在ACTION_DOWN事件会记下手指按下的位置,然后在ACTION_MOVE事件决定是否拦截当前触摸事件,注释1处则是决定是否拦截事件的关键代码,只要水平方向上的移动距离大于垂直方向上的移动距离就认为是水平滑动,则拦截该触摸事件,交由容器自身的onTouchEvent()函数处理。
-
onTouchEvent()函数中在ACTION_MOVE事件先计算出手指移动的偏移量,然后计算水平方向滑动的距离,为了防止超过View内容的左右边界,这里会处理边界值,然后调用scrollBy()函数进行滑动。
6.实现快速滑动
public class HorizontalLayout extends ViewGroup {
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private int mFinalScrollX;
private float mInterceptX;
private float mInterceptY;
private float mLastTouchMoveX;
private boolean isIntercepted;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 当手指按下时,滑动没完成需要打断
if (!mScroller.isFinished()) { // 1
mScroller.abortAnimation();
}
mInterceptX = ev.getX();
mInterceptY = ev.getY();
// ACTION_DOWN事件没有被拦截,所以要在这记录一下
mLastTouchMoveX = ev.getX();
isIntercepted = false;
break;
case MotionEvent.ACTION_MOVE:
// 计算是否水平滑动,若水平滑动则拦截事件
float offsetX = ev.getX() - mInterceptX;
float offsetY = ev.getY() - mInterceptY;
if (Math.abs(offsetX) - Math.abs(offsetY) > 0) {
intercept = true;
isIntercepted = true; // 2
}
mInterceptX = ev.getX();
mInterceptY = ev.getY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 如果手指移动时拦截了,抬起也要拦截,交由onTouchEvent()函数处理
if (isIntercepted) {
intercept = true;
isIntercepted = false;
}
break;
default:
break;
}
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int offsetX = (int) (event.getX() - mLastTouchMoveX);
int x;
if (offsetX > 0) {
x = Math.abs(offsetX) > getScrollX() ? getScrollX() : offsetX;
} else {
x = Math.abs(offsetX) + getScrollX() > mFinalScrollX ? mFinalScrollX - getScrollX() : offsetX;
}
scrollBy(-x, 0);
mLastTouchMoveX = event.getX();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 计算滑动速度
mVelocityTracker.computeCurrentVelocity(100, 100);
// 得到水平方向的滑动速度
float xVelocity = mVelocityTracker.getXVelocity();
// 如果滑动速度的绝对值大于50,就认为是快速滑动
if (Math.abs(xVelocity) > 50) {
int dx = (int) (xVelocity * 10);
// 向右滑动
if (xVelocity > 0) {
// 如果向右滑动时,增量大于scrollX,则会超出View内容的左边,需要强制让它拉回来
dx = Math.abs(dx) > getScrollX() ? getScrollX() : dx;
// 向左滑动
} else {
// 如果向左滑动时,增量与scrollX的和大于水平方向上可以滑动的最大距离,
// 则会超出View内容的右边,需要强制将它拉回来
dx = Math.abs(dx) + getScrollX() > mFinalScrollX ? mFinalScrollX - getScrollX() : dx;
}
mScroller.startScroll(getScrollX(), getScrollY(),
-dx, 0, 1000);
}
mVelocityTracker.clear();
break;
default:
break;
}
return true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 需要及时回收
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mVelocityTracker.recycle();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}
快速滑动是在滑动的基础上扩展的,当手指抬起时会计算滑动速度,一旦滑动速度大于某个值就判定为快速滑动,即便手指不在屏幕上也会在滑动一段距离后才停止。
-
onInterceptTouchEvent()函数中在ACTION_DOWN事件在注释1处增加了两行代码,如果手指再次按下时,上一次的快速滑动没结束则打断。在ACTION_MOVE事件注释2处增加了是否拦截的标志isIntercepted。在ACTION_UP事件会根据isIntercepted决定手指抬起事件是否拦截,如果isIntercepted为true则需要拦截抬起事件交由onTouchEvent()函数处理快速滑动。
-
onTouchEvent()函数在ACTION_UP事件会先计算滑动速度,如果水平方向的滑动速度大于50则认为是快速滑动,这里有一点需要注意,就是View内容的左右边界值,超过边界需要强制拉回来,然后调用Scroller的startScroll()函数开始滑动。
-
onDetachedFromWindow()函数需要重写做回收,释放等操作。
7.全部代码
public class HorizontalLayout extends ViewGroup {
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private int mFinalScrollX;
private float mInterceptX;
private float mInterceptY;
private float mLastTouchMoveX;
private boolean isIntercepted;
public HorizontalLayout(Context context) {
this(context, null);
}
public HorizontalLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HorizontalLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
mVelocityTracker = VelocityTracker.obtain();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 计算出ViewGroup需要的宽和高
int maxWidth = 0;
int maxHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 子元素的宽需要考虑子元素的左右外边距
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
maxWidth += childWidth;
// 子元素的高需要考虑子元素的上下外边距
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
maxHeight = Math.max(maxHeight, childHeight);
}
// 容器的宽需要考虑容器左右内边距
maxWidth += (getPaddingLeft() + getPaddingRight());
// 容器的高需要考虑容器上下内边距
maxHeight += (getPaddingTop() + getPaddingBottom());
// 设置测量尺寸
setMeasuredDimension(computeMeasuredDimension(maxWidth, widthMeasureSpec),
computeMeasuredDimension(maxHeight, heightMeasureSpec));
// 测量子元素
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
/**
* 计算容器大小
*
* @param defaultSize
* @param measureSpec
* @return
*/
private int computeMeasuredDimension(int defaultSize, int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.AT_MOST:
result = defaultSize;
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
result = 0;
break;
default:
break;
}
return result;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 需要考虑容器容器的左内边距
int left = getPaddingLeft();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 需要考虑子元素的左外边距
left += lp.leftMargin;
// 确定子元素的位置,需要考虑子元素的上下外边距
child.layout(left, getPaddingTop() + lp.topMargin,
left + child.getMeasuredWidth(),
getPaddingTop() + child.getMeasuredHeight() - lp.bottomMargin);
// 下一个子元素开始的位置,需要考虑子元素的右外边距
left += (child.getMeasuredWidth() + lp.rightMargin);
}
// 计算出水平方向上可以滑动的最大距离
mFinalScrollX = left - getMeasuredWidth();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 当手指按下时,滑动没完成需要打断
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mInterceptX = ev.getX();
mInterceptY = ev.getY();
// ACTION_DOWN事件没有被拦截,所以要在这记录一下
mLastTouchMoveX = ev.getX();
isIntercepted = false;
break;
case MotionEvent.ACTION_MOVE:
// 计算是否水平滑动,若水平滑动则拦截事件
float offsetX = ev.getX() - mInterceptX;
float offsetY = ev.getY() - mInterceptY;
if (Math.abs(offsetX) - Math.abs(offsetY) > 0) {
intercept = true;
isIntercepted = true;
}
mInterceptX = ev.getX();
mInterceptY = ev.getY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 如果手指移动时拦截了,抬起也要拦截
if (isIntercepted) {
intercept = true;
isIntercepted = false;
}
break;
default:
break;
}
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int offsetX = (int) (event.getX() - mLastTouchMoveX);
int x;
if (offsetX > 0) {
// 向右滑动
x = Math.abs(offsetX) > getScrollX() ? getScrollX() : offsetX;
} else {
// 向左滑动
x = Math.abs(offsetX) + getScrollX() > mFinalScrollX ? mFinalScrollX - getScrollX() : offsetX;
}
scrollBy(-x, 0);
mLastTouchMoveX = event.getX();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(100, 100);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) > 50) {
int dx = (int) (xVelocity * 10);
if (xVelocity > 0) {
// 向右滑动
dx = Math.abs(dx) > getScrollX() ? getScrollX() : dx;
} else {
// 向左滑动
dx = Math.abs(dx) + getScrollX() > mFinalScrollX ? mFinalScrollX - getScrollX() : dx;
}
mScroller.startScroll(getScrollX(), getScrollY(),
-dx, 0, 1000);
}
mVelocityTracker.clear();
break;
default:
break;
}
return true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 需要及时回收
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mVelocityTracker.recycle();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
/**
* 以下三个函数需要重写
* 根据不同参数创建容器自己的布局参数对象并返回
*/
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (p instanceof LayoutParams) {
return new LayoutParams((LayoutParams) p);
} else if (p instanceof MarginLayoutParams) {
return new LayoutParams((MarginLayoutParams) p);
}
return new LayoutParams(p);
}
/**
* 添加容器自己的布局参数对象
* 继承自ViewGroup.MarginLayoutParams
*/
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
以上就是HorizontalLayout的全部代码,其中有很多不足,仅作参考学习。
8.使用方式
<?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.chad.view.view.HorizontalLayout
android:background="@android:color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:background="@android:color/holo_green_dark"
android:text="ONE"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:background="@android:color/holo_red_dark"
android:layout_width="match_parent"
android:text="TWO"
android:layout_margin="20dp"
android:gravity="center"
android:layout_height="match_parent"/>
<TextView
android:background="@android:color/holo_blue_dark"
android:text="THREE"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:background="@android:color/holo_orange_dark"
android:text="FOUR"
android:layout_margin="40dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:background="@android:color/holo_green_light"
android:text="FIVE"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:background="@android:color/holo_red_light"
android:text="SIX"
android:layout_margin="60dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:background="@android:color/holo_blue_light"
android:text="SEVEN"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.chad.view.view.HorizontalLayout>
</RelativeLayout>
这里贴上一个xml文件,可以清晰的看到使用方式与LinearLayout等布局容器的使用方式相同,标签内部可以包含多个控件。
总结
本篇文章可以说是对前几篇关于View的文章做个总结,通过一个实例对View方面的知识加深理解。
PS:本人才疏学浅,若有不足请赐教!!!
网友评论