自定义ViewGroup实现左滑效果

时间:2023-02-09 14:53:24

相信很多人见过也写过这样的控件,我也参照网上的例子,自己模仿着写了一个,主要的目的是为了梳理下自定义ViewGroup的方法跟流程。在这里,做个记录,也提供给大家做个了解,如果有写的不好的地方,希望能够及时给我指正。

效果图,这里我就不贴了,就是大家常见的那种左滑的效果。但是,我这里,并没有把左滑放在列表里面,因为我在列表里面,触摸其他地方,我还不知道怎么把之前的那个左滑的View给关闭。当然,网上有比较好的方案,看了好几个,不是我想要的,所以,如果大家有好的思路,可以自己去实现下,我这里就不做任何的讲解,我实在是还没有想到一个好的思路。所以,这里只是说明一下,怎样构造一个可以左滑的ViewGroup。

大家都知道,自定义ViewGroup的流程,主要就是onMeasure()和onLayout()两个方法以及事件处理这些方法,这里,onDraw()方法没有用到,所以就 不多说了。

OK,那就先从onMeasure()开始。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i("SwipeLayout","onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setClickable(true);

boolean measureMatchParent=MeasureSpec.getMode(widthMeasureSpec)!=MeasureSpec.EXACTLY
||MeasureSpec.getMode(heightMeasureSpec)!=MeasureSpec.EXACTLY;
int maxWidth=0,maxHeight=0;
for (int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()!=GONE){
measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0);
MarginLayoutParams layoutParams= (MarginLayoutParams) child.getLayoutParams();
//拿到最大宽度跟最大高度,来决定父控件的宽高
maxWidth=Math.max(maxWidth,layoutParams.leftMargin+
child.getMeasuredWidth()+layoutParams.rightMargin);
maxHeight=Math.max(maxHeight,layoutParams.topMargin+
child.getMeasuredHeight()+layoutParams.bottomMargin);

//如果父控件是wrap_content的情况下,这个时候,子控件如果是match_parent,
// 那么需要重新计算下子控件的宽高
if(measureMatchParent) {
if (layoutParams.width==MeasureSpec.EXACTLY ||
layoutParams.height==MeasureSpec.EXACTLY){
//这里先加入到一个集合中,下面计算
mChildMatchParents.add(child);
}
}
}
}
//考虑下背景的宽高
maxHeight=Math.max(maxHeight,getSuggestedMinimumHeight());
maxWidth=Math.max(maxWidth,getSuggestedMinimumWidth());

setMeasuredDimension(resolveSizeAndState(maxWidth,widthMeasureSpec,0)
,resolveSizeAndState(maxHeight,heightMeasureSpec,0));


for (int i=0;i<mChildMatchParents.size();i++){
View child=mChildMatchParents.get(i);
int childWidthSpec;
MarginLayoutParams layoutParams= (MarginLayoutParams) child.getLayoutParams();
if(layoutParams.width== LayoutParams.MATCH_PARENT){
int width=Math.max(0,getMeasuredWidth()-
layoutParams.leftMargin-layoutParams.rightMargin);
childWidthSpec=MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);
}else{
childWidthSpec=MeasureSpec.makeMeasureSpec(layoutParams.leftMargin+
layoutParams.width+layoutParams.rightMargin,MeasureSpec.EXACTLY);
}

int childHeightSpec;
if(layoutParams.height==LayoutParams.MATCH_PARENT){
int height=Math.max(0,getMeasuredHeight()-
layoutParams.topMargin-layoutParams.bottomMargin);
childHeightSpec=MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
}else{
childHeightSpec=MeasureSpec.makeMeasureSpec(layoutParams.topMargin+
layoutParams.height+layoutParams.bottomMargin,MeasureSpec.EXACTLY);
}
child.measure(childWidthSpec,childHeightSpec);
}
}
这里是onMeasure的过程。代码里面写了一些注释。这里再详细做下说明。

boolean measureMatchParent=MeasureSpec.getMode(widthMeasureSpec)!=MeasureSpec.EXACTLY
||MeasureSpec.getMode(heightMeasureSpec)!=MeasureSpec.EXACTLY;

先来说下这一段代码,这里就是说,如果父控件的宽度或者高度是wrap_content 并且子View 是match_parent的情况下,我们需要对这些子View进行重新测量,当然了,有人可能会问父控件都是wrap_content了,怎么再对这些match_parent的子View进行测量呢?这里我的做法是对那些能够测量出来的子View,取它们的最大宽度跟最大高度给到父控件,这样,就直接把父控件的宽高定好了。然后那些match_parent的子View是不是就能够拿到宽高了呢。曾今我这里测量的时候有个疑问,就是子View是wrap_content的话是怎么测量的,因为 widthMeasureSpec跟 heightMeasureSpec 是用来测量父控件的。我这里用到的是measureChildWidthMargins 这个方法,我们就从这个方法入手,看看源码是怎么来解决这样的事情的。

大家都知道View的宽高有3中模式EXACTLY、AT_MOST、UNSPECFIED。

EXACTLY 表示的是match_parent或者是固定宽高。

AT_MOST 表示的是wrap_content

UNSPECFIED 表示的是未指定大小,就是子View想要多大就给多大了,这种情况很少用到,反正我是没有用过这个。

OK,了解了这个,我们直接看源码吧。

protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

这个是measureChildMargins方法,看到里面调用了getChildMeasureSpec这个方法,我们继续看。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这里的代码是重点,主要解决的问题是测量子View的问题。我们可以看到里面的switch语句主要做的事情是这样的,这里是父控件的MeasureSpec,分为三种模式,就是我们刚刚讲过的那三种。

