安卓自定義view - 2048 小遊戲

爲了學習自定義 ViewGroup,正碰巧最近無心間玩了下 2048 的遊戲,所以這裏就來實現一個 2048 小遊戲。想必不少人應該是玩過這個遊戲的,若是沒有玩過的能夠下載玩一下。下圖是我實現的效果。git

2048 遊戲規則

遊戲規則比較簡單,共有以下幾個步驟:github

  1. 向一個方向移動,全部格子會向那個方向移動
  2. 相同的數字合併,即相加
  3. 每次移動時,空白處會隨機出現一個數字2或4
  4. 當界面不可移動時,即格子被數字填滿,遊戲結束,網格中出現 2048 的數字遊戲勝利,反之遊戲失敗。

2048 遊戲算法

算法主要是討論上下左右四個方向如何合併以及移動,這裏我以向左和向上來講明,而向下和向右就由讀者自行推導,由於十分類似。算法

向左移動算法

先來看下面兩張圖,第一張是初始狀態,能夠看到網格中有個數字 2。在這裏用二維數組來描述。它的位置應該是第2行第2列 。第二張則是它向左移動後的效果圖,能夠看到 2 已經被移動到最左邊啦!canvas

咱們最常規的想法就是首先遍歷這個二維數組,找到這個數的位置,接着合併和移動。因此第一步確定是循環遍歷。數組

int i;
for (int x = 0; x < 4; x++) {
    for (int y = 0; y < 4; ) {
        Model model = models[x][y];
        int number = model.getNumber();
        if (number == 0) {
            y++;
            continue;
        } else {
        // 找到不爲零的位置. 下面就須要進行合併和移動處理
         
        }
    }
 }

複製代碼

上面的代碼很是簡單,這裏引入了 Model 類,這個類是封裝了網格單元的數據和網格視圖。定義以下:bash

public class Model {

    private int number;
    /**
     *  單元格視圖.
     */
    private CellView cellView;

    public Model(int number, CellView cellView) {
        this.number = number;
        this.cellView = cellView;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public CellView getCellView() {
        return cellView;
    }

    public void setCellView(CellView cellView) {
        this.cellView = cellView;
    }
}

複製代碼

先不糾結視圖的繪製,咱們先把算法理清楚,算法搞明白了也就解決一大部分了,其餘就是自定義 View 的知識。上述的過程就是,遍歷整個網格,找到不爲零的網格位置。dom

讓咱們來思考一下,合併要作什麼,那麼咱們再來看一張圖。ide

從這張圖中咱們能夠看到在第一行的最後兩個網格單元都是2,當向左移動時,根據 2048 遊戲規則,咱們須要將後面的一個2 和前面的 2 進行合併(相加)運算。是否是能夠推理,咱們找到第一個不爲零的數的位置,而後找到它右邊第一個不爲零的數,判斷他們是否相等,若是相等就合併。算法以下:佈局

int i;
for (int x = 0; x < 4; x++) {
    for (int y = 0; y < 4; ) {
        Model model = models[x][y];
        int number = model.getNumber();
        if (number == 0) {
            y++;
            continue;
        } else {
            // 找到不爲零的位置. 下面就須要進行合併和移動處理
            // 這裏的 y + 1 就是找到這個數的右側
            for (i = y + 1; i < 4; i++) {
                if (models[x][i].getNumber() == 0) {
                    continue;
                } else if (models[x][y].getNumber() == models[x][i].getNumber()) {
                    // 找到相等的數
                    // 合併,相加操做
                    models[x][y].setNumber(
                    models[x][y].getNumber() + models[x][i].getNumber())
                    
                    // 將這個數清0
                    models[x][i].setNumber(0);
                    
                    break;
                } else {
                    break;
                }
            }
            
            // 防止陷入死循環,因此必需要手動賦值,將其跳出。
            y = i;
        }
    }
 }

複製代碼

經過上面的過程,咱們就將這個數右側的第一個相等的數進行了合併操做,是否是也好理解的。不理解的話能夠在草稿紙上多畫一畫,多推導幾回。post

搞定了合併操做,如今就是移動了,移動確定是要將全部數據的單元格都移動到左側,移動的條件是,找到第一個不爲零的數的座標,繼續向前找到第一個數據爲零即空白單元格的位置,將數據覆蓋它,並將後一個單元格數據清空。算法以下:

for (int x = 0; x < 4; x++) {
    for (y = 0; y < 4; y++) {
        if (models[x][y].getNumber() == 0) {
            continue;
        } else {
            // 找到當前數前面爲零的位置,即空格單元
            for (int j = y; j >= 0 && models[x][j - 1].getNumber() == 0; j--) {
                // 數據向前移動,即數據覆蓋.
                models[j - 1][y].setNumber(
                models[j][y].getNumber())
                // 清空數據
                models[j][y].setNumber(0)
            }
        }
    }
}

複製代碼

到此向左移動算法完畢,接着就是向上移動的算法。

向上移動算法

有了向左移動的算法思惟,理解向上的操做也就變得容易一些啦!首先咱們先來看合併,合併的條件也就是找到第一個不爲零的數,而後找到它下一行第一個不爲零且相等的數進行合併。算法以下:

int i = 0;
for (int y = 0; y < 4; y++) {
    for (x = 3; x >= 0; ) {
        if (models[x][y].getNumber() == 0) {
            continue;
        } else {
            for (i = x + 1; i < 4; i++) {
                if (models[i][y].getNumber() == 0) {
                    continue;
                } else if (models[x][y].getNumber() == models[i][y].getNumber()) {
                   models[x][y].setNumber(
                    models[x][y].getNumber() + models[i][y].getNumber();
                   )
                   
                   models[i][y].setNumber(0);
                   
                   break; 
                } else {
                    break;
                }
            }
        }
    }
}

複製代碼

移動的算法也相似,即找到第一個不爲零的數前面爲零的位置,即空格單元的位置,將數據覆蓋並將後一個單元格的數據清空。

for (int x = 0; x < 4; x++) {
    for (int y = 0; y < 4; y++) {
        if (models[x][y].getNumber() == 0) {
            continue;
        } else {
            for (int j = x; x > 0 && models[j - 1][y].getNumber() == 0; j--) {
                models[j -1][y].setNumber(models[j][y].getNumber());
                
                models[j][y].setNumber(0);
            }
        }
    }
}

複製代碼

到此,向左移動和向上移動的算法就描述完了,接下來就是如何去繪製視圖邏輯啦!

網格單元繪製

首先先忽略數據源,咱們只是單純的繪製網格,有人可能說了咱們不用自定義的方式也能實現,我只想說能夠,可是不推薦。若是使用自定義 ViewGroup,將每個小的單元格做爲單獨的視圖。這樣擴展性更好,好比我作了對隨機顯示的單元格加上動畫。

既然是自定義 ViewGroup, 那咱們就建立一個類並繼承 ViewGroup,其定義以下:

public class Play2048Group extends ViewGroup {

