自定义ViewGroup,子View可对换位置

开始之前,先贴一个GIF。


现在就来实现上面的效果。

因为本篇的重点在于子View之间的位置变换,所以中间的矩形部分就只贴出代码,不做解释了,代码上也有注释。

一、该ViewGroup的构造函数

 public PositionLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);
        initScreenSize();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MeetingRoomView);
        //外矩形的位置  左上,右下
        mLeft = typedArray.getFloat(R.styleable.MeetingRoomView_left, mScreenWidth * 2 / 10);
        mTop = typedArray.getFloat(R.styleable.MeetingRoomView_top, mScreenHeight / 4);
        mRight = typedArray.getFloat(R.styleable.MeetingRoomView_right, mScreenWidth * 8 / 10);
        mBottom = typedArray.getFloat(R.styleable.MeetingRoomView_bottom, mScreenHeight * 3 / 5);
        //内外矩形的间距,默认为100
        mAlignInner = typedArray.getFloat(R.styleable.MeetingRoomView_align_inner_rect, 100);
        //是否需要显示圆角矩形
        mIsRoundRect = typedArray.getBoolean(R.styleable.MeetingRoomView_round_rect, false);
        //圆角矩形的半径,默认值为50,该值在mIsRoundRect为true时,设置才有效
        mRadius = typedArray.getFloat(R.styleable.MeetingRoomView_round_rect_radius, 50);
        //两个矩形的背景色
        mBgColor = typedArray.getColor(R.styleable.MeetingRoomView_background_color,
                mContext.getResources().getColor(R.color.colorPrimary));
        mInnerBgColor = typedArray.getColor(R.styleable.MeetingRoomView_inner_background_color,
                mContext.getResources().getColor(R.color.colorWhite));
        typedArray.recycle();
        initPaint();
    }

ViewDragHelper是一个用于拖动View时处理事件的帮助类,用法会在下面有涉及到。

private void initScreenSize() {//获取屏幕宽高
        mScreenWidth = ScreenSize.getScreenWidth(mContext);
        mScreenHeight = ScreenSize.getScreenHeight(mContext);
    }
private void initPaint() {
        //外矩形的画笔设置
        mOutPaint = new Paint();
        mOutPaint.setColor(mBgColor);
        mOutPaint.setAntiAlias(true);
//        mOutPaint.setStrokeWidth(20);//画笔宽度
        mOutPaint.setStyle(Paint.Style.FILL);//画笔空心  FILL 为实心
        //内矩形的画笔设置
        mInPaint = new Paint();
        mInPaint.setColor(mInnerBgColor);
        mInPaint.setAntiAlias(true);
//        mInPaint.setStrokeWidth(20);//画笔宽度
        mInPaint.setStyle(Paint.Style.FILL);//画笔空心  FILL 为实心
        //外矩形
        mOutRectF = new RectF(mLeft, mTop, mRight, mBottom);
        //内矩形
        mInRectF = new RectF(mLeft + mAlignInner, mTop + mAlignInner, mRight - mAlignInner, mBottom - mAlignInner);
        //ViewGroup  默认不调用onDraw函数,设置下面的方法让它调用
        this.setWillNotDraw(false);
    }
用于画矩形部分的画笔设置。
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mIsRoundRect) {//需要圆角矩形
            canvas.drawRoundRect(mOutRectF, mRadius, mRadius, mOutPaint);
            canvas.drawRoundRect(mInRectF, mRadius, mRadius, mInPaint);
        } else {
            canvas.drawRect(mOutRectF, mOutPaint);
            canvas.drawRect(mInRectF, mInPaint);
        }
    }
画矩形,这部分都是些比较基本的,就不详细讲了。

二、接下来就是ViewGroup每个子View的绘制与布局。

   画所有的子View只需要看两部分就可以了,左右两个单个的View为一部分,上下两排子View为一部分。

1、左右单个的部分,先看一张位置的分析图


cl,ct,cr,cb 对应的是这个View的左上右下的点位,上面的图都能清晰地解释每个点的位置。

其中有个20的值,是为了让子View跟矩形保持一定的距离,不让整个布局显得很拥挤,当然,这个值可以根据自己的看的最舒服的大小来设置的。


2、上下两排View的布局方式,同样看一下分析图。



其实只需要看一排的位置设置就能相应推出另一排的位置。

在这里,设置好每排只放四个View,然后根据这四个View来平均分配矩形的宽就可以了。

先将矩形的宽分成八等分,因为只显示四个,所以每个View的中心的X坐标分别就是 1/8,3/8,5/8,7/8。

