Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發自定義View實現數字與圖片無縫切換的2048

Android開發自定義View實現數字與圖片無縫切換的2048

編輯:關於Android編程

最近在學自定義View,無意中看到鴻洋大神以前寫過的2048,看起來很不錯,所以自己在他的基礎上做一個加強版的2048。先看圖:

 

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

\

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

\

\

下面開始挑戰2048:

 

一共兩個自定義View:一個容器GameLayout,一個小方格GameItem。容器主要監聽整體變化如數的變化,邏輯處理、小方格的位置等等,具體畫小方格的顏色、圖片、數字還是由小方塊自己畫,而調用的時候是對GameLayout進行操作。
寫自定義View的第一步:分析有什麼屬性。
一、容器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的原則之一,能又其他屬性算出來的就直接算出來而不重復設。它的屬性應該有類型(是圖片還是數字)、數字、圖片、背景色。
1、默認類型是數字,可以用setType方法改變模式;

 

 

    /**
     * 設置類型
     * @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();
    }
}
\

 

其實源代碼我注釋也寫的很詳細,大家可以下載,相信一看就懂的。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved