学习Android已经快一年了,这之间学习了很多大神写的Demo和完整项目,但是总是感觉学的是别人的思维,作为一个上进的程序员来说,这是远远不够的,所以今天来写一篇比较简单的自定View,用的都是最基础的知识,主要的目的是捋清楚一下思路和实现的一些算法。希望能帮助到一些初学者来建立起自己的思维方式。
忽略一下这么丑的gif图,主要是展示下效果
下面我们一步步讲解,首先在拿到这个课题后,先要想想它需要一些什么功能,通过什么方式实现。比如这个Demo是自定义View拼图游戏。那么我们就需要考虑自定义View需要处理的几个问题
1.怎么获取拼图所需要的图片
2.怎么将图片合适的缩小放大以适配当前设备
3.怎么将图片分割成等大小块
4.怎么将分好的图片重新排列成原图的大小格式
5.怎么实现拼图,拼图的方式如何选择,以什么方式移动图片
6.怎么判断拼图是否完成
基本的一些问题已经列出来了,之后我们将进行一步步分析,实现
首先是怎么获取所需要的图片,两种方式,第一种就是从本地系统相册获取,第二种就是调用相机拍照获取。这两种方式想必大家都明白,我这里就不多说获取图片的方式,实在不理解的人,等下我会贴出源码
第二怎么将图片合适的缩小放大:现在大部分的用户手机里面存的图片大小都不一致,一般用手机相机拍出来的图片都比较大,如果是按原图显示只能显示出一个部分,所以必须要进行缩小和放大图片,下面是相应代码
public static Bitmap zoomImage(Bitmap bgimage, double newWidth,double newHeight) {
// 获取这个图片的宽和高
float width = bgimage.getWidth();
float height = bgimage.getHeight();
// 创建操作图片用的matrix对象
Matrix matrix = new Matrix();
// 计算宽高缩放率
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// 缩放图片动作
matrix.postScale(scaleWidth, scaleHeight);
Bitmap bitmap = Bitmap.createBitmap(bgimage, 0, 0, (int) width,(int) height, matrix, true);
return bitmap;
}
通过传入三个值进行缩放,第一个传第一步我们获得的图片的bitmap,第二三分别是传入缩放后的宽度和高度,这个宽度和高度不能直接填你手机的高度和宽度,因为图片的宽高比例不一定适合你手机的宽高比例,所以我们必须通过比例来计算一下传入的宽高,为了美观,宽度可以直接填手机的宽度,但是高度必须通过乘上图片bitmap的高宽比例换算后传入
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
float screenWidth = dm.widthPixels;
float screenHeight = screenWidth*bm.getHeight()/bm.getWidth();
第三。将处理后的bitmap分割一下 用一个ImagePiece对象保存,分割处理的代码如下
public static List<ImagePiece> splitImage(Bitmap bitmap, int piece)
{
List<ImagePiece> imagePieces = new ArrayList<ImagePiece>();
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int pieceWidth = width / piece;
int pieceHeight = height / 4;
for (int i = 0; i < piece; i++){
for (int j = 0; j < piece+1; j++){
ImagePiece imagePiece = new ImagePiece();
int x = i * pieceWidth;
int y = j * pieceHeight;
imagePiece.setBitmap(Bitmap.createBitmap(bitmap, x, y,
pieceWidth, pieceHeight));
imagePiece.setPosition(i*4+j);
imagePieces.add(imagePiece);
}
}
return imagePieces;
}
分割的方式是通过Bitmap.createBitmap完成,即通过一个嵌套循环在原始图片上从上到下,从左到右分割出12个新的bitmap,然后存入list容器,Position是用于之后拼图时识别是否拼图成功。
第四:终于进入了主题,需要将这些小图片显示出来,自然需要自定View将每个bitmap通过合适的位置绘制出来,所以第一步,新建一个PuzzleView对象,让其继承View,重写其onDraw(),在绘制过程中,我们依然像切割图片一样处理
for (int i=0;i<3;i++){
for (int j=0;j<4;j++){
canvas.drawBitmap(list.get(i*4+j).getBitmap(),i*imageWidth,j*imageHeight,paint);
canvas.drawLine(i*imageWidth,(j+1)*imageHeight,(i+1)*imageWidth,(j+1)*imageHeight,paint);
if(i!=2){
canvas.drawLine((i+1)*imageWidth,j*imageHeight,(i+1)*imageWidth,(j+1)*imageHeight,paint);
}
}
通过传入list中中bitmap数据和对应的X,Y坐标绘制出相应的小图片,坐标通过image的宽高乘以i和j换算出它在原图的位置坐标,与上面切割时坐标的换算相同
然后在绘制出小图片后自然是需要绘制一下小图片周边的白线形成拼图的格局,注意一点就是当图片出于最后一列时(即i为2)时就只需要绘制下面的线
第五:怎么实现拼图移动,一想到需要在拼图时移动图片,自然是需要重写一下onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
fingerIsUp=false;
x=event.getX();
y=event.getY();
currentDownPosition=(int)(x/imageWidth)*4+(int)(y/imageHeight);
break;
case MotionEvent.ACTION_MOVE:
float moveX=event.getX();
float moveY=event.getY();
moveCurrentImage(x,y,moveX,moveY);
break;
case MotionEvent.ACTION_UP:
judgeIsChange();
moveDistance=0;
fingerIsUp=true;
relativePosition=-1;
currentDownPosition=-1;
invalidate();
checkIsWin();
break;
}
return true;
}
重写其手指落下、移动、松开事件即可,落下时需要记录下手指落下的那个图片position,然后在move事件中来重绘一下图片达到图片移动的效果
/**
* 移动当前手指落下的图片
* @param x
* @param y
* @param moveX
* @param moveY
*/
private void moveCurrentImage(float x, float y, float moveX, float moveY) {
if (Math.max(Math.abs(moveX-x),Math.abs(moveY-y))<80f||isWin)
return;
isVertical=Math.max(Math.abs(moveX-x),Math.abs(moveY-y))==Math.abs(moveY-y);
if (isVertical){
moveDirection=(moveY-y)>0?BOTTOM:TOP;
moveDistance=Math.abs(moveY-y)>imageHeight?imageHeight:(moveY-y);
if (moveDistance==imageHeight&&moveY-y<0)
moveDistance=-imageHeight;
}else {
moveDirection=(moveX-x)>0?RIGHT:LEFT;
moveDistance=(Math.abs(moveX-x)>imageWidth)?imageWidth:(moveX-x);
if (moveDistance==imageWidth&&(moveX-x)<0)
moveDistance=-imageWidth;
}
invalidate();
}
重绘主要在于计算手指的移动距离,但是在这里要注意一点,就是移动时必须按一个方向移动,垂直或者是水平,不然图片随着手指乱移动会导致下面的问题不好处理,也不适合拼图游戏的规则,所以需要判断一下移动的方向,在这里我是通过比较手指在滑动过程中,X轴与Y轴移动的距离大小来判断的,如果在X轴上的移动距离比较大那么,就把移动方向设置为水平,自然图片就是沿着水平滑动,同理Y轴的滑动距离大时沿垂直方向滑动。滑动的方向确定,然后就是计算一下滑动的距离即滑动方向上的两点X的差,或是Y的差值。在上面代码中即值得是moveDistance。
数值计算完毕则开始我们的绘制工作
for (int i=0;i<3;i++){
for (int j=0;j<4;j++){
if (i*4+j==relativePosition&&!fingerIsUp){
canvas.drawLine(i*imageWidth,(j+1)*imageHeight,(i+1)*imageWidth,(j+1)*imageHeight,paint);
if(i!=2){
canvas.drawLine((i+1)*imageWidth,j*imageHeight,(i+1)*imageWidth,(j+1)*imageHeight,paint);
}
continue;
}
if (i*4+j==currentDownPosition&&!fingerIsUp){
if (isVertical){
canvas.drawBitmap(list.get(i*4+j).getBitmap(),i*imageWidth,j*imageHeight+moveDistance,paint);
}else {
canvas.drawBitmap(list.get(i*4+j).getBitmap(),i*imageWidth+moveDistance,j*imageHeight,paint);
}
drawMoveRelative(canvas,i,j);
}else{
canvas.drawBitmap(list.get(i*4+j).getBitmap(),i*imageWidth,j*imageHeight,paint);
}
canvas.drawLine(i*imageWidth,(j+1)*imageHeight,(i+1)*imageWidth,(j+1)*imageHeight,paint);
if(i!=2){
canvas.drawLine((i+1)*imageWidth,j*imageHeight,(i+1)*imageWidth,(j+1)*imageHeight,paint);
}
}
}
绘制时只需要找出当前手指落下选中的图片下标然后绘制其位置时把moveDistance加上即可,比如改图片在水平上移动时,只需在绘制当前图片时在X轴坐标上加上moveDistance即可。
图片的移动实现了,但是还需要注意游戏规则,移动的距离不能超过一个图片,则在计算moveDistance的时候需要判断一下距离,超过一个图片的距离时让moveDistance=imageWidth或者是imageHeight。
随手指移动的图片实现好了,但是还有一个对应的图片也需要同时移动,移动的方式与上面查不多,主要的就是去获取对应的图片的position,获取的方式也比较简单。这些图片的排列是从上到下,从左到右4*3的网格状,所以在判断移动反向后,即可以计算出出对应的图片position。具体的计算代码如下所示,需要注意的就是四边的图片向外移动的时候,不要计算对应的图片position。
/**
* 当手指滑动图片移动时,绘制移动方向想对的图片位置
* @param canvas
* @param i
* @param j
*/
private void drawMoveRelative(Canvas canvas,int i,int j) {
switch (moveDirection){
case LEFT:
if (currentDownPosition!=0¤tDownPosition!=1¤tDownPosition!=2¤tDownPosition!=3){
relativePosition=currentDownPosition-4;
canvas.drawBitmap(list.get(relativePosition).getBitmap(),(i-1)*imageWidth+Math.abs(moveDistance),j*imageHeight,paint);
}else {
relativePosition=-1;
}
break;
case RIGHT:
if (currentDownPosition!=8¤tDownPosition!=9¤tDownPosition!=10¤tDownPosition!=11){
relativePosition=currentDownPosition+4;
canvas.drawBitmap(list.get(relativePosition).getBitmap(),(i+1)*imageWidth-Math.abs(moveDistance),j*imageHeight,paint);
}else {
relativePosition=-1;
}
break;
case TOP:
if (currentDownPosition!=0¤tDownPosition!=4¤tDownPosition!=8){
relativePosition=currentDownPosition-1;
canvas.drawBitmap(list.get(relativePosition).getBitmap(),i*imageWidth,(j-1)*imageHeight+Math.abs(moveDistance),paint);
}else {
relativePosition=-1;
}
break;
case BOTTOM:
if (currentDownPosition!=3¤tDownPosition!=7¤tDownPosition!=11){
relativePosition=currentDownPosition+1;
canvas.drawBitmap(list.get(relativePosition).getBitmap(),i*imageWidth,(j+1)*imageHeight-Math.abs(moveDistance),paint);
}else {
relativePosition=-1;
}
break;
}
}
基本的操作已经实现,现在需要做的就是开始编写拼图实现的部分,第一拼图时当移动的距离适当时需要交换两张图片,第二拼图开始时需要打乱顺序,第三需要判断一下是否拼图成功
判断时候需要交换小照片时,即做一个if判断moveDistance的大小,要是超过小图片的对应的宽度(或在垂直移动时对应的高度)的1/2时,交换两个的bitmap
private void judgeIsChange() {
if ((isVertical&&Math.abs(moveDistance)>imageHeight/2)|(!isVertical&&Math.abs(moveDistance)>imageWidth/2)){
if (relativePosition==-1){
return;
}
ImagePiece img1=new ImagePiece();
img1.setBitmap(list.get(currentDownPosition).getBitmap());
img1.setPosition(list.get(currentDownPosition).getPosition());
ImagePiece img2=new ImagePiece();
img2.setBitmap(list.get(relativePosition).getBitmap());
img2.setPosition(list.get(relativePosition).getPosition());
list.remove(currentDownPosition);
list.add(currentDownPosition,img2);
list.remove(relativePosition);
list.add(relativePosition,img1);
}
}
打乱顺序,则更为简单,即将存储数据的list集合打断即可
判断拼图是否成功,这里我利用了每个存储小图片对象ImagePiece中的属性position,每次移动后判断集合中的ImagePiece的position值是否与正确的排列顺序相同,相同即拼图成功
*/
private void checkIsWin() {
if (isWin){
return;
}
for (int i=0;i<list.size();i++){
if (list.get(i).getPosition()!=i){
isWin=false;
return;
}
}
isWin=true;
Toast.makeText(getContext(), "拼图完成!", Toast.LENGTH_SHORT).show();
}
写到这里,就差不多了,知识虽然简单,但是还是比较有利于初学者的思维联系,如有表达不清楚的地方,敬请谅解
下面附上源码