分好之后就可以计算出每个View的cl,跟cr了。

计算公式看上面的分析图就可以很清楚地理解了。


每个子View的位置确定,其实在分析图中应该说明地很清晰了,所以没有再进行更详细的解释,如果还有不懂的可以留个言,我会再进行解释的。

再把布局的代码贴出来:

//作为用户位置占矩形宽度的 ?/?  的标识,每次自增 2
    private int up = 1;
    private int down = 1;

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (!changed)
            return;
        int childCount = getChildCount();
        float widthAll = mRight - mLeft;//外矩形的宽
        float heightAll = mBottom - mTop;//外矩形的高
        for (int i = 0; i < childCount; i++) {//遍历子View,进行位置的确认
            int measuredWidth = getChildAt(i).getMeasuredWidth();
            int measuredHeight = getChildAt(i).getMeasuredHeight();
            //子View的位置,cl(左),ct(上),cr(右),cb(下)
            int cl, ct, cr, cb;
            if (i == 0) {
                cl = (int) (mLeft - measuredWidth - 20);
                ct = (int) (mBottom - heightAll / 2 - measuredHeight / 2);
                cr = (int) (mLeft - 20);
                cb = (int) (mBottom - heightAll / 2 + measuredHeight / 2);
                getChildAt(i).layout(cl, ct, cr, cb);
                //将位置与对应的View绑定
                mChildViewRect = new ChildViewRect(cl, ct, cr, cb);
                childViewMap.put(mChildViewRect, getChildAt(i));
            } else if (i == 9) {
                cl = (int) mRight + 20;
                ct = (int) (mBottom - heightAll / 2 - measuredHeight / 2);
                cr = (int) mRight + 20 + measuredWidth;
                cb = (int) (mBottom - heightAll / 2 + measuredHeight / 2);
                getChildAt(i).layout(cl, ct, cr, cb);
                //将位置与对应的View绑定
                mChildViewRect = new ChildViewRect(cl, ct, cr, cb);
                childViewMap.put(mChildViewRect, getChildAt(i));
            } else {
                if (i % 2 == 0) {

                    cl = (int) (widthAll * down / 8 - measuredWidth / 2);
                    ct = (int) (mBottom + 20);
                    cr = (int) (widthAll * down / 8 + measuredWidth / 2);
                    cb = (int) (mBottom + measuredHeight + 20);

                    down += 2;
                } else {

                    cl = (int) (widthAll * up / 8 - measuredWidth / 2);
                    ct = (int) (mTop - measuredHeight - 20);
                    cr = (int) (widthAll * up / 8 + measuredWidth / 2);
                    cb = (int) (mTop - 20);
                    up += 2;

                }
                getChildAt(i).layout(cl + (int) mLeft, ct, cr + (int) mLeft, cb);
                //将位置与对应的View绑定
                //这里的左跟右的位置一定要加上 mLeft
                //在做的过程中就是因为没加,花了很多时间排查位置对换的错乱问题
                mChildViewRect = new ChildViewRect(cl + (int) mLeft, ct, cr + (int) mLeft, cb);
                childViewMap.put(mChildViewRect, getChildAt(i));
            }
        }
    }

这就是子View位置的确定与绘制,其中像下面的代码:

//将位置与对应的View绑定
mChildViewRect = new ChildViewRect(cl, ct, cr, cb);
childViewMap.put(mChildViewRect, getChildAt(i));
这部分可以先不看,这是为了切换子View位置需要用到的一些变量。

在这个自定义ViewGroup中,只让它显示10个View,所以布局的时候可以判断当前是第几个View(比如:i==0,i==9,分别是第一个和最后一个)来进行位置确定。

布局确定之后,就可以为当前类添加子View的方法:

/**
     * 为ViewGroup添加子View
     * @param users                 需要添加到ViewGroup的所有View的模型集合,
     *                              UserModel只有viewName,viewRole两个字段,
     *                              一个保存名字,一个作为每个View显示哪张图片的标识。
     * @param currentName           设置哪个名字的子View需要显示明显一点。
     * @param onUserClickListener   每个子View的点击回调接口。
     */
    public void addUsers(List<UserModel> users, String currentName, final OnUserClickListener onUserClickListener) {
        this.removeAllViews();
        for (int i = 0; i < users.size(); i++) {
            mName = (TextView) View.inflate(this.getContext(), R.layout.text_view, null);
            users.get(i).setPicForUser(mContext, mName);//为每个View设置相对应的图片
            String userName = users.get(i).getViewName();
            mName.setText(userName);
            final int finalI = i;
            mName.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onUserClickListener != null)
                        onUserClickListener.onUserClick(v, finalI);
                }
            });
            if (userName.equals(currentName)) {
                users.get(i).setCurrentDelegate(mContext, mName);//根据传入的名字,让该名字对应的View显示高亮
            }
            this.addView(mName);
        }
        //通知ViewGroup绘制
        postInvalidate();
    }

