andriod 电影票在线选座控件
封面图: |
---|
不知道大家有没有跟我一样的感觉,看了那么多的介绍自定义控件原理、事件分发机制的书籍,文章,教程,依然还是不能随心所欲的自定义控件。甚至是看了再忘,忘了再看,很尴尬有木有。有的时候真正遇到了事件冲突一脸懵逼有木有。其实导致这些问题原因很简单,一句话就可以说明问题了“纸上得来终觉浅,绝知此事要躬行。”正如这篇介绍如何练习1万小时的文章里所说的,从不会到会,秘诀是重复。我们需要一遍一遍仔细的阅读理解,并用代码实践来验证,学到的这些概念流程知识,这样才会在脑海里留下比较深刻的印象,才能自如的应用学到的知识。学习view的绘制原理,事件分发机制的目的是为了自定义控件,所以学了这些知识后,就需要通过实战多自定义几个控件,来不停的应用,消化这些知识。当你真正自己写了几个自定义控件后,你会发现view的绘制原理,事件分发机制这些东西都是死的,真正麻烦的是绘制逻辑,绘图逻辑,计算逻辑以及一些相关的数学知识。 下面开始正题,不知道大家有没有用过,淘宝电影客户端(淘票票)买过电影票,纵观各类在线选座app的在线选座功能 淘宝在线选座功能用户体验最好,用起来最顺手,夸张点说已经到了炉火纯青的地步,下面我们看一下效果: 效果分析:整个控件分成几个部分,座位图区域、座位缩略图区域、行号区域、屏幕区域 涉及到的知识点:view的绘制原理、事件分发机制这些就不说了,这些是基础,这里并不打算介绍,网上有非常多的这方面的资料。 编码实现通过以下几个核心部分来介绍,其他部分都是类似的思路实现 1、绘制座位图座位图实际上就是个二维矩阵,有行数和列数,我们只需要根据行数和列数加上一定的间距绘制即可。 void drawSeat() { for (int i = 0; i < row; i++) { for (int j = 0; j < column; j++) { int left = j * seatBitmap.getWidth() + j * spacing; int top = i * seatBitmap.getHeight() + i * verSpacing; int seatType = getSeatType(i, j); switch (seatType) { case SEAT_TYPE_AVAILABLE: seatCanvas.drawBitmap(seatBitmap, left, top, paint); break; case SEAT_TYPE_NOT_AVAILABLE: break; case SEAT_TYPE_SELECTED: seatCanvas.drawBitmap(checkedSeatBitmap, left, top, paint); break; case SEAT_TYPE_SOLD: seatCanvas.drawBitmap(seatSoldBitmap, left, top, paint); break; } } } isNeedDrawSeatBitmap = false; }
getSeatType()方法是用来判断当前的座位是否可用,是否已经卖出去,是否已经选中,根据这些状态绘制不同的座位图。 2、座位图的缩放和移动 移动缩放功能使用 Matirx来实现,Matrix在Android中可以用来对图片进行缩放、移动、旋转等变换。 MSCALE_X MSKEW_X MTRANS_X 实际上就是一个有9个元素的数组 Matrix类有一些方法可以对这些值进行改变 座位图平移缩放的实现思路: 要做两件事情来实现座位图的缩放移动 ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.OnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { float scaleFactor = detector.getScaleFactor(); matrix.postScale(scaleFactor, scaleFactor, scaleX, scaleY); invalidate(); return true; } }); onTouchEvent处理逻辑 @Override public boolean onTouchEvent(MotionEvent event) { int y = (int) event.getY(); int x = (int) event.getX(); scaleGestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = x; downY = y; break; case MotionEvent.ACTION_MOVE: int downDX = Math.abs(x - downX); int downDY = Math.abs(y - downY); if (downDX > 10 || downDY > 10) { int dx = x - lastX; int dy = y - lastY; matrix.postTranslate(dx, dy); invalidate(); } break; case MotionEvent.ACTION_UP: .... break; } lastY = y; lastX = x; return true; } onDraw的时候 @Override protected void onDraw(Canvas canvas) { ..... canvas.drawBitmap(seat, matrix, paint); ..... } 3、座位图的自动回弹、自动缩放效果的实现 为什么要自动回弹呢,因为你操作的时候有可能把座位图移到屏幕外,缩放的时候把图缩放的比较小,或者比较大,这个时候程序通过计算给你自动的移动到一个比较合适的位置,比较合适的缩放大小。这样就有着不错的使用体验。 自动回弹实现的思路: 移动规则如下(参考的淘票票客户端的移动逻辑) 移动和缩放涉及到一个弹性移动和缩放的问题,所谓弹性移动就是有动画效果的移动。因为如果你当前在100,100 这个位置,你需要移动到800,100这个位置,如果你直接移动到800这个位置,而不是通过,先移动到110,在移动到120。。。一直到800这样一段一段的移动。那么移动效果将是非常僵硬的,刷的闪过去的感觉,效果非常不好。
int FRAME_COUNT = 10; int time = 15; int count; Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (count < FRAME_COUNT) { count++; MoveInfo moveInfo = (MoveInfo) msg.obj; float moveXLength = moveInfo.moveXLength; float moveYLength = moveInfo.moveYLength; float xValue = moveXLength / FRAME_COUNT; float yValue = moveYLength / FRAME_COUNT; matrix.postTranslate(xValue, yValue); invalidate(); Message message = Message.obtain(); message.obj = msg.obj; //循环调用 handler.sendMessageDelayed(message, time); } else { count = 0; } return true; } 弹性缩放的原理跟弹性移动的原理一致4、缩略图部分的绘制实现 缩略图是座位图的缩小版,缩略图的宽高和座位图的宽高有一定比例,比如五分之一。当然这是可以根据效果来调整的。之所以必须是座位图的一定比例,是因为缩略图上有一个动态的红色方框表示当前可见的座位区域,这个红色方框是需要根据座位图的移动而移动的。比例确定后,就这可以根据座位图的移动来移动红色方框。举个例子来说-如果当前座位图向上移动了100个像素,那么缩略图中对应的红色方框部分向下移动 100/4=250个像素即可。 绘制概览图代码: Bitmap drawOverview() { //计算概览图上代表座位的正方形宽度、高度 //规则 :座位图上座位的宽度/缩放比例 float rectSize = seatBitmap.getHeight() / overviewScale; //计算概览图的宽度和高度 rectW = column * rectWidth + (column - 1) * overviewSpacing + overviewSpacing * 2; rectH = row * rectSize + (row - 1) * overviewVerSpacing + overviewVerSpacing * 2; Canvas canvas = new Canvas(overviewBitmap); //绘制透明灰色背景 canvas.drawRect(0, 0, rectW, rectH, paint); paint.setColor(Color.WHITE); //循环绘制 for (int i = 0; i < row; i++) { float top = i * rectSize + i * overviewVerSpacing + overviewVerSpacing; for (int j = 0; j < column; j++) { //获取座位是什么状态 已经售出、未选中、已经选中 int seatType = getSeatType(i, j); switch (seatType) { case SEAT_TYPE_AVAILABLE: paint.setColor(Color.WHITE); break; case SEAT_TYPE_NOT_AVAILABLE: continue; case SEAT_TYPE_SELECTED: paint.setColor(overview_checked); break; case SEAT_TYPE_SOLD: paint.setColor(overview_sold); break; } float left; left = j * rectWidth + j * overviewSpacing + overviewSpacing; canvas.drawRect(left, top, left + rectWidth, top + rectSize, paint); } } return overviewBitmap; } 绘制概览图上的红色方框 /** * 绘制概览图 */ void drawOverviewBorder(Canvas canvas) { //绘制红色框 int left = (int) -getTranslateX();//获取当前座位图x轴上的平移位置 if (left < 0) { left = 0; } left /= overviewScale; //overviewScale 表示概览图占座位图的比例 left /= getMatrixScaleX();//getMatrixScaleX() 获取当前座位图的缩放比例 //判断当前座位图的宽度是否超出 整个控件的宽度,如果超出了,那么超出的部分就看不见。表示当前能看到的位置的红色方框就不能把超出的部分框进来。 int currentWidth = (int) (getTranslateX() + (column * seatBitmap.getWidth() + spacing * (column - 1)) * getMatrixScaleX()); if (currentWidth > getWidth()) { currentWidth = currentWidth - getWidth(); } else { currentWidth = 0; } int right = (int) (rectW - currentWidth / overviewScale / getMatrixScaleX()); float top = -getTranslateY()+headHeight; if (top < 0) { top = 0; } top /= overviewScale; top /= getMatrixScaleY(); if (top > 0) { top += overviewVerSpacing; } //判断当前座位图的宽度是否超出 整个控件的高度,如果超出了,那么超出的部分就看不见。表示当前能看到的位置的红色方框就不能把超出的部分框进来。 int currentHeight = (int) (getTranslateY() + (row * seatBitmap.getHeight() + verSpacing * (row - 1)) * getMatrixScaleY()); if (currentHeight > getHeight()) { currentHeight = currentHeight - getHeight(); } else { currentHeight = 0; } int bottom = (int) (rectH - currentHeight / overviewScale / getMatrixScaleY()); canvas.drawRect(left, top, right, bottom, redBorderPaint); } 控件性能优化 千辛万苦终于把控件做出来了,结果一运行卡的不要不要的。特别是行数列数一多,卡顿的懵逼了。这个时候呢我们要对性能进行优化,总结下来主要从以下几个方面: 后续填坑坑一 一开始的时候为了方便,整个座位图是在一个Bitmap上绘制的,这会导致一个问题,当行数列数一多,就会创建出一个尺寸巨大的Bitmap,直接导致卡顿或者oom >_<||| 坑二 弹性移动,缩放是自己用handler实现的‘(>﹏<)′ 不要自己用handler实现 ,用属性动画来做,也就是ValueAnimator来做。 坑三 写代码,的时候还是要考虑代码的运行效率的。。。对onDraw方法来说“差之毫厘失之千里”
|