前言部分
本文基本实现电影选座的效果,参考的是猫眼的效果来进行编写。2019年开年第一个月这篇可能是这个月的最后一篇了,希望今年继续做到坚持每月写博客的习惯,虽然博客的质量还不行,这主要还是因为能力上差的还多,但是不能轻易放弃,毕竟不能人人都是大神,博客能帮到别人或者帮到自己就有它的价值了。2019希望各位通过努力遇见更好的自己。
-
下面效果图先来镇楼吧
-
添加了缩略图,上个新图
内容部分
实现步骤
- 常规的自定义View部分,这里需要做的是把电影的选座的内容都绘制到画布上,包括但不限于以下:影院屏幕、行坐标、座位矩阵等。(这里我说通过定义view来实现,其实我感觉通过定义ViewGroup来实现应该效果回更好,但是目前还没有尝试。)
- 处理手势操作的部分,这部分的重点是GestureDetector和它的派生类。要注意变化后的画布坐标都会变化,还要处理一些滑动缩放的冲突
- Matrix的使用,通过矩阵来实现我们需要的效果(平移和缩放,其实还有错切、旋转等);这里数学要求多一些,也是我很不擅长的部分花费了大量的时间。
- 这里主要是做一些边界限制,主要的难点是由于变化Matrix导致坐标的变化,在比较的时候需要来回的转换。这里也很头大,并且我写的限制边界不是很流畅,会有比较明显的卡顿现象,这里我觉得可以加动画来修正或者添加一些其他的限制条件。
具体操作流程
主要介绍一下关键点的代码的原理,具体的完整代码已上传到github在文末点击进去看一下就可以了。
-
就从onMeasure方法开始吧,主要还是针对测量规则来进行不同的操作,这里不介绍测量部分,主要是绘制部分占绝大部份内容,贴一下代码:
//宽和高都是AT_MOST,则设置宽度所有子元素的宽度的和;高度设置为第一个元素的高度; setMeasuredDimension(measureWidth(widthMeasureSpec), measureWidth(heightMeasureSpec)); /** * 宽度计算 * * @param widthMeasureSpec * @return */ private int measureWidth(int widthMeasureSpec) { int result = 0; int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec); if (specMode == MeasureSpec.EXACTLY) { result = widthMeasureSpec; } else { result = 500; if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } } return result; }
大家的代码多数都这个样子吧,主要就是是否是精确尺寸,不是的话就给你个磨人的大小就可以。
-
多数代码在这个绘制onDraw(Canvas canvas)方法中,这里我逐一介绍一下分为三个部分 座位区域、行数区域、电影院屏幕区域,其他也可以绘制,如猫眼在整体的下面绘制了猫眼电影这几个字。
-
绘制电影座位的区域,这里主要就是通过外部传入的一个二位数组来绘制一个几行几列的矩形区域,需要注意处理的地方如:没有座位的区域,如果没有座位其实我也是把位置绘制出来了,不过使用的透明的画笔;显示列号的时候要注意去掉没有座位的位置数;点击事件需要整体刷新view所以选择还是取消选择都在这里进行处理了。
private void drawSeatView(Canvas canvas) { seatRect = new Rect(); seatRect.left = seatWidth + seatWidth / 2 + margiHorizontal; seatRect.top = marginTopScreen; //绘制多少排座位 for (int i = 0; i < seatList.length; i++) { int startY; if (i == 0) { startY = marginTopScreen; } else { startY = i * seatWidth + marginTopScreen; } int emptyCount = 0; //每排多少座位 for (int x = 0; x < seatList[i].length; x++) { int left; //开始绘制矩阵图 if (x == 0) { left = seatWidth + seatWidth / 2; } else { left = (x + 1) * seatWidth + seatWidth / 2; } int top = startY - seatWidth / 2 - margiVertical; seatRect.right = left + seatWidth; seatRect.bottom = top + seatWidth; // LogUtil.i(left + "--" + top + "--" + (left + seatWidth) + "--" + (top + seatWidth)); int seatState = seatList[i][x]; SelectRectBean selectRectBean = new SelectRectBean(); Rect rect = new Rect(left + margiHorizontal, top + margiVertical, left + seatWidth, top + seatWidth); selectRectBean.setRect(rect); selectRectBean.setSeatState(seatState); //需要计算从中间开始绘制座位 switch (seatState) { case EMPTY_SEAT: emptyCount++; paintSeat.setColor(Color.TRANSPARENT); canvas.drawRect(rect, paintSeat); break; case NORMAL_SEAT: paintSeat.setColor(Color.WHITE); selectRectBean.setColumn(x + 1 - emptyCount); selectRectBean.setRow(i + 1); canvas.drawRect(rect, paintSeat); break; case SELL_SEAT: paintSeat.setColor(Color.RED); canvas.drawRect(rect, paintSeat); break; case SELECT_SEAT: paintSeat.setColor(Color.GREEN); selectRectBean.setColumn(x + 1 - emptyCount); selectRectBean.setRow(i + 1); canvas.drawRect(rect, paintSeat); break; default: paintSeat.setColor(Color.TRANSPARENT); canvas.drawRect(rect, paintSeat); break; } //收集所有的位置信息 mRectList.add(selectRectBean); } } //绘制变化的 if (selectList.size() > 0) { for (int i = 0; i < selectList.size(); i++) { SelectRectBean selectRectBean = selectList.get(i); Rect rect = selectRectBean.getRect(); paintSeat.setColor(Color.GREEN); canvas.drawRect(rect, paintSeat); canvas.drawText(selectRectBean.getRow() + "排" + selectRectBean.getColumn() + "列", rect.left, rect.top + seatWidth / 2, textPaint); } } }
-
绘制行数的列表比较简单,代码如下:
private void drawRowIndex(Canvas canvas) { RectF rect = new RectF(transformOldCoordX(0), marginTopScreen - seatWidth / 2, transformOldCoordX(seatWidth), marginTopScreen + seatWidth * row - seatWidth / 2 - margiVertical); screenPaint.setColor(Color.parseColor("#44666666")); canvas.drawRoundRect(rect, 20, 20, screenPaint); for (int i = 0; i < row; i++) { int startY; if (i == 0) { startY = marginTopScreen; } else { startY = i * seatWidth + marginTopScreen; } //绘制一下左边的排数 canvas.drawText((i + 1) + "", transformOldCoordX(margiLeft - 5), startY == 0 ? seatWidth : startY, textPaint); } }
-
绘制中央屏幕区域,这里代码比较简单,主要就Path来绘制了一个梯形,在绘制一个文字,具体代码如下:
private void drawFilmScreen(Canvas canvas) { //计算出实时的顶部位置,座位的矩阵部分其实是原始的坐标。 screenPaint.setColor(Color.parseColor("#ffffff")); float centerX; if (scale == 1.0) { centerX = ((seatRect.right + seatRect.left) / 2); } else { centerX = ((seatRect.right + seatRect.left) / 2); } float newLeft = (centerX - 100); float newRight = (centerX - filmScreenHeight); float newTop = (centerX + filmScreenHeight); float newBottom = (centerX + 100); Path path1 = new Path(); path1.moveTo(newLeft, transformOldCoordY(0)); path1.lineTo(newRight, transformOldCoordY(filmScreenHeight / 2)); path1.lineTo(newTop, transformOldCoordY(filmScreenHeight / 2)); path1.lineTo(newBottom, transformOldCoordY(0)); path1.close(); canvas.drawPath(path1, screenPaint); canvas.drawText("屏幕", (centerX - filmScreenHeight / 4), transformOldCoordY(filmScreenHeight / 4), textPaint); // LogUtil.i("drawFilmScreen = " + centerX + "---" + getMatrixTranslateY() + "***" + getMatrixTranslateX()); canvas.drawLine(centerX, transformOldCoordY(0), centerX, marginTopScreen + seatWidth * row - seatWidth / 2 - margiVertical, screenPaint); }
-
-
手势部分操作,这里需要注意的地方是需要对手势进行一些筛选和限制操作,比如:滑动整个画布时候如果能够完全显示整个座位区域则不需要滑动;滑动区域的时候也不能无限的滑动,否则会把整个座位区域划出屏幕;下面贴代码:
//首先你要把事件都转发给gestureDetector,然后gestureDetector内部会给我们区分手势,返回给我们需要的手势,如滑动和缩放 @Override public boolean onTouchEvent(MotionEvent event) { //事件分发给手势处理器进行缩放和平移 gestureDetector.onTouchEvent(event); scaleGestureDetector.onTouchEvent(event); return true; }
上面转发后我们就可以在监听回调的方法了,代码有点多,主要因为手势监听和过滤处理比较多,代码如下:
private void initGesture(Context context) { scale = 1.0f; //图片完全显示的伸缩值 // mCanvasMatrix.postTranslate(translateX, translateY); // mCanvasMatrix.postScale(scale, scale); //缩放手势 scaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { isScaling = true; // LogUtil.i("focusX = " + detector.getFocusX()); // 缩放中心,x坐标 // LogUtil.i("focusY = " + detector.getFocusY()); // 缩放中心y坐标 // LogUtil.i("scale = " + detector.getScaleFactor()); // 缩放因子 float scaleFactor = detector.getScaleFactor(); //当前的缩放比例 float fx = detector.getFocusX(); float fy = detector.getFocusY(); // float[] points = mapPoint(fx, fy, mCanvasMatrix); float realScaleFactor = getRealScaleFactor(scaleFactor); mCanvasMatrix.postScale(realScaleFactor, realScaleFactor, 0, 0); invalidate(); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { super.onScaleEnd(detector); isScaling = false; scale = getMatrixScaleX(); reviseTranslate(); } }); //移动手势 gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //方向是相反的,所以需要加负号。 // LogUtil.i("onScroll_change = " + "获取移动的距离" + getMatrixTranslateX() + "---" + getMatrixTranslateY()); //通过移动距离的大小来判断x移动或y轴移动 if (Math.abs(distanceX) > Math.abs(distanceY)) { float x = seatRect.left / getMatrixScaleX() - getMatrixTranslateX() + seatWidth; float standardX = transformNewCoordX(seatRect.left); float xRight = seatRect.right * getMatrixScaleX() + getMatrixTranslateX(); float standardRightX = measuredWidth - seatWidth; // LogUtil.i("onScroll_change = " + "获取移动的距离" + xRight + "---" + standardRightX); if (standardX >= x) { mCanvasMatrix.preTranslate(-5, 0); return true; } else if (xRight < standardRightX) { mCanvasMatrix.preTranslate(5, 0); return true; } mCanvasMatrix.postTranslate(-distanceX, 0); } else if (Math.abs(distanceX) < Math.abs(distanceY)) { float x = seatRect.top / getMatrixScaleY() - getMatrixTranslateY(); float standardX = transformNewCoordY(seatRect.top); float xRight = seatRect.bottom * getMatrixScaleY() + getMatrixTranslateY(); float standardRightX = measuredHeight - seatWidth; LogUtil.i("onScroll_change = " + "获取移动的距离" + xRight + "---" + standardRightX); if (standardX >= x) { mCanvasMatrix.preTranslate(0, -5); return true; } else if (xRight < standardRightX) { mCanvasMatrix.preTranslate(0, 5); return true; } mCanvasMatrix.postTranslate(0, -distanceY); } else { } //应该限制左边的坐标 invalidate(); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent event) { // LogUtil.i(event.getX() + "--onSingleTapConfirmed--" + event.getY()); //这里做点击了处理,开始的x、y坐标 float currentX = event.getX(); float currentY = event.getY(); //需要做坐标点转换,转换为原始的点,在进行点击事件 LogUtil.i(currentX + "--before--" + currentY); currentPoint.set((int) currentX, (int) currentY); clickSeat(currentPoint); if (childSelectListener != null) { childSelectListener.onChildSelect(selectList); } return super.onSingleTapConfirmed(event); } }); }
GestureDetector中有点击的处理这里我也可以直接放到onTouchEvent中进行,速度会比这里要快一些,具体校验点击到哪个具体的位置判断方法在clickSeat()中,判断的依据主要是通过点击的点的坐标,和实际画布上的坐标做比较可以得出哪个座位被点击了。代码如下:
private void clickSeat(Point currentPoint) { for (int i = 0; i < mRectList.size(); i++) { Rect rect = mRectList.get(i).getRect(); SelectRectBean selectRectBean = mRectList.get(i); if (selectRectBean.getSeatState() == SELL_SEAT) { continue; } float newLeft = rect.left * getMatrixScaleX() + 1 * getMatrixTranslateX(); float newRight = rect.right * getMatrixScaleY() + 1 * getMatrixTranslateX(); if (currentPoint.x > newLeft && currentPoint.x < newRight) { float newTop = rect.top * getMatrixScaleX() + 1 * getMatrixTranslateY(); float newBottom = rect.bottom * getMatrixScaleY() + 1 * getMatrixTranslateY(); if (currentPoint.y > newTop && currentPoint.y < newBottom) { //点击到了某一个 if (selectList.contains(selectRectBean)) { selectList.remove(selectRectBean); } else { selectList.add(selectRectBean); } //更新界面 invalidate(); break; } } } }
以上就是主要代码的主要内容了,其实还是比较清楚,整个流程下来 绘制各部分——>手势监听——>手势限制,
介绍了大概,如果需要具体看细节的话,建议看看具体的源码吧,本项目为了练习所以业务上不完整,后续会继续更新完整。
新添加了概览图
添加整体的概览图,主要是通过对整个画布进行一个等比例的缩放操作实现。在ondraw方法中添加了两个方法,这里其实是由其他方案,因为概览图的座位并不是每次都需要绘制,这里可以把座位背景单独绘制,通过标识来决定是否重绘,重绘的关键点其实就是座位状态的变化,因为其他情况是不需要重绘缩略的座位图的。
代码如下:
//是否绘制概览区域
if (isDrawOver) {
//绘制概览区域
drawOverView(canvas);
//绘制边框
drawOverBorder(canvas);
}
方法分为几个步骤:
- 挥着整个背景区域+座位信息,这里主要需要注意的是缩放的比例要和画布保持一致,因为后来你要添加的选中可视区域的方框是要同比例的放到概览图上,所以比例一定不能有差异。下面我尝试做一些优化但是还没完成,主要是想减少概览图的绘制。
看一下代码:
private void drawOverView(Canvas canvas) {
float left = transformOldCoordX(0);
float top = transformOldCoordY(0);
float right = left + transformCoverDistance(mCanvasRect.right - mCanvasRect.left);
float bottom = top + transformCoverDistance(mCanvasRect.bottom - mCanvasRect.top);
RectF rect = new RectF(left, top, right, bottom);
// mCoverCanvasMatrix.reset();
// mCoverCanvasMatrix.postScale(1 / getMatrixScaleX(), 1 / getMatrixScaleY());
// mCoverCanvasMatrix.postTranslate(-getMatrixTranslateX()/ getMatrixScaleX() / ratioOver, -getMatrixTranslateY()/ getMatrixScaleX() / ratioOver);
//绘制中心线和屏幕
drawFilmScreenCover(rect, canvas);
//是否绘制图,一般只有点击时间才会重绘
if (isDrawOverBitmap) {
}
mBitmap = drawSeatRectOver(rect, canvas);
// if (mBitmap != null) {
// canvas.drawBitmap(mBitmap, mCoverCanvasMatrix, overPaint);
// }
}
- 绘制可视区域的窗口框,这里其实就是view的尺寸,因为view的大小是固定的,我们通过移动画布来改变可视区域,我们只要包view的尺寸线缩放到概览图的尺寸,然后在根据手势的变化去改变这个方框的尺寸就可以了,这里我花费了比较多的时间,因为开始我只是把座位区域的rect移到概览图导致比例不对显示的框和屏幕不一致。下面看一下代码:
private void drawOverBorder(Canvas canvas) {
//绘制移动的框,应该显示屏幕区域内的座位,屏幕点转换到画布上,在转换到缩略图上
float left = transformOldCoordX(0) - transformCoverDistance2(getMatrixTranslateX());
float top = transformOldCoordY(0) - transformCoverDistance2(getMatrixTranslateY());
float right = left + transformCoverDistance2(measuredWidth);
float bottom = top + transformCoverDistance2(measuredHeight);
// float right = transformOldCoordX(measuredWidth) / getMatrixScaleX() / ratioOver - getMatrixTranslateX() / getMatrixScaleX() / ratioOver;
// float bottom = transformOldCoordY(measuredHeight) / getMatrixScaleY() / ratioOver - getMatrixTranslateY() / getMatrixScaleY() / ratioOver;
// LogUtil.i(left + "-右-" + right + "-下-");
RectF rectBorder = new RectF(
left,
top,
right,
bottom);
canvas.drawRect(rectBorder, paintBorder);
}
完成上面的内容概览图基本就完成了效果还不错吧。
分为两个一个是绘制中间的屏幕和中心线,一个是绘制座位了。代码就不贴出来了,因为主要是做了缩放和位置的校正。
修改了边界限制的方式
- 以前使用的是到边界值就直接把座位图,放到合适的位置。
- 新的方法是把边界进行一个回弹的操作
这种方式是参照的这个项目实现的,其实我觉得也不是很好把,我以前的方案主要存在的问题是,在达到边界的时候,强制限制画布位置,会操作很僵硬,不流畅,体验不好,所以一直在寻找一种更好的方式。
代码不写了,直接到项目中了解把。
如果有更好的方案,欢迎留言给我啊,谢谢了。
设置座位初始状态的方法如下,我是通过一个二维数组来实现的,代码如下:
这种方式和实际业务可能不符合,但是可以通过自己业务需求修改,思想是座位的几个状态是通过一个int指来进行区分的。
//外层数组,这里是,默认座位状态。0等于空白位置;1等于未选择座位;2等于已经选择座位
seatList = new int[9][];
for (int i = 0; i < 9; i++) {
int[] indes = new int[13];
for (int x = 0; x < 13; x++) {
if (i == 4) {
if (x < 3 || x > 9) {
indes[x] = 0;
} else if (x == 6) {
indes[x] = 2;
} else {
indes[x] = 1;
}
} else {
indes[x] = 1;
}
}
seatList[i] = indes;
}
searchSeat.setSeatList(seatList);
后记
未完成部分功能:
-
- 左上角的小的概览图
-
- 边界修正的优化