    public Play2048Group(Context context) {
        this(context, null);
    }

    public Play2048Group(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    
     @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        ......
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        .....
    }

}

複製代碼

咱們要根據子視圖的大小來測量容器的大小,在 onLayout 中擺放子視圖。爲了更好的交給其餘開發者使用,咱們儘可能可讓 view 能被配置。那麼就要自定義屬性。

  1. 自定義屬性

這裏只是提供了設置網格單元行列數,其實這裏我我只取兩個值的最大值做爲行列的值。

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="Play2048Group">
        <attr name="row" format="integer"/>
        <attr name="column" format="integer"/>
    </declare-styleable>


</resources>

複製代碼
  1. 佈局中加載自定義屬性

能夠看到將傳入的 row 和 column 取大的做爲行列數。

public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Play2048Group);

        try {
            mRow = a.getInteger(R.styleable.Play2048Group_row, 4);
            mColumn = a.getInteger(R.styleable.Play2048Group_column, 4);
            // 保持長寬相等排列, 取傳入的最大值
            if (mRow > mColumn) {
                mColumn = mRow;
            } else {
                mRow = mColumn;
            }

            init();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            a.recycle();
        }
    }

複製代碼
  1. 網格子視圖

由於整個網格有一個個網格單元組成,其中每個網格單元都是一個 view, 這個 view 其實也就只是繪製了一個矩形,而後在矩形的中間繪製文字。考慮文章篇幅,我這裏只截取 onMeasure 和 onDraw 方法。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 我這裏直接寫死了,固然爲了屏幕適配,這個值應該由外部傳入的,
        // 這裏就當我留下的做業吧 😄
        setMeasuredDimension(130, 130);
    }

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 繪製矩形.
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

        // 若是當前單元格的數據不爲0,就繪製。
        // 若是爲零,就使用背景的顏色做爲畫筆繪製,這麼作就是爲了避免讓它顯示出來😳
        if (!mNumber.equalsIgnoreCase("0")) {
            mTextPaint.setColor(Color.parseColor("#E451CD"));
            canvas.drawText(mNumber,
                    (float) (getMeasuredWidth() - bounds.width()) / 2,
                    (float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
        } else {
            mTextPaint.setColor(Color.parseColor("#E4CDCD"));
            canvas.drawText(mNumber,
                    (float) (getMeasuredWidth() - bounds.width()) / 2,
                    (float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
        }
    }


複製代碼
  1. 測量容器視圖

因爲網格是行列數都相等,則寬和高都相等。那麼全部的寬加起來除以 row, 全部的高加起來除以 column 就獲得了最終的寬高, 不過記得要加上邊距。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = 0;
        int height = 0;

        int count = getChildCount();
        
        MarginLayoutParams layoutParams = 
        (MarginLayoutParams)getChildAt(0).getLayoutParams();
        
        // 每個單元格都有左邊距和上邊距
        int leftMargin = layoutParams.leftMargin;
        int topMargin = layoutParams.topMargin;

        for (int i = 0; i < count; i++) {
            CellView cellView = (CellView) getChildAt(i);
            cellView.measure(widthMeasureSpec, heightMeasureSpec);

            int childW = cellView.getMeasuredWidth();
            int childH = cellView.getMeasuredHeight();

            width += childW;
            height += childH;
        }

        // 須要加上每一個單元格的左邊距和上邊距
        setMeasuredDimension(width / mRow + (mRow + 1) * leftMargin,
                height / mRow + (mColumn + 1) * topMargin);
    }

複製代碼
  1. 佈局子視圖(網格單元)

佈局稍微麻煩點,主要是在換行處的計算有點繞。首先咱們找一下何時是該換行了,若是是 4 * 4 的 16 宮格,咱們能夠知道每一行的開頭應該是 0、四、八、12,若是要用公式來表示的就是: temp = mRow * (i / mRow), 這裏的 mRow 爲行數,i 爲索引。

咱們這裏首先就是要肯定每一行的第一個視圖的位置,後面的視圖就好肯定了, 下面是推導過程:

第一行: 
    網格1: 
        left = lefMargin; 
        top = topMargin; 
        right = leftMargin + width; 
        bottom = topMargin + height;
        
    網格2: 
        left = leftMargin + width + leftMargin
        top = topMargin;
        right = leftMargin + width + leftMargin + width
        bottom = topMargin + height
        
    網格3: 
        left = leftMargin + width + leftMargin + width + leftMargin
        right = leftMargin + width + leftMargin + width + leftMargin + width
    
    ...    
 第二行: 
    網格1:
        left = leftMargin
        top = topMargin + height
        right = leftMargin + width
        bottom = topMargin + height + topMargin + height
        
    網格2: 
        left = leftMargin + width + leftMargin
        top = topMargin + height + topMargin
        right = leftMargin + width + lefMargin + width 
        bottom = topMargin + height + topMargin + height
        
        
上面的應該很簡單的吧,這是根據畫圖的方式直觀的排列,咱們能夠概括總結,找出公式。

除了每一行的第一個單元格的 left, right 都相等。 其餘的能夠用一個公式來總結:

left = leftMargin * (i - temp + 1) + width * (i - temp)
right = leftMargin * (i - temp + 1) + width * (i - temp + 1)

能夠隨意帶數值進入而後對比畫圖看看結果,好比(1, 1) 即第二行第二列。

temp = row * (i / row)  => 4 * 1 = 4

left = leftMargin * (5 - 4 + 1) + width * (5 - 4)
     =  leftMargin * 2 + width
     
right = leftMargin * (5 - 4 + 1) + width * (5 - 4 + 1)
      = lefMargin * 2 + width * 2
      
和上面的手動計算徹底同樣,至於爲何 i = 5, 那是由於 i 循環到第二行的第二列爲 5


除了第一行第一個單元格其餘的 top, bottom 能夠用公式:

top = height * row + topMargin * row + topMargin
bottom = height * (row + 1) + topMargin(row + 1)

複製代碼
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            CellView cellView = (CellView) getChildAt(i);
            MarginLayoutParams layoutParams = (MarginLayoutParams) cellView.getLayoutParams();
            int leftMargin = layoutParams.leftMargin;
            int topMargin = layoutParams.topMargin;

            int width = cellView.getMeasuredWidth();
            int height = cellView.getMeasuredHeight();

            int left = 0, top = 0, right = 0, bottom = 0;

            // 每一行開始, 0, 4, 8, 12...
            int temp = mRow * (i / mRow);
            // 每一行的開頭位置.
            if (i == temp) {
                left = leftMargin;
                right = width + leftMargin;
            } else {
                left = leftMargin * (i - temp + 1) + width * (i - temp);
                right = leftMargin * (i - temp + 1) + + width * (i - temp + 1);
            }

            int row = i / mRow;
            if (row == 0) {
                top = topMargin;
                bottom = height + topMargin;
            } else {
                top = height * row + topMargin * row + topMargin;
                bottom = height * (row + 1) + (row + 1) * topMargin;
            }

            cellView.layout(left, top, right, bottom);
        }
    }