到这里,就能让所有的子View显示出来了。


三、子View的切换及位置变换

1、首先应该是怎么让每个子View能够拖动,在上面有提到一个帮助类的ViewDragHelper,这个帮助类是在V4包下的,有了这个帮助类,就能帮开发者省去拖动View的那些处理逻辑。

在实例化ViewDragHelper之后,需要重写ViewGroup的onInterceptTouchEvent和onTouchEvent方法,让ViewDragHelper来处理。

实例化操作在构造函数中已经实现了
mViewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

主要的处理逻辑在ViewDragHelper.callback中。在看代码之前,先说一下我关于切换子View位置的思路。

1、首先,要让两个View的位置进行变换,得知道这两个View的具体位置(也就是View的左上右下),然后再让这个位置与在这个位置上的View进行一一绑定起来。

开始拖动View,如果释放这个View的时候,拖动的View有覆盖另一个View的情况出现,就让这两个View的位置信息相互交换,让拖动的View刚开始的位置与被覆盖的View绑定,被覆盖的View的位置与拖动的View绑定,这样就完成了两个View的位置变换。

但是没有一个类是能存View的位置的,所以得先定义一个类,用来保存View的位置,创建类ChildViewRect

public class ChildViewRect {
    private int left;
    private int top;
    private int right;
    private int bottom;
还有一些构造函数,setter和getter方法,就不贴出来了。
还记得在上面的onLayout方法中有一些代码像:
//将位置与对应的View绑定
mChildViewRect = new ChildViewRect(cl, ct, cr, cb);
childViewMap.put(mChildViewRect, getChildAt(i));
这个就是用来将View的位置与View绑定的操作。

childViewMap就是保存一一绑定的集合,其实就是一个HashMap,位置类作为键,View作为值。

//保存所有的View,绑定位置与该位置对应的View
private Map<ChildViewRect, View> childViewMap = new HashMap<>();
这样,在布局完成的同时,就已经将每个View的位置与View都一一绑定起来了。

代码时间:

private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
        //手指拖动的那个View 的坐标位置
        private ChildViewRect mMoveViewRect;

