一、实现效果
二、应用到的主要技术点
- android:animateLayoutChanges="true"设置这个属性有效。主要的作用是当childView 显示位置发生变化时,系统自动播放动画。
- 自定义ViewGroup要求我们自己实现onMeasure和onLayout控制childView的大小和显示位置。
- 重写onInterruptTouchEvent方法判断自定义ViewGroup是否需要截断事件,判断依据是是否有有效滑动距离产生。
- 重写onTouchEvent方法实现childViewd的拖拽显示(拖拽的是childView的截图)。
- 重写draw方法用于绘制被拖拽的childView截图。(注意:在自定义ViewGroup初始化阶段要调用setWillNotDraw(false);关闭系统的绘制优化,否则ViewGroup接到invalidate事件后,draw方法不会被调用)
- 调用childView.getDrawingCache()创建childView截图。
三、实现细节
1.通过重写onMeasure和onLayout方法控制childView显示大小和位置。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//由于需要支持RTL布局,所以开始布局的横坐标根据padding而不同。
int statLeft = Build.VERSION.SDK_INT < 17 ? getPaddingLeft() : getPaddingStart();
int starTop = getPaddingTop();
//布局第一个大图
if (childCount > 0) {
View childView = getChildAt(0);
LayoutParameter layoutParameter = (LayoutParameter) childView.getLayoutParams();
//view的宽度是等于容器的宽度减掉容器的padding,高度等于跨度
int childWidth = width - getPaddingLeft() - getPaddingRight();
childView.measure(MeasureSpec.makeMeasureSpec(childWidth, EXACTLY), MeasureSpec.makeMeasureSpec(childWidth, EXACTLY));
//在自定义的LayoutParameter中保存view显示的位置,在onLayout阶段可以直接使用这个数据进行布局。
layoutParameter.left = statLeft;
layoutParameter.top = starTop;
layoutParameter.right = layoutParameter.left + childView.getMeasuredWidth();
layoutParameter.bottom = layoutParameter.top + childView.getMeasuredHeight();
starTop = layoutParameter.bottom;
}
starTop += divideSpace;
//计算第二行小图的宽度,排除容器的padding和小图间的空隙进行4等分
int secondLineChildWidth = width - getPaddingLeft() - getPaddingRight() - secondLineLeftSpace - secondLineRightSpace - (secondLineDivideSpace * 3);
secondLineChildWidth /= 4;
bitmapRect.set(0, 0, secondLineChildWidth, secondLineChildWidth);
statLeft = Build.VERSION.SDK_INT >= 17 ? getPaddingStart() : getPaddingLeft();
statLeft += secondLineLeftSpace;
//RTL布局排列第二行的小图
if (Build.VERSION.SDK_INT >= 17 && getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
for (int i = childCount - 1; i > 0; i--) {
View childView = getChildAt(i);
LayoutParameter layoutParameter = (LayoutParameter) childView.getLayoutParams();
//小图的宽高也是相等的。
childView.measure(MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY), MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY));
layoutParameter.left = statLeft;
layoutParameter.top = starTop;
layoutParameter.right = layoutParameter.left + childView.getMeasuredWidth();
layoutParameter.bottom = layoutParameter.top + childView.getMeasuredHeight();
statLeft = layoutParameter.right;
statLeft += secondLineDivideSpace;
}
} else {
//LTR布局排列第二行
for (int i = 1; i < childCount; i++) {
View childView = getChildAt(i);
LayoutParameter layoutParameter = (LayoutParameter) childView.getLayoutParams();
childView.measure(MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY), MeasureSpec.makeMeasureSpec(secondLineChildWidth, EXACTLY));
layoutParameter.left = statLeft;
layoutParameter.top = starTop;
layoutParameter.right = layoutParameter.left + childView.getMeasuredWidth();
layoutParameter.bottom = layoutParameter.top + childView.getMeasuredHeight();
statLeft = layoutParameter.right;
statLeft += secondLineDivideSpace;
}
}
//设置容器的大小
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
View childView;
LayoutParameter layoutParameter;
for (int i = 0; i < childCount; i++) {
childView = getChildAt(i);
layoutParameter = (LayoutParameter) childView.getLayoutParams();
//根据onMeasure阶段保存的位置数据进行布局
childView.layout(layoutParameter.left, layoutParameter.top, layoutParameter.right, layoutParameter.bottom);
}
}
2.重写onInterruptTouchEvent和onTouchEvent方法实现childView的拖拽。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
startX = 0;
startY = 0;
int x = (int) ev.getX();
int y = (int) ev.getY();
//根据坐标点找到对应的childView
dragView = findViewByPosition(x, y);
Log.d(TAG, "onInterceptTouchEvent ACTION_DOWN dragView " + dragView);
if (dragView != null) {
Object object = dragView.getTag();
boolean canMove = false;
if (object != null && object instanceof Holder) {
canMove = ((Holder) object).canMove();
}
Log.d(TAG, "onInterceptTouchEvent ACTION_DOWN canMove " + canMove);
//如果找到的childView是可以移动的,那么保存当前的坐标。
if (canMove) {
Log.d(TAG, "onInterceptTouchEvent ACTION_DOWN find a view");
startX = x;
startY = y;
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
//其中有一个变量不为空,说明找到的childView是可移动的
if (startX != 0 || startY != 0) {
int x = (int) ev.getX();
int y = (int) ev.getY();
//如果是长按或是移动距离发生溢出,我们启动childView拖拽功能。
if (ev.getEventTime() - ev.getDownTime() >= ViewConfiguration.getLongPressTimeout() || Math.abs(startX - x) > touchSlop || Math.abs(startY - y) > touchSlop) {
Log.d(TAG, "onInterceptTouchEvent ACTION_MOVE start move");
touchMode = TouchMode.DRAG;
//不允许其他控件进行事件截断。
requestDisallowInterceptTouchEvent(true);
lastX = x;
lastY = y;
//启动拖拽功能。
buildDragBitmap(x, y);
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//重置拖拽参数、
resetDragBitmap();
break;
}
return touchMode == TouchMode.DRAG;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE: {
int x = (int) event.getX();
int y = (int) event.getY();
if (touchMode == TouchMode.DRAG && (Math.abs(x - lastX) > 5 || Math.abs(y - lastY) > 5)) {
Log.d(TAG, "onTouchEvent ACTION_MOVE ===========");
//移动childView的截图
moveDragBitmap(x - lastX, y - lastY);
lastX = x;
lastY = y;
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
touchMode = TouchMode.NORMAL;
//重置拖拽参数。
resetDragBitmap();
break;
}
return touchMode == TouchMode.DRAG;
}
3.在开始拖拽的时候通过调用getDrawingCache创建childView截图并播放动画。
private void buildDragBitmap(float x, float y) {
dragBitmap = null;
if (dragView == null) {
Log.d(TAG, "buildDragBitmap dragView == null!!!");
return;
}
Object obj = dragView.getTag();
if (obj != null && obj instanceof Holder) {
((Holder) obj).beforeHideView();
}
//销毁老的drawing cache
dragView.destroyDrawingCache();
dragView.setDrawingCacheEnabled(true);
//获取新的截图
Bitmap bitmap = dragView.getDrawingCache();
int offsetX = 0;
int offsetY = 0;
if (bitmap != null) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.setScale(bitmapRect.width() * 1.0f / width, bitmapRect.height() * 1.0f / height);
//根据要求的大小生成截图
dragBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
offsetX = (width - dragBitmap.getWidth()) / 2;
offsetY = (height - dragBitmap.getHeight()) / 2;
}
if (dragBitmap == null) {
Log.d(TAG, "buildDragBitmap dragBitmap == null!!!");
return;
}
if (obj != null && obj instanceof Holder) {
((Holder) obj).hideView(indexOfChild(dragView));
}
dragBitmapWidth = dragBitmap.getWidth();
dragBitmapHeight = dragBitmap.getHeight();
//因为截图大小发生了变化,所以需要计算截图显示位置。
dragBitmapX = dragView.getLeft() + offsetX;
dragBitmapY = dragView.getTop() + offsetY;
dragOffsetX = dragBitmapX;
dragOffsetY = dragBitmapY;
animationOffsetX = 0;
animationOffsetY = 0;
//播放动画显示childView到截图大小和位置的变化
startAnimation(dragView.getLeft() + ((dragView.getWidth() - dragBitmapWidth) / 2.0f), x - (dragBitmapWidth / 2.0f), dragView.getTop() + ((dragView.getHeight() - dragBitmapHeight) / 2.0f),
y - (dragBitmapHeight / 2.0f), dragView.getWidth() * 1.0f / dragBitmapWidth, 1.0f, null, startAnimationDuration);
}
4.接收ACTION_MOVE事件更新childView的截图显示位置,拖动过程中判断是否更改childView的顺序。
private void moveDragBitmap(float dx, float dy) {
//如果正在播放动画则不处理移动事件
if (animationIsRunning) {
animationOffsetX += dx;
animationOffsetY += dy;
return;
}
//更新childView截图显示问题
dragBitmapX += dx;
dragBitmapY += dy;
dragOffsetX += dx;
dragOffsetY += dy;
invalidate();
if (dragView == null) {
Log.d(TAG, "moveDragBitmap dragView == null");
return;
}
currentTime = System.currentTimeMillis();
//进行childView的重排序,判断是否重排序的时间间隔是300ms
if (currentTime - lastFindIntersectTime < 300) {
Log.d(TAG, "moveDragBitmap currentTime - lastFindIntersectTime < 300");
return;
}
lastFindIntersectTime = currentTime;
//找到截图下方重合的childView
View childView = findViewByPosition((int) dragOffsetX + (dragBitmapWidth / 2), (int) dragOffsetY + (dragBitmapHeight / 2));
if (childView == null) {
Log.d(TAG, "moveDragBitmap childView == null");
return;
}
//判断是否是上次获取的childView
if (intersectView != null && intersectView == childView) {
Log.d(TAG, "moveDragBitmap intersectView == childView");
return;
}
Object object = childView.getTag();
if (object != null && object instanceof Holder && !((Holder) object).canMove()) {
Log.d(TAG, "moveDragBitmap child can not move");
return;
}
intersectView = childView;
//获取相交childView的index
int intersectIndex = indexOfChild(intersectView);
//获取被拖拽的childView的index
int dragIndex = indexOfChild(dragView);
if (intersectIndex < 0 || dragIndex < 0 || intersectIndex == dragIndex) {
Log.d(TAG, "moveDragBitmap intersectIndex < 0 || dragIndex < 0 || intersectIndex == dragIndex");
return;
}
//进行childView排序,同时系统的layout change animation开始播放。
removeView(dragView);
addView(dragView, intersectIndex);
}
5.拖拽childView截图的描绘
private void drawDragBitmap(Canvas canvas) {
if (dragBitmap != null) {
canvas.save();
dragMatrix.reset();
//如果正在播放拖拽开始动画,要将位移动画的数值计算在内
if (animationIsRunning) {
if (animationRequestFinished) {
dragOffsetX += animationOffsetX;
dragOffsetY += animationOffsetY;
dragMatrix.setTranslate(dragOffsetX, dragOffsetY);
animationIsRunning = false;
animationRequestFinished = false;
animationOffsetX = 0;
animationOffsetY = 0;
} else {
dragMatrix.setTranslate(dragOffsetX + animationOffsetX, dragOffsetY + animationOffsetY);
}
} else {//没有播放动画时直接根据拖拽的位置描绘
dragMatrix.setTranslate(dragOffsetX, dragOffsetY);
}
//设置缩放比例
dragMatrix.preScale(dragScale, dragScale, dragBitmapWidth / 2.0f, dragBitmapHeight / 2.0f);
//应用变换矩阵并描绘截图
canvas.concat(dragMatrix);
canvas.drawBitmap(dragBitmap, 0, 0, paint);
canvas.restore();
}
}
6.拖拽结束播放结束动画
private void resetDragBitmap() {
if (dragView == null) {
return;
}
startAnimation(dragOffsetX, dragView.getLeft() + (dragView.getWidth() / 2.0f) - (dragBitmapWidth / 2.0f), dragOffsetY,
dragView.getTop() + (dragView.getHeight() / 2.0f) - (dragBitmapHeight / 2.0f), dragScale, dragView.getWidth() * 1.0f / dragBitmapWidth, new Runnable() {
@Override
public void run() {
if (dragView != null) {
Object obj = dragView.getTag();
if (obj != null && obj instanceof Holder) {
((Holder) obj).showView(indexOfChild(dragView));
}
}
dragBitmap = null;
dragView = null;
intersectView = null;
notifyAllHolderChange();
invalidate();
}
}, endAnimationDuration);
}
四、代码
https://github.com/mjlong123123/SortLayout
我的公众号已经开通,公众号会同步发布。
欢迎关注我的公众号