編輯:關於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二、接下來輪到小方格了,他應該設什麼屬性呢?你可能會想到邊長吧,其實邊長是可以不用考慮的,因為容器的邊長確定了,行數確定了,內邊距也確定了,小方格的邊長也就確定了,這也符合自定義View的原則之一,能又其他屬性算出來的就直接算出來而不重復設。它的屬性應該有類型(是圖片還是數字)、數字、圖片、背景色。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(); }
/** * 設置類型 * @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在各種開發中都廣泛應用,Android也不例外。作為承載數據的一個重要角色,如何讀寫XML成為Android開發中一項重要的技能。今天就由
前言:前面介紹了瀑布流的基本實現,實際上瀑布流還有一些事件需要監聽。比如點擊事件,下拉和上拉事件。這裡接著上次的 android—UI—Recyc
開發者都知道驗證表單裡的數據是令人厭煩而且容易出錯的,日期輸入框的驗證也是如此。我們可以開發出一個外觀看起來與EditText相同Button,點擊該Button後,會顯
在寫博客園客戶端的時候,突然想到,弄個知乎日報風格的簡單清爽多好!不需要那麼多繁雜的信息干擾視野。先貼上效果圖,左邊是知乎日報的,右邊是本方案的 本文所使用的ide是an