EXACTYL 这种情况下,由于用的是MarginLayoutParams,所以我们可以轻松的拿到子View的宽高是match_parent还是wrap_content或者是固定的大小。然后就是知道子View的MeasureSpec 是什么了。这里我们可以看到是没有UNSPECFIED这种模式的。

WRAP_CONTENT 跟上面的差不多。就是固有的一些逻辑判断,大家通过代码应该也能看出来,就不多说了

UNSPECFIED 这个也不用多说了,就是一些正常的逻辑判断,相信大家也能够看得懂。

这样就可以拿到子View的MeasureSpec了。我们在回到measureChildWidthMargins这个方法,它最后调用了child.measure方法,用来测量的。这样就完成了整个的测量过程。

还有一点需要注意的是,我们这里用到的是MarginLayoutParams,我们需要重写一个方法,如下:

 @Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
不重写的话,会报一个layoutparams转换错误。这里就为大家揭秘下,这个方法是具体是干嘛的。

大家应该都知道LayoutInflate.inflate(),这个方法,干嘛用的呢,是用来解析布局文件的,我们会在加载一个布局的时候用到。那么我告诉你,系统在解析你的布局文件的时候也是通过这个方法。这个方法里面的代码还算多的,我贴一段主要的代码。

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
看到没有,这里会通过generateLayoutParams这个方法拿到它的layoutparams,所以我们通过重写这个方面就可以将layoutparams变成MarginLayoutParams了,就是使用margin相关的东西。这里主要是为了适配能够在这个左滑的ViewGroup里面能够写margin。

是不是很明了。ok, 继续啊,到了onLayout()。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentView=getChildAt(0);
mRightView=getChildAt(1);
MarginLayoutParams cParams=null;
if(mContentView!=null){
cParams= (MarginLayoutParams) mContentView.getLayoutParams();
int cl=l+cParams.leftMargin;
int ct=t+cParams.topMargin;
int cr=cl+mContentView.getMeasuredWidth();
int cb=ct+mContentView.getMeasuredHeight();
mContentView.layout(cl,ct,cr,cb);
}
if(mRightView!=null){
MarginLayoutParams rParams= (MarginLayoutParams) mRightView.getLayoutParams();
int rl=mContentView.getRight()+cParams.rightMargin+rParams.leftMargin;
int rt=t+rParams.topMargin;
int rr=rl+mRightView.getMeasuredWidth();
int rb=rt+mRightView.getMeasuredHeight();
mRightView.layout(rl,rt,rr,rb);
}
}
这里我为了简单起见,就直接默认写死了两个子View。这里需要注意下。 测量好了,摆放就很简单了,就是摆放在自己想要的地方就好了,没啥说的。

然后就是事件处理了,我这里重写了dispatchOnTouchEvent这个方法,当然也可以是onTouchEvent。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
lastPoint.set(ev.getRawX(),ev.getRawY());
firstPoint.set(ev.getRawX(),ev.getRawY());
break;
case MotionEvent.ACTION_MOVE:
float delx=ev.getRawX()-lastPoint.x;
float dely=ev.getRawY()-lastPoint.y;
if(Math.abs(delx)>Math.abs(dely) && Math.abs(delx)>mTouchSlop){//
scrollBy(-(int) delx,0);
if(getScrollX()>=0){
if(getScrollX()>=mRightView.getMeasuredWidth()){
scrollTo(mRightView.getMeasuredWidth(),0);
}
}else{
if(getScrollX()<mContentView.getLeft()){
scrollTo(0,0);
}
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
float smoothX=ev.getRawX()-firstPoint.x-mTouchSlop;
if(smoothX>=0 && getScrollX()>mContentView.getLeft()){
smoothClose();
}else if(smoothX<0 && getScrollX()<mRightView.getMeasuredWidth()){
smoothExpand();
}
break;
}
lastPoint.set(ev.getRawX(),ev.getRawY());
return super.dispatchTouchEvent(ev);
}
这里呢其实,就是一些逻辑判断,简单说下展开跟关闭两个动画。其实这里可以用scroller来写。看个人爱好了。

private ValueAnimator mExpandAnim,mCloseAnim;
public void smoothExpand(){

if(mExpandAnim==null){
mExpandAnim=ValueAnimator.ofInt(getScrollX(),mRightView.getMeasuredWidth());
}
//每次动画之前先取消所有的动画
cancelAnim();
mExpandAnim.setInterpolator(new LinearInterpolator());
mExpandAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value= (int) animation.getAnimatedValue();
scrollTo(value,0);
}
});
mExpandAnim.setDuration(500);
mExpandAnim.start();
}
public void smoothClose(){

if(mCloseAnim==null){
mCloseAnim=ValueAnimator.ofInt(getScrollX(),0);
}
cancelAnim();
mCloseAnim.setInterpolator(new LinearInterpolator());
mCloseAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value= (int) animation.getAnimatedValue();
Log.i("SwipeLayout","value="+value);
scrollTo(value,0);
}
});
mCloseAnim.setDuration(500);
mCloseAnim.start();
}
private void cancelAnim(){
if(mExpandAnim!=null){
mExpandAnim.cancel();
}
if(mCloseAnim!=null){
mCloseAnim.cancel();
}
}
其实就是,看你的ACTION_UP跟ACTION_CANCEL在什么时候触发,来控制动画的距离。

此次分析就是以上,希望能够帮到大家。


Thanks:

点击打开链接