編輯:關於Android編程
最近在學自定義View,無意中看到鴻洋大神以前寫過的2048,看起來很不錯,所以自己在他的基礎上做一個加強版的2048。先看圖:
功能除了正常的2048外,還支持數字與圖片無縫切換而沒有任何影響,此外,圖片不是嵌在自定義View裡面的,而是開發者自己在調用時再自己添加的,如:在MainActivity裡面添加圖片,缺點是Activity被銷毀後再進入是重新開始的,不過這只是做一個demo而已,就不講究這麼多了。其實想要開發者改變更多的樣式而不用改自定義View內部的關鍵在於對外暴露的方法的多少,如你可以在自定義View裡面寫4行4列,也可以暴露一個改變行列數的方法,結果其實沒差,只是說這樣會減少對自定義View內部的直接操作。

下面這兩張圖是對應的,切換只需按一下按鈕。


下面開始挑戰2048:
一共兩個自定義View:一個容器GameLayout,一個小方格GameItem。容器主要監聽整體變化如數的變化,邏輯處理、小方格的位置等等,具體畫小方格的顏色、圖片、數字還是由小方塊自己畫,而調用的時候是對GameLayout進行操作。
1、可以用一個數組來存放小方格,數組的大小由行數決定,之後數字變化了都會對這個數組進行操作,保證每時每刻位置和數字都是對的;
/**
* 測量Layout的寬和高,以及設置Item的寬和高,這裡忽略wrap_content 以寬、高之中的最小值繪制正方形
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 獲得正方形的邊長
int length = Math.min(getMeasuredHeight(), getMeasuredWidth());
// 獲得Item的寬度
int childWidth = (length - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn;
if (!once) {
if (mItems == null) {
mItems = new GameItem[mColumn * mColumn];
}
// 放置Item
for (int i = 0; i < mItems.length; i++) {
GameItem item = new GameItem(getContext());
mItems[i] = item;
item.setId(i + 1);
RelativeLayout.LayoutParams lp = new LayoutParams(childWidth, childWidth);
// 設置橫向邊距,不是最後一列
if ((i + 1) % mColumn != 0) {
lp.rightMargin = mMargin;
}
// 如果不是第一列
if (i % mColumn != 0) {
lp.addRule(RelativeLayout.RIGHT_OF, mItems[i - 1].getId());
}
// 如果不是第一行,設置縱向邊距,非最後一行
if ((i + 1) > mColumn) {
lp.topMargin = mMargin;
lp.addRule(RelativeLayout.BELOW, mItems[i - mColumn].getId());
}
addView(item, lp);
}
//生成數字
generateNum();
}
once = true;
setMeasuredDimension(length, length);
}
2、對於手勢,為了簡單方便,我們枚舉四個方向,自己寫一個類繼承GestureDetector.SimpleOnGestureListener,在裡面判斷向那邊滑動,注釋寫的很清楚就不多說了,對於裡面的action方法,它會根據你向哪邊滑動做出響應的處理,如對小方格移動、數字的合並等等;
/**
* 運動方向的枚舉
*/
private enum ACTION {
LEFT, RIGHT, UP, DOWM
}
/**
* 根據坐標變化判斷手勢
*/
class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {
// 設置最小滑動距離
final int FLING_MIN_DISTANCE = 50;
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// 得到在X軸移動的距離
float x = e2.getX() - e1.getX();
// 得到在Y軸移動的距離
float y = e2.getY() - e1.getY();
if (x > FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) {
// 向右滑
action(ACTION.RIGHT);
} else if (x < -FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) {
// 向左滑
action(ACTION.LEFT);
} else if (y > FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) {
// 向下滑
action(ACTION.DOWM);
} else if (y < -FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) {
// 向上滑
action(ACTION.UP);
}
return true;
}
}
3、不從界面,單純從邏輯考慮,當用戶向某一方向移動時,其實就是不斷遍歷再判斷,表的遍歷需要兩重for循環,根據方向從方向的最前面開始,一個一個判斷是不是0(0表示空白),從而判斷能不能移動,然後判斷是否能合並以及設置合並後的值,之後在值為0的空白小方格中隨機選一塊產生2或4,當然,到最後無法產生隨機數就說明游戲結束了,邏輯差不多就這樣吧。
/**
* 根據用戶運動,整體進行移動合並值等
*/
private void action(ACTION action) {
// 行|列
for (int i = 0; i < mColumn; i++) {
List row = new ArrayList<>();
// 行|列
//記錄不為0的數字
for (int j = 0; j < mColumn; j++) {
// 得到下標
int index = getIndexByAction(action, i, j);
GameItem item = mItems[index];
// 記錄不為0的數字
if (item.getNumber() != 0) {
row.add(item);
}
}
//判斷是否發生移動
for (int j = 0; j < mColumn && j < row.size(); j++) {
int index = getIndexByAction(action, i, j);
GameItem item = mItems[index];
if (item.getNumber() != row.get(j).getNumber()) {
isMoveHappen = true;
}
}
// 合並相同的
mergeItem(row);
// 設置合並後的值
for (int j = 0; j < mColumn; j++) {
int index = getIndexByAction(action, i, j);
if (row.size() > j) {
mItems[index].setNumber(row.get(j).getNumber());
} else {
mItems[index].setNumber(0);
}
}
}
//生成數字
generateNum();
}
二、接下來輪到小方格了,他應該設什麼屬性呢?你可能會想到邊長吧,其實邊長是可以不用考慮的,因為容器的邊長確定了,行數確定了,內邊距也確定了,小方格的邊長也就確定了,這也符合自定義View的原則之一,能又其他屬性算出來的就直接算出來而不重復設。它的屬性應該有類型(是圖片還是數字)、數字、圖片、背景色。
/**
* 設置類型
* @param type 0為數字, 1為圖片
*/
public void setType(int type) {
this.type = type;
invalidate();
}
2、通過setNumber方法改變內容,改變時又會根據不同的數字選取不同的顏色(這些顏色是我自己一個一個試的,感覺還可以,還有就是我比較喜歡藍色的,所以你會看到demo運行後基本上界面都是藍色的),同理,圖片也是根據這個來變化的。
/**
* 得到圖片id數組,並轉換成Bitmap類型
*
* @param iamges
*/
public void setImages(int[] Images) {
this.mImages = Images;
if (mBitmaps == null) {
mBitmaps = new Bitmap[mImages.length];
for (int i = 0; i < mImages.length; i++) {
// 將圖片id轉化成Bitmap
mBitmaps[i] = BitmapFactory.decodeResource(getResources(), mImages[i]);
}
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (type == TYPE_NUMBER) {
String bgColor = null;
switch (mNumber) {
case 0:
bgColor = "#616ba1";
break;
case 2:
bgColor = "#bfc8f7";
break;
case 4:
bgColor = "#b0bbf7";
break;
case 8:
bgColor = "#9facf5";
break;
case 16:
bgColor = "#909ff4";
break;
case 32:
bgColor = "#8394f2";
break;
case 64:
bgColor = "#788bf4";
break;
case 128:
bgColor = "#6f83f2";
break;
case 256:
bgColor = "#6379f2";
break;
case 512:
bgColor = "#5971f4";
break;
case 1024:
bgColor = "#4f69f2";
break;
case 2048:
bgColor = "#3F51B5";
break;
default:
bgColor = "#8899f5";
break;
}
// 用對應的顏色充滿整個小方格
mPaint.setColor(Color.parseColor(bgColor));
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
// 如果有數字就畫出來
if (mNumber != 0) {
mPaint.setColor(Color.BLACK);
float x = (getWidth() - mBound.width()) / 2;
float y = getHeight() / 2 + mBound.height() / 2;
canvas.drawText(mNumber + "", x, y, mPaint);
}
} else {
int index = -1;
// 將數字轉換成圖片下標
switch (mNumber) {
case 2:
index = 0;
break;
case 4:
index = 1;
break;
case 8:
index = 2;
break;
case 16:
index = 3;
break;
case 32:
index = 4;
break;
case 64:
index = 5;
break;
case 128:
index = 6;
break;
case 256:
index = 7;
break;
case 512:
index = 8;
break;
case 1024:
index = 9;
break;
case 2048:
index = 10;
break;
}
// 如果沒有圖片,則直接用顏色充滿整個小方格
if (mNumber == 0) {
mPaint.setColor(Color.parseColor("#616ba1"));
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
}
// 如果有圖片就畫出來
if (mNumber != 0)
canvas.drawBitmap(mBitmaps[index], null, new Rect(0, 0, getWidth(), getHeight()), null);
}
}
三、接下來就是使用了,其實很簡單,加入xml後,在Activity 中找到控件,設置各種監聽和處理
Activity也只是簡答的判斷邏輯
package com.talentclass.numberimage2048;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.preference.Preference;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
/**
* 程序入口
*
* @author talentClass
*/
public class MainActivity extends AppCompatActivity implements GameLayout.Game2048Listener {
public static final String SCORE = "score";
/**
* 模式:false為數字,true為圖片
*/
private boolean bType;
private TextView tvScore, tvMaxScore; // 當前分數、最高分
private Button btnType, btnRestart; // 設置類型、重新開始
private GameLayout mGameLayout; // 自定義View容器
// 放置圖片的數組
private int[] mImages = {R.mipmap.image1, R.mipmap.image2, R.mipmap.image3, R.mipmap.image4, R.mipmap.image5, R.mipmap.image6,
R.mipmap.image7, R.mipmap.image8, R.mipmap.image9, R.mipmap.image10, R.mipmap.image11};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化界面
init();
}
/**
* 初始化界面
*/
private void init() {
tvScore = (TextView) findViewById(R.id.id_score);
tvMaxScore = (TextView) findViewById(R.id.id_max_score);
btnType = (Button) findViewById(R.id.id_type);
btnRestart = (Button) findViewById(R.id.id_restart);
mGameLayout = (GameLayout) findViewById(R.id.id_game2048);
mGameLayout.setOnGame2048Listener(this);
btnType.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(bType){// 如果當前是圖片模式,則此時按鈕顯示數字模式,所以點下去後,按鈕顯示圖片模式
bType = false;
btnType.setText("圖片模式");
// 設置類型為數字模式
mGameLayout.setType(GameItem.TYPE_NUMBER);
}else {// 如果當前是數字模式,則按鈕顯示圖片模式,所以點下去後,按鈕顯示數字模式
bType = true;
btnType.setText("數字模式");
// 先把圖片放進去,然後再設置類型為圖片模式
mGameLayout.setImage(mImages);
mGameLayout.setType(GameItem.TYPE_IMAGE);
}
}
});
btnRestart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveScore(tvScore.getText().toString());
// 重新開始
mGameLayout.restart();
}
});
tvMaxScore.setText(getScore());
}
/**
* 獲取最高分
*
* @return
*/
private String getScore() {
return getSharedPreferences(SCORE, MODE_PRIVATE).getString(SCORE, "0");
}
/**
* 根據得分判斷是否保存到最高分
*
* @param score
*/
private void saveScore(String score) {
// 先轉換成int類型比較大小
int now = Integer.parseInt(tvScore.getText().toString());
int max = Integer.parseInt(tvMaxScore.getText().toString());
// 如果超過最高分
if (now > max) {
tvMaxScore.setText(score);
// 保存起來,下次啟動再拿出來
SharedPreferences.Editor editor = getSharedPreferences(SCORE, MODE_PRIVATE).edit();
editor.putString(SCORE, score);
editor.commit();
}
}
@Override
public void onBackPressed() {
// 推出前先保存分數
saveScore(tvMaxScore.getText().toString());
super.onBackPressed();
}
@Override
public void onScoreChange(int score) {
tvScore.setText(score + "");
}
@Override
public void onGameOver() {
new AlertDialog.Builder(this).setTitle("游戲結束")
.setMessage("你的得分是:" + tvScore.getText())
.setPositiveButton("再來一次", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
saveScore(tvScore.getText().toString());
mGameLayout.restart();
}
})
.setNegativeButton("不玩了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 保存分數後直接退出應用
saveScore(tvScore.getText().toString());
finish();
}
}).show();
}
}

其實源代碼我注釋也寫的很詳細,大家可以下載,相信一看就懂的。
XML 文件解析總結
一.基礎概念的介紹? ??XML在各種開發中都廣泛應用,Android也不例外。作為承載數據的一個重要角色,如何讀寫XML成為Android開發中一項重要的技能。今天就由
android---UI---RecyclerView實現瀑布流(2)
前言:前面介紹了瀑布流的基本實現,實際上瀑布流還有一些事件需要監聽。比如點擊事件,下拉和上拉事件。這裡接著上次的 android—UI—Recyc
Android開發技巧二--避免在EditText中驗證日期
開發者都知道驗證表單裡的數據是令人厭煩而且容易出錯的,日期輸入框的驗證也是如此。我們可以開發出一個外觀看起來與EditText相同Button,點擊該Button後,會顯
Android使用RecyclerView和CardView,實現知乎日報精致布局
在寫博客園客戶端的時候,突然想到,弄個知乎日報風格的簡單清爽多好!不需要那麼多繁雜的信息干擾視野。先貼上效果圖,左邊是知乎日報的,右邊是本方案的 本文所使用的ide是an