        /**
         * 手指按住的那个View
         * @param child         View
         * @param pointerId
         * @return
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            mMoveViewRect = new ChildViewRect(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
            Set<ChildViewRect> childViewRects = childViewMap.keySet();
            Iterator<ChildViewRect> iterator = childViewRects.iterator();
            while (iterator.hasNext()) {
                ChildViewRect childViewRect = iterator.next();
                if (childViewRect.getLeft() == mMoveViewRect.getLeft()
                        && childViewRect.getBottom() == mMoveViewRect.getBottom()
                        && childViewRect.getRight() == mMoveViewRect.getRight()
                        && childViewRect.getTop() == mMoveViewRect.getTop()) {
                    //拖动时先把View从childViewMap中移除掉,没有替换的话会在还原位置的时候重新添加到childViewMap
                    childViewMap.remove(childViewRect);
                    break;
                }
            }
            return true;
        }
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //不让控件拖出屏幕,横向
            final int leftBound = getPaddingLeft();
            final int rightBound = getWidth() - mName.getWidth() - leftBound;
            final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
            return newLeft;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            //不让控件拖出屏幕,纵向
            final int topBound = getPaddingTop();
            final int bottomBound = getHeight() - mName.getHeight() - topBound;
            final int newTop = Math.min(Math.max(top, topBound), bottomBound);
            return newTop;
        }

        //手指释放的时候回调
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            int top = releasedChild.getTop();
            int left = releasedChild.getLeft();
            int right = releasedChild.getRight();
            int bottom = releasedChild.getBottom();
            Set<ChildViewRect> childViewRects = childViewMap.keySet();//获取所有的键
            Iterator<ChildViewRect> iterator = childViewRects.iterator();
            //是否有View被覆盖
            boolean isReplace = false;
            View beReplaceView = null;
            ChildViewRect beReplaceChildViewRect = null;

            while (iterator.hasNext()) {//循环遍历键,取得每个位置类,跟拖动的View的当前位置进行对比,是否有覆盖
                ChildViewRect childViewRect = iterator.next();
                boolean temp1 = bottom >= childViewRect.getTop();
                boolean temp2 = top <= childViewRect.getBottom();
                boolean temp3 = right >= childViewRect.getLeft();
                boolean temp4 = left <= childViewRect.getRight();
                //如果拖动的那个View覆盖在其中一个子View上
                if (temp1 && temp2 && temp3 && temp4) {
                    beReplaceView = childViewMap.get(childViewRect);
                    beReplaceChildViewRect = childViewRect;
                    //将这个被替换的位置移除出childViewMap
                    childViewMap.remove(childViewRect);
                    //然后将被拖动的View的位置与被覆盖View绑定,让childViewMap的对应关系跟View的个数一致
                    childViewMap.put(mMoveViewRect, beReplaceView);
                    isReplace = true;
                    break;
                }
            }
            if (isReplace) {//交换两个View的位置
                beReplaceView.layout(mMoveViewRect.getLeft(), mMoveViewRect.getTop(), mMoveViewRect.getRight(), mMoveViewRect.getBottom());
                releasedChild.layout(beReplaceChildViewRect.getLeft(), beReplaceChildViewRect.getTop(),
                        beReplaceChildViewRect.getRight(), beReplaceChildViewRect.getBottom());
                //将被覆盖的View的位置与拖动的View绑定
                childViewMap.put(beReplaceChildViewRect, releasedChild);
            } else {
                //如果拖动的View释放后,没有覆盖任何一个View,则返回到之前的位置,并且从新绑定位置
                releasedChild.layout(mMoveViewRect.getLeft(), mMoveViewRect.getTop(), mMoveViewRect.getRight(), mMoveViewRect.getBottom());
                childViewMap.put(mMoveViewRect, releasedChild);
            }
        }
        //在边界拖动时回调
        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            return getMeasuredWidth() - child.getMeasuredWidth();
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            return getMeasuredHeight() - child.getMeasuredHeight();
        }

        //拖动View的同时,只要坐标一直在变化,就会一直调用这个方法
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }
    };
这里我们需要重点关注 tryCaptureView 和   onViewReleased  这两个方法。

一个是按住View开始拖动时就会调用,一个是释放拖动的View时会调用。

在拖动开始时,把正在拖动的View位置跟View的绑定关系先解除掉,在释放View时如果有其他View被覆盖了,就让拖动的View的起始位置与被覆盖的View绑定,而被覆盖的View的位置与拖动的View绑定,如果释放时并没有其他的View被覆盖,则重新将拖动的View的起始位置跟拖动的View再重新绑定。保持childViewMap的大小跟View的个数一致。

那么,还需要判断什么情况下,一个View会是覆盖另一个View,其实也很简单,来看看分析图:


被拖动的View的下边界 >= 被覆盖的View的上边界,拖动的View的上边界 <= 被覆盖的View的下边界,

被拖动的View的右边界 >= 被覆盖的View的左边界,拖动的View的左边界 <= 被覆盖的View的右边界,

只要这四种情况同时成立,那么就证明一个View已经覆盖另一个View了。


以上,子View可以自由切换位置的自定义ViewGroup就完成了,接下来就是使用这个控件了。

四、使用该自定义ViewGroup

在MAinActivity的布局文件中直接引用就行了。

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="xiedroid.com.dragviewgroup.MainActivity">

    <xiedroid.com.dragviewgroup.PositionLayout
        android:id="@+id/id_room"
        app:round_rect="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </xiedroid.com.dragviewgroup.PositionLayout>
</RelativeLayout>

app:round_rect="true"  让矩形显示为圆角矩形


MainActivity.class

public class MainActivity extends AppCompatActivity {
    private PositionLayout mPositionLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final List<UserModel> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            UserModel userModel = new UserModel();
            userModel.setViewName("View" + i);
            if (i % 2 == 0) {
                userModel.setViewRole(1);
            } else {
                userModel.setViewRole(2);
            }
            list.add(userModel);
        }

        mPositionLayout = (PositionLayout) findViewById(R.id.id_room);
        mPositionLayout.addUsers(list, "View5", new OnUserClickListener() {
            @Override
            public void onUserClick(View v, int position) {//每个子View的点击事件
                Toast.makeText(MainActivity.this, ""+list.get(position).getViewName(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}
其他一些图片的就不贴出来了。

运行程序,就能显示如最开始那样的效果了。

点击源码下载链接

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值