複製代碼
  1. 初始數據
private void init() {
        models = new Model[mRow][mColumn];
        cells = new ArrayList<>(mRow * mColumn);

        for (int i = 0; i < mRow * mColumn; i++) {
            CellView cellView = new CellView(getContext());
            MarginLayoutParams params = new MarginLayoutParams(
                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

            params.leftMargin = 10;
            params.topMargin = 10;
            cellView.setLayoutParams(params);
            
            Model model = new Model(0, cellView);
            cells.add(model);

            addView(cellView, i);
        }
    }

複製代碼

以上就是未帶數據源的宮格繪製過程,接下來開始接入數據源來動態改變宮格的數據啦!

動態改變數據

  1. 初始化數據源,隨機顯示一個數據 2
private void init() {
        ... 省略部分代碼.....
        
        int i = 0;
        for (int x = 0; x < mRow; x++) {
            for (int y = 0; y < mColumn; y++) {
                models[x][y] = cells.get(i);
                i++;
            }
        }

        // 生成一個隨機數,初始化數據.
        mRandom = new Random();
        rand = mRandom.nextInt(mRow * mColumn);
        Model model = cells.get(rand);
        model.setNumber(2);
        CellView cellView = model.getCellView();
        cellView.setNumber(2);

        // 初始化時空格數爲總宮格個數 - 1
        mAllCells = mRow * mColumn - 1;
        
        // 程序動態變化這個值,用來判斷當前宮格還有多少空格可用.
        mEmptyCells = mAllCells;

        
     ... 省略部分代碼.....
    }

複製代碼
  1. 計算隨機數生成的合法單元格位置

生成的隨機數據必須在空白的單元格上。

private void nextRand() {
        // 若是全部宮格被填滿則遊戲結束, 
        // 固然這裏也有坑,至於怎麼發現,你多玩幾回機會發現,
        // 這個坑我就不填了,有興趣的能夠幫我填一下😄😄
        if (mEmptyCells <= 0) {
            findMaxValue();
            gameOver();
            return;
        }

        int newX, newY;

        if (mEmptyCells != mAllCells || mCanMove == 1) {
            do {
                // 經過僞隨機數獲取新的空白位置
                newX = mRandom.nextInt(mRow);
                newY = mRandom.nextInt(mColumn);
            } while (models[newX][newY].getNumber() != 0);

            int temp = 0;

            do {
                temp = mRandom.nextInt(mRow);
            } while (temp == 0 || temp == 2);

            Model model = models[newX][newY];
            model.setNumber(temp + 1);
            CellView cellView = model.getCellView();
            cellView.setNumber(model.getNumber());
            playAnimation(cellView);

            // 空白格子減1
            mEmptyCells--;
        }
    }

複製代碼
  1. 向左移動

算法是咱們前面推導的,最後調用 drawAll() 繪製單元格文字, 以及調用 nextRand() 生成新的隨機數。

public void left() {
        if (leftRunnable == null) {
            leftRunnable = new Runnable() {
                @Override
                public void run() {
                    int i;
                    for (int x = 0; x < mRow; x++) {
                        for (int y = 0; y < mColumn; ) {
                            Model model = models[x][y];
                            int number = model.getNumber();
                            if (number == 0) {
                                y++;
                                continue;
                            } else {
                                // 找到不爲零的位置. 日後找不爲零的數進行運算.
                                for (i = y + 1; i < mColumn; i++) {
                                    Model model1 = models[x][i];
                                    int number1 = model1.getNumber();
                                    if (number1 == 0) {
                                        continue;
                                    } else if (number == number1) {
                                        // 若是找到和這個相同的,則進行合併運算(相加)。
                                        int temp = number + number1;
                                        model.setNumber(temp);
                                        model1.setNumber(0);

                                        mEmptyCells++;
                                        break;
                                    } else {
                                        break;
                                    }
                                }

                                y = i;
                            }
                        }
                    }

                    for (int x = 0; x < mRow; x++) {
                        for (int y = 0; y < mColumn; y++) {
                            Model model = models[x][y];
                            int number = model.getNumber();
                            if (number == 0) {
                                continue;
                            } else {
                                for (int j = y; (j > 0) && models[x][j - 1].getNumber() == 0; j--) {
                                    models[x][j - 1].setNumber(models[x][j].getNumber());
                                    models[x][j].setNumber(0);

                                    mCanMove = 1;
                                }
                            }
                        }
                    }

                    drawAll();
                    nextRand();
                }
            };
        }

        mExecutorService.execute(leftRunnable);
    }
複製代碼
  1. 隨機單元格動畫
private void playAnimation(final CellView cellView) {
        mainHandler.post(new Runnable() {
            @Override
            public void run() {
                ObjectAnimator animator = ObjectAnimator.ofFloat(
                        cellView, "alpha", 0.0f, 1.0f);
                animator.setDuration(300);
                animator.start();
            }
        });
    }

複製代碼

寫到這兒就寫完了, 至於要看完整代碼的請點擊此處。 總結一下吧!其實核心就是上下左右的合併和移動算法。至於自定義 ViewGroup,這裏並不複雜,但也將自定義 view 的幾個流程包含進來了。

相關文章
相關標籤/搜索