用到内容:
- 自定义View过程onMeasure,onLayout,onDraw。
- Matrix的简单应用。
- 对滑动方式理解。
- 手势ScaleGestureDetector,GestureDetector使用。
- 一些简单图形的绘图,如文字,椭圆,矩形;画布的保存save和恢复retore。
遇到的问题:
1.滑动速度过快,导致手轻轻滑动视图就移动出屏幕。
2.OnTouch与手势冲突,在OnTouch里接收不到ACTION_UP事件。
3.放缩与滑动,在放缩过程中出现了滑动。
4.滑动卡屏,这个貌似是因为原来该滑动的还未结束,就符合自动恢复原位的条件,导致视图在恢复到原位置后又继续滑动(刷新视图),然后由于速度过快,就看见视图在闪。
5.给画布设置Matrix,导致整张画布移动也就是画布上所有的东西一起移动,放缩。
6.设置缩放中心跟手。
思路:
第一,这个座位图是个二维矩阵,并且让元素值0,1区分座位是否已售。
第二,索引图的数字是和座位图的行关联的,画出来的屏幕的Left距离是和座位图的Left相关联的(这里其实最好是和中心座位的左边关联),这样可以实现:只移动座位图,索引图和屏幕跟着滑动并且相对位置不变。
第三,缩略图只需要跟踪座位图的宽高和缩放比例就好。
第四,点击事件可以根据点击位置的范围判断是哪个元素,并通过元素值来决定是否可选。
第五,底下的button背景及文字变化,根据之前选座生成的队列大小是否为0来设置,为0,则显示“请先选座”并关闭点击事件,不为0,则进行计算然后显示价格并开启点击事件。这里只需要设置一个标志位即可控制点击事件的执行。
第六,座位图的缩放、平移通过手势监听和在OnTouch中处理。
好了,大体思路知道了,我们来看一下实现步骤和具体逻辑处理。
实现步骤:
1.定义一个MyView类(名字自己取,尽量有意义)继承View,然后重写onLayout,onMeasure,onDraw方法。
onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.e("onMeasure"," ");
mWidth = MeasureSpec.getSize(widthMeasureSpec); //获取到View的宽
mHeight = MeasureSpec.getSize(heightMeasureSpec); //获取到View的高
getLayoutParams().height = mHeight; //设置View的宽
getLayoutParams().width = mWidth; //设置View的高
Log.i("onMeasure",mWidth +" "+mHeight );
super.onMeasure(widthMeasureSpec , heightMeasureSpec );
}
onLayout:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.e("onLayout"," ");
super.onLayout(changed, left, top, right, bottom);
myViewTop = top; //MyView的top,因为它与手机屏幕顶部有距离,先记录下来
mViewH = bottom -top; //MyView的高度
mViewW = right -left; //MyView的宽度
mSeatW = seatBitmap.getWidth(); //一个座位的宽
mSeatH = seatBitmap.getHeight(); //一个座位的高
sViewH = (int)(mViewH /overviewScale); //缩略图中一个座位的宽度
sViewW = (int)(mViewW /overviewScale); //缩略图中一个座位的高度
Log.i("onLayout","height ="+getLayoutParams().height);
}
下来是我们整个View视图的重点,也就是onDraw,它绘出了我们看到的整个视图:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e("OnDraw"," ");
drawSeat(canvas); //画座位图
canvas.save(); //保存
drawScreen(canvas); //画屏幕
drawScreenText(canvas); //画屏幕上的文字
drawIndexView(canvas); //画索引条
drawOverView(canvas); //画缩略图
canvas.restore(); //恢复,即当前画布为画座位图的画布
isCanDrag = true; //这是一个标志位,用来控制拖拽
//这个if判断在做一件事:根据是否选座来设置底下Button的背景、文字、点击事件
if(selectList != null && selectList.size() !=0){
MainActivity.order.setBackgroundResource(R.color.colorAccent) ;
MainActivity.order.setText(MyView.selectList.size()*price+"元 确认支付"); //给button设置文字
isClick = true; //这是一个标志,点击有效
}else{
isClick = false; //点击无效
MainActivity.order.setBackgroundResource(R.color.colorOrder_bg_unClick) ;
MainActivity.order.setText("请先选座"); //给button设置文字
MainActivity.order.setTextColor(R.color.colorOrder);
}
}
2.具体实现刚刚onDraw中的绘图操作
drawSeat(Canvas canvas) 绘制座位图
public void drawSeat(Canvas canvas) {
selectList.clear();
for(int i = 0 ; i< row; i++)
{
for(int j = 0 ; j< column; j++)
{
int left = (j+1)* seatBitmap.getWidth() +j * spacing;
int top = (i+1)* seatBitmap.getHeight() +i * varSpacing;
left = (int)(left*getScale())+marginLeft;
top = (int)(top*getScale())+marginTop;
Paint paint = new Paint();
switch(data[i][j]){
case SEAT_TYPE_NOT_SELECTED:
canvas.drawBitmap(seatBitmap ,left ,top ,paint);
break;
case SEAT_TYPE_SELECTED:
selectList.add(new Seat(SEAT_TYPE_SELECTED ,i,j,left,top ));
canvas.drawBitmap(checkedSeatBitmap,left ,top ,paint );
paint.setColor(Color.WHITE);
paint.setStrokeWidth(2);
int size = 24;
paint.setTextSize(size);
canvas.drawText((i+1)+"排",left+10,top+25,paint);
canvas.drawText((j+1)+"列",left+10,top+45,paint );
break;
case SEAT_TYPE_SOLD:
canvas.drawBitmap(seatSoldBitmap ,left ,top ,paint );
break;
}
}
}
}
可以看到,通过两层for循环遍历我们的座位二维数组,然后计算出它们的left和top位置,最后通过switch语句判断该绘制那种类型的座位。我们有三张图,分别对应三种情况:未选、已售、已选。
drawScreen(Canvas canvas) 绘制屏幕
@SuppressLint("ResourceAsColor")
public void drawScreen(Canvas canvas){
Path path = new Path(); //通过路径绘图
Paint paint = new Paint(); //画笔
paint.setColor(getResources().getColor(R.color.colorS_bg)); //设置画笔颜色
paint.setStyle(Paint.Style.FILL); //设置画笔风格,这里全部填充
path.moveTo(mViewW/2-300+DScreen,20 ); //画笔移动到
path.lineTo(mViewW /2-300+DScreen,20); //左上
path.lineTo(mViewW/2+250+DScreen,20 ); //右上
path.lineTo(mViewW/2+200+DScreen,80 ); //右下
path.lineTo(mViewW/2-250+DScreen,80 ); //左下
path.close(); //路径闭合
canvas.drawPath(path ,paint); //画出路径
}
里面的逻辑很简单,就是会找了一个填充的梯形。你在写这个代码之前,你可以在纸上或者绘图工具上画一个梯形,然后找到第一个左上点然后算出坐标,按顺序写上去就好。
drawScreenText(Canvas canvas) 绘制屏幕文字
@SuppressLint("ResourceAsColor")
public void drawScreenText(Canvas canvas){
Paint paint = new Paint();
paint.setColor(R.color.colorOrder);
paint.setStrokeWidth(10); //设置画笔宽度
int size = 30; //字体大小
paint.setTextSize(size); //设置字体大小
canvas.drawText("7号厅3D荧屏",mViewW/2-100+DScreen ,60,paint);
}
屏幕文字,可以设置为成员变量,这样就可以通过你给它传的值而改变,字体大小本应该乘以放缩比,这样可以实现文字的放缩。
drawIndexView(Canvas canvas) 绘制索引
@SuppressLint("ResourceAsColor")
public void drawIndexView(Canvas canvas){
Paint paint = new Paint();
paint.setColor(R.color.colorS_bg);
paint.setStyle(Paint.Style.FILL);
RectF rectF = new RectF();
rectF.top = marginTop + seatBitmap.getHeight()*getScale()/2;
indextViewTop = (int) rectF.top;
rectF.bottom =(int) ((column * seatBitmap.getHeight() + (column-1)*varSpacing)*getScale()) +marginTop;
rectF.left = indextViewLeft;
rectF.right = 70;
canvas.drawRoundRect(rectF,20 ,20,paint);
//绘制数字
paint.setColor(R.color.colorText);
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(30);
paint.setStrokeWidth(1);
for(int i = 0 ; i< row ;i++){
float x = rectF.left +10;
float y = rectF.top + (i*varSpacing +(i+1)*seatBitmap.getHeight())*getScale();
canvas.drawText(""+(i+1),x,y,paint );
}
}
逻辑很简单,先绘制了一个填充椭圆,并且它的top和座位图左上角第一个的top一样,bottom和座位图第一列最后一个的bottom一致,这个通过计算就可以得到。然后绘制文字,间隔与座位图竖直间隔一致。
drawOverView(Canvas canvas) 绘制缩略图
@SuppressLint("ResourceAsColor")
public void drawOverView(Canvas canvas){
Paint paint = new Paint();
paint.setColor(R.color.colorS_bg);
paint.setAlpha(8); //设置透明度
paint.setStyle(Paint.Style.FILL);
Rect rect = new Rect(mViewW-sViewW ,10,mViewW-20 ,sViewH-20);
canvas.drawRect(rect ,paint );
Paint paint1 = new Paint();
paint1.setColor(R.color.colorText);
paint1.setStyle(Paint.Style.FILL);
for(int i = 0 ; i< row ; i++)
for(int j = -0; j< column ; j++)
{
int left =(int) (((j+1)* seatBitmap.getWidth() +j * spacing)/overviewScale);
int top = (int)(((i+1)* seatBitmap.getHeight() +i * varSpacing)/overviewScale);
int right = left + (int)(mSeatW/overviewScale);
int bottom = top - (int)(mSeatH/overviewScale );
Rect rect1 = new Rect(rect.left+left ,rect.top +top ,rect.left+right ,rect.top+ bottom );
canvas.drawRect(rect1 ,paint1);
}
drawOverRect(canvas );
}
这个地方我只是实现了视图,它和真实放大缩小的位置,程度暂时还没实现关联。这个有待改正。
3.手势监听的设置及一些处理
主要是缩放手势,然后就是滑动和点击,我用到了ScaleGestureDetector、GestureDetector和View.OnTouchListener。第一个用来监听放缩手势,用MyView implements它,实现它的三个方法,主要是onScale;第二个我并没有用MyView implements它,因为直接implements它,会有一堆方法,而我只想用它的onSingleTap方法,并且这里面有一个坑,后面写到我再说;第三个用来实现滑动,在它的onTouch方法中通过对基本事件的捕捉实现。
public class MyView extends View implements View.OnTouchListener,ScaleGestureDetector.OnScaleGestureListener
缩放实现
private ScaleGestureDetector scaleGestureDetector;
//在MyView构造中
scaleGestureDetector = new ScaleGestureDetector(context , this);
//在onTouch中给它分发事件
scaleGestureDetector.onTouchEvent(event);
//需要实现的三个方法
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = getScale(); //上次缩放比
float scaleFactor = detector.getScaleFactor(); //当前缩放比
Log.i("onScale","缩放中");
if((scale < SCALE_MAX && scaleFactor >1.0f) ||(scaleFactor < 1.0f && scale > SCALE_INIT))
{
//计算缩放比scaleFactor
if(scaleFactor * scale < SCALE_INIT)
{
scaleFactor = SCALE_INIT / scale;
}
if(scaleFactor * scale > SCALE_MAX)
{
scaleFactor = SCALE_MAX /scale;
}
matrix.postScale(scaleFactor ,scaleFactor ,detector.getFocusX() ,detector.getFocusY()); //给matrix设置缩放值及中心点
invalidate(); //刷新界面
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
isScale = true;
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
isScale = false;
}
//这里还用到了Matrix
//获取Scale值
public float getScale(){
matrix.getValues(matrixValues);
return matrixValues[Matrix.MSCALE_X];
}
//给Matrix设置Scale在onScale方法中
matrix.postScale(scaleFactor ,scaleFactor ,detector.getFocusX() ,detector.getFocusY());
监听缩放手势,在当发生缩放手势时,调用这三个方法onScaleBegin,onScale,onScaleEnd,只要缩放的逻辑在onScale中,detector.getScaleFactor()获取的值是:当前缩放比/上次缩放比,也就是缩放比的增量比。
这里还有一个问题,由于我画bitmap的时候并没用matrix,所以缩放的位置并没有跟随手,因为如果在draw的时候设置matrix,bitmap的位置有问题。这个我之后会更改,如下是我draw bitmap:
canvas.drawBitmap(seatBitmap ,left ,top ,paint);//可以看出,我用的是位置不是matrix画出来的
单击实现
private GestureDetector gestureDetector ;
//在MyView构造中:
gestureDetector = new GestureDetector(context ,new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onSingleTapUp(MotionEvent e) {
isCanDrag = false; //点击时不可以移动
handleClickEvent(e); //这个方法是单击事件的具体处理内容
isCanDrag = true;
return true;
}
});
//在onTouch方法中给它分发事件
gestureDetector.onTouchEvent(event);
我说了这里有一个坑,那就是你会发现我并没有像ScaleGestureDetector那样直接的用MyView类implement它,而是在MyView构造中才去实例化它的,这不仅是因为我不想让它产生那么多方法,并且我发现,如果直接去implement它,在onTouch方法中会收不到up事件,这会导致我后面的逻辑不能执行,也就是在onTouch中up事件的处理得不到执行。
滑动实现
@Override
public boolean onTouch(View v, final MotionEvent event) {
scaleGestureDetector.onTouchEvent(event);
gestureDetector.onTouchEvent(event);
float x = 0;
float y = 0;
int pointerCount = event.getPointerCount(); //获取屏幕上的手指个数
for(int i = 0 ; i< pointerCount ; i++){
x += event.getX(i);
y += event.getY(i);
}
//计算平均值
x = x/ pointerCount;
y = y/ pointerCount;
//触摸屏幕状态改变
if(pointerCount != lastPointerCount){
isCanDrag = false;
mLastX = 0;
mLastY = 0;
}
lastPointerCount = pointerCount;
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
if(mLastY == 0 && mLastX ==0){
mLastX = x;
mLastY = y;
break;
}
int dx = (int)(x - mLastX);
int dy = (int)(y - mLastY);
Log.i("MOVE上次","mLastX="+(int)mLastX+",mLastY="+(int)mLastY);
Log.i("MOVE这次","x="+(int)x+",y="+(int)y);
Log.i("dx,dy",dx+","+dy);
isCanDrag = isCanDrag(dx,dy);
if(isCanDrag && !isAutoDraging ) {
pingMuAnimTrans(dx,dy); //实现滑动方法
invalidate();
}
if(getScale() >1.0 && ((initMarginTop -marginTop)<-200*getScale()||(initMarginTop -marginTop)>500*getScale()
|| initMarginLeft -marginLeft>500*getScale()|| initMarginLeft -marginLeft<-300*getScale()))
AutoPosition();
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mLastX = 0;
mLastY = 0;
if(getScale() <=1.0)
AutoPosition();
break;
}
return true;
}
//真正实现滑动的方法
public void pingMuAnimTrans(final int dx ,final int dy){
if(isAutoDraging)
return;
Log.i("pingMuTrans","在滑动");
ValueAnimator valueAnimator = ValueAnimator.ofInt(1 ,100) ;
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener( ) {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
marginLeft +=dx*0.1; //座位图左右,这个0.1也是为了控制滑动的速度,通过位移
marginTop +=dy*0.1; //座位图上下
indextViewTop +=dy*0.1; //移动的是索引图的上下
DScreen +=dx*0.1; //移动的是电影屏幕的左右
// Log.i("Anim",dx+","+dy);
invalidate();
}
});
valueAnimator.setDuration(12); //通过这个可以改变滑动的速度,通过时间
valueAnimator.start();
}
不要看代码很长,但很好理解,在OnTouch中通过记录上次手指的平均位置和当前手指的平局位置,计算出每次需要移动的dx,dy。然后通过pingMuAnimTrans(final int dx ,final int dy)完成滑动,我的滑动是通过改变marginLeft(左边与座位图的距离)和marginTop(上边与座位图的距离)来改变view的位置,并通过ValueAnimator来实现一个滑动效果。这里来说一下。scrollerTo,scrollerBy方法移动的是View图的内容,并且它只是影像的移动,真实位置其实并没有改变。matrix与画布绑定,也就是canvas.setMatrix(matrix),移动的是整张画布,就像移动一张图片。还可以通过handler实现,这个要写个Runnable,由于本人比较懒,没用这种方法。注意,View动画不会改变真实位置,所以如果有交互,滑动要改变它的属性才可以。
4.点击事件实现(选座)
public void handleClickEvent(MotionEvent e){
float x = e.getX();
float y = e.getY();
for(int i = 0 ; i< row ; i++){
for(int j = 0 ; j< column ; j++){
float left = (j+1)* seatBitmap.getWidth() +j * spacing;
float top = (i+1)* seatBitmap.getHeight() +i * varSpacing;
left = left*getScale()+marginLeft;
top = top*getScale() +marginTop;
float minX = left ;
float minY = top ;
float maxX = left +seatBitmap.getWidth()*getScale();
float maxY = top+seatBitmap.getHeight()*getScale();
if(x >=minX && x<=maxX &&y >=minY && y<=maxY && data[i][j] ==SEAT_TYPE_NOT_SELECTED){
addASeat(SEAT_TYPE_SELECTED ,i,j);
return ;
}
if(x >=minX && x<=maxX &&y >=minY && y<=maxY && data[i][j] ==SEAT_TYPE_SELECTED){
data[i][j] = SEAT_TYPE_NOT_SELECTED;
postInvalidate();
}
}
}
}
逻辑挺简单的,就是判断当前点击位置是否在二维数组元素的范围内,如果在,就可以获取到它的i,j也就是x,y坐标。根据元素值采取相应措施。
5.回到原始位置实现
public void AutoPosition(){
isAutoDraging = true;
ValueAnimator valueAnimator = ValueAnimator.ofInt(1,100);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener( ) {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fract = animation.getAnimatedFraction();
marginLeft +=(initMarginLeft -marginLeft)*fract;
DScreen -=DScreen*fract;
marginTop +=(initMarginTop - marginTop)*fract;
indextViewTop +=(initMarginTop - marginTop)*fract;
invalidate();
}
});
valueAnimator.setDuration(500);
valueAnimator.start();
invalidate();
isAutoDraging = false;
}
因为我的滑动是通过控制距离实现的,所以要实现回到原位置就很简单了,只需要在开始的时候记录一下距离,然后只需要用原始距离和当前距离的距离差,进行一个滑动,并且在原始比例会回到原始位置和在放大时如果超出代码中给的边界值就会回到原始位置。