开发界面布局
本程序将会使用一个RelativeLayout作为整体的界面布局元素,界面布局的上面是一个自定义组件,下面是一个水平排列的LinearLayout。
程序清单:codes\18\Link\res\layout\main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/room">
<!-- 游戏主界面的自定义组件 -->
<org.crazyit.link.view.GameView
android:id="@+id/gameView"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<!-- 水平排列的LinearLayout -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
android:layout_marginTop="380px"
android:background="#1e72bb"
android:gravity="center">
<!-- 控制游戏开始的按钮 -->
<Button
android:id="@+id/startButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_selector" />
<!-- 显示游戏剩余时间的文本框 -->
<TextView
android:id="@+id/timeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20dip"
android:width="150px"
android:textColor="#ff9" />
</LinearLayout>
</RelativeLayout>
这个界面布局很简单,指定按钮的背景色时使用了@drawable/button_selector,这是一个在res\drawable目录下配置的StateListDrawable对象,配置文件代码如下。
程序清单:codes\18\Link\res\drawable-mdpi\button_selector.xml
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 指定按钮按下时的图片 -->
<item android:state_pressed="true"
android:drawable="@drawable/start_down"
/>
<!-- 指定按钮松开时的图片 -->
<item android:state_pressed="false"
android:drawable="@drawable/start"
/>
</selector>
其中GameView只是一个View的普通子类,开发了上面的界面布局文件之后,运行该程序将可以看到如图18.3所示的界面。
开发游戏界面组件
本游戏的界面组件采用了一个自定义View:GameView,它从View基类派生而出,这个自定义View的功能就是根据游戏状态来绘制游戏界面上的全部方块。
为了开发这个GameView,本程序还提供了一个Piece类,一个Piece对象代表游戏界面上的一个方块,它除了封装方块上的图片之外,还需要封装该方块代表二维数组中的哪个元素;也需要封装它的左上角在游戏界面中X、Y坐标。图18.4示意了方块左上角的X、Y坐标的作用。
方块左上角的X、Y坐标可决定它的绘制位置,GameView根据这两个坐标值绘制全部方块即可。下面是该程序中Piece类的代码。
程序清单:codes\18\Link\src\org\crazyit\link\view\Piece.java
public class Piece
{
// 保存方块对象的所对应的图片
private PieceImage image;
// 该方块的左上角的x坐标
private int beginX;
// 该方块的左上角的y坐标
private int beginY;
// 该对象在Piece[][]数组中第一维的索引值
private int indexX;
// 该对象在Piece[][]数组中第二维的索引值
private int indexY;
// 只设置该Piece对象在棋盘数组中的位置
public Piece(int indexX , int indexY)
{
this.indexX = indexX;
this.indexY = indexY;
}
public int getBeginX()
{
return beginX;
}
public void setBeginX(int beginX)
{
this.beginX = beginX;
}
// 下面省略了各属性的setter和getter方法
...
// 判断两个Piece上的图片是否相同
public boolean isSameImage(Piece other)
{
if (image == null)
{
if (other.image != null)
return false;
}
// 只要Piece封装图片ID相同,即可认为两个Piece相等
return image.getImageId() == other.image.getImageId();
}
}
上面的Piece类中封装的PieceImage代表了该方块上的图片,但此处并未直接使用Bitmap对象来代表方块上的图片——因为我们需要使用PieceImage来封装两个信息:
Ø Bitmap对象。
Ø 图片资源的ID。
其中Bitmap对象用于在游戏界面上绘制方块;而图片资源的ID则代表了该Piece对象的标识,当两个Piece所封装的图片资源的ID相等时,即可认为这两个Piece上的图片相同。如以上程序中粗体字代码所示。
下面是PieceImage类的代码。
程序清单:codes\18\Link\src\org\crazyit\link\view\PieceImage.java
public class PieceImage
{
private Bitmap image;
private int imageId;
// 有参数的构造器
public PieceImage(Bitmap image, int imageId)
{
super();
this.image = image;
this.imageId = imageId;
}
// 省略了各属性的setter和getter方法
...
}
GameView主要就是根据游戏的状态数据来绘制界面上的方块,GameView继承了View组件,重写了View组件上onDraw(Canvas canvas)方法,重写该方法主要就是绘制游戏里剩余的方块;除此之外,它还会负责绘制连接方块的连接线。
GamaView的代码如下。
程序清单:codes\18\Link\src\org\crazyit\link\view\GameView.java
public class GameView extends View
{
// 游戏逻辑的实现类
private GameService gameService; //①
// 保存当前已经被选中的方块
private Piece selectedPiece;
// 连接信息对象
private LinkInfo linkInfo;
private Paint paint;
// 选中标识的图片对象
private Bitmap selectImage;
public GameView(Context context, AttributeSet attrs)
{
super(context, attrs);
this.paint = new Paint();
// 设置连接线的颜色
this.paint.setColor(Color.RED);
// 设置连接线的粗细
this.paint.setStrokeWidth(3);
this.selectImage = ImageUtil.getSelectImage(context);
}
public void setLinkInfo(LinkInfo linkInfo)
{
this.linkInfo = linkInfo;
}
public void setGameService(GameService gameService)
{
this.gameService = gameService;
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
if (this.gameService == null)
return;
Piece[][] pieces = gameService.getPieces(); //②
if (pieces != null)
{
// 遍历pieces二维数组
for (int i = 0; i < pieces.length; i++)
{
for (int j = 0; j < pieces[i].length; j++)
{
// 如果二维数组中该元素不为空(即有方块),将这个方块的图片画出来
if (pieces[i][j] != null)
{
// 得到这个Piece对象
Piece piece = pieces[i][j];
// 根据方块左上角X、Y坐标绘制方块
canvas.drawBitmap(piece.getImage().getImage(),
piece.getBeginX(), piece.getBeginY(), null);
}
}
}
}
// 如果当前对象中有linkInfo对象, 即连接信息
if (this.linkInfo != null)
{
// 绘制连接线
drawLine(this.linkInfo, canvas);
// 处理完后清空linkInfo对象
this.linkInfo = null;
}
// 画选中标识的图片
if (this.selectedPiece != null)
{
canvas.drawBitmap(this.selectImage, this.selectedPiece.
getBeginX(),
this.selectedPiece.getBeginY(), null);
}
}
// 根据LinkInfo绘制连接线的方法
private void drawLine(LinkInfo linkInfo, Canvas canvas)
{
// 获取LinkInfo中封装的所有连接点
List<Point> points = linkInfo.getLinkPoints();
// 依次遍历linkInfo中的每个连接点
for (int i = 0; i < points.size() - 1; i++)
{
// 获取当前连接点与下一个连接点
Point currentPoint = points.get(i);
Point nextPoint = points.get(i + 1);
// 绘制连线
canvas.drawLine(currentPoint.x, currentPoint.y,
nextPoint.x, nextPoint.y, this.paint);
}
}
// 设置当前选中方块的方法
public void setSelectedPiece(Piece piece)
{
this.selectedPiece = piece;
}
// 开始游戏方法
public void startGame()
{
this.gameService.start();
this.postInvalidate();
}
}
上面的GameView中第一段粗体字代码用于根据游戏的状态数据来绘制界面中的所有方块,第二段粗体字代码则用于根据LinkInfo来绘制两个方块之间的连接线。
上面的程序中①号代码处定义了GameService对象,②号代码则调用了GameService的getPieces()方法来获取游戏中剩余的方块,GameService是游戏的业务逻辑实现类。后面会详细介绍该类的实现,此处暂不讲解。
处理方块之间的连接线
LinkInfo是一个非常简单的工具类,它用于封装两个方块之间的连接信息——其实就是封装一个List,List里保存了连接线需要经过的点。
在实现LinkInfo对象之前,先来分析两个方块可以相连的情形。连连看游戏的规则约定:两个方块之间最多只能用3条线段相连,也就是说最多只能有2个“拐点”,加上两个方块的中心,方块的连接信息最多只需要4个连接点。图18.5显示了允许出现的连接情况。
考虑到LinkInfo最多需要封装4个连接点,最少需要封装2个连接点,因此程序定义如下LinkInfo类。
程序清单:codes\18\Link\src\org\crazyit\link\object\LinkInfo.java
public class LinkInfo
{
// 创建一个集合用于保存连接点
private List<Point> points = new ArrayList<Point>();
// 提供第一个构造器, 表示两个Point可以直接相连, 没有转折点
public LinkInfo(Point p1, Point p2)
{
// 加到集合中去
points.add(p1);
points.add(p2);
}
// 提供第二个构造器, 表示三个Point可以相连, p2是p1与p3之间的转折点
public LinkInfo(Point p1, Point p2, Point p3)
{
points.add(p1);
points.add(p2);
points.add(p3);
}
// 提供第三个构造器, 表示四个Point可以相连, p2, p3是p1与p4的转折点
public LinkInfo(Point p1, Point p2, Point p3, Point p4)
{
points.add(p1);
points.add(p2);
points.add(p3);
points.add(p4);
}
// 返回连接集合
public List<Point> getLinkPoints()
{
return points;
}
}
LinkInfo中所用的Point代表一个点,程序直接使用了android.graphics.Point类,每个Point封装了该点的X、Y坐标。