看了极客的安卓2048的开发教程,大概了解了一下思路,然后自己就开始写了。后来发现这个设计思路不是太好,不方便加移动动画,就只加了创建卡片和合并的动画,不过用来练手还可以。
游戏截图如下:
如果想拷贝到本地运行的话,注意修改和包名相关的地方
或者在创建工程的时候按照以下命名:
项目名: Game2048
包名: pers.hurric.game2048
AndroidManifest.xml
仅需设置screenOrientation
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="pers.hurric.game2048"><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><activity android:name=".MainActivity"android:screenOrientation="portrait"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/textScore"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/score"android:textColor="@color/colorPrimary"android:textSize="24sp"android:textStyle="bold"app:layout_constraintBottom_toTopOf="@+id/guideline4"app:layout_constraintEnd_toStartOf="@+id/guideline2"app:layout_constraintStart_toStartOf="@+id/guideline6"app:layout_constraintTop_toTopOf="@+id/guideline3" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"app:layout_constraintGuide_percent="0.5" /><Buttonandroid:id="@+id/buttonReplay"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/button_replay"android:textColor="@android:color/holo_blue_light"app:layout_constraintBottom_toTopOf="@+id/guideline4"app:layout_constraintEnd_toStartOf="@+id/guideline7"app:layout_constraintStart_toStartOf="@+id/guideline2"app:layout_constraintTop_toTopOf="@+id/guideline3" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline3"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="horizontal"app:layout_constraintGuide_percent="0.12" /><TextViewandroid:id="@+id/textHighestScore"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/highest_score"android:textColor="@android:color/holo_purple"android:textSize="18sp"android:textStyle="italic"app:layout_constraintBottom_toTopOf="@+id/guideline3"app:layout_constraintEnd_toStartOf="@+id/guideline7"app:layout_constraintStart_toStartOf="@+id/guideline2"app:layout_constraintTop_toTopOf="parent" /><pers.hurric.game2048.GameViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"app:layout_constraintBottom_toTopOf="@+id/guideline5"app:layout_constraintEnd_toStartOf="@+id/guideline7"app:layout_constraintStart_toStartOf="@+id/guideline6"app:layout_constraintTop_toTopOf="@+id/guideline4" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline4"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="horizontal"app:layout_constraintGuide_percent="0.26" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline5"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="horizontal"app:layout_constraintGuide_percent="0.9" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline6"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"app:layout_constraintGuide_percent="0.05109489" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline7"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"app:layout_constraintGuide_percent="0.95" />
</androidx.constraintlayout.widget.ConstraintLayout>
string.xml
<resources><string name="app_name">Game2048</string><string name="score">Score : 0</string><string name="highest_score">Highest Score : 0</string><string name="button_replay">Restart</string>
</resources>
color.xml
<?xml version="1.0" encoding="utf-8"?>
<resources><color name="colorPrimary">#008577</color><color name="colorPrimaryDark">#00574B</color><color name="colorAccent">#D81B60</color><color name="gameViewBackgroundColor">#D2B48C</color><color name="backgroundColor0">#DEB887</color><color name="textColor0">#00000000</color><color name="textColor2">#333300</color><color name="textColor4">#333300</color><color name="textColorCommon">#FFFAFA</color><color name="backgroundColor2">#FDF5E6</color><color name="backgroundColor4">#FFE4B5</color><color name="backgroundColor8">#FFE4C4</color><color name="backgroundColor16">#FFDAB9</color><color name="backgroundColor32">#FF7F50</color><color name="backgroundColor64">#FF4500</color><color name="backgroundColor128">#FF8C00</color><color name="backgroundColor256">#FFD700</color><color name="backgroundColor512">#9ACD32</color><color name="backgroundColor1024">#483D8B</color><color name="backgroundColor2048">#4B0082</color><color name="backgroundColorBiggerThan2048">#000000</color>
</resources>
MainActivity.java
package pers.hurric.game2048;import androidx.appcompat.app.AppCompatActivity;import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;public class MainActivity extends AppCompatActivity {private final String FILE_NAME = "MyData";private final String HIGHEST_SCORE = "highest_score";private TextView textScore;private TextView textHighestScore;private Button buttonReplay;private int score = 0;private int highestScore = 0;public static MainActivity mainActivity;public MainActivity(){mainActivity = this;}public void addScore(int score) {this.score += score;textScore.setText("Score : " + this.score);//更新最高分updateHighestScore(this.score);}private void updateHighestScore(int score){if(score > highestScore){highestScore = score;textHighestScore.setText("HighestScore : " + score);//存储最高分SharedPreferences shp = getSharedPreferences(FILE_NAME, MODE_PRIVATE);SharedPreferences.Editor editor = shp.edit();editor.putInt(HIGHEST_SCORE, highestScore);editor.apply();}}public void clearScore(){score = 0;textScore.setText("Score : " + 0);}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);textScore = findViewById(R.id.textScore);textHighestScore = findViewById(R.id.textHighestScore);buttonReplay = findViewById(R.id.buttonReplay);//读取最高分SharedPreferences shp = getSharedPreferences(FILE_NAME, MODE_PRIVATE);highestScore = shp.getInt(HIGHEST_SCORE, 0);textHighestScore.setText("HighestScore : " + highestScore);buttonReplay.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {GameView.gameView.replayGame();}});}private boolean isExit = false;class ExitHandler extends Handler{@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);if(msg.what == 0){isExit = false;//修改状态为退出}}};ExitHandler mHandler = new ExitHandler();@Overridepublic boolean onKeyDown(int keyCode, KeyEvent event) {if(keyCode == KeyEvent.KEYCODE_BACK){if(!isExit){isExit = true;Toast.makeText(this, "再按一次退出游戏", Toast.LENGTH_SHORT).show();//延迟更改状态信息mHandler.sendEmptyMessageDelayed(0, 2000);}else{finish();}}return false;}
}
GameView.java
自定义游戏面板
package pers.hurric.game2048;import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.view.animation.ScaleAnimation;
import android.widget.GridLayout;
import android.widget.Toast;import java.util.Random;public class GameView extends GridLayout {public static GameView gameView;private Card[][] cards = new Card[4][4];public GameView(Context context) {super(context);gameView = this;initGame();}public GameView(Context context, AttributeSet attrs) {super(context, attrs);gameView = this;initGame();}public GameView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);gameView = this;initGame();}public void initGame(){this.setBackgroundColor(getResources().getColor(R.color.gameViewBackgroundColor));setColumnCount(4);int cardWidth = GetCardWidth();addCards(cardWidth, cardWidth);randomCreateCard(2);setListener();}public void replayGame(){MainActivity.mainActivity.clearScore();for(int i = 0; i < 4; ++i){for(int j = 0; j < 4; ++j){cards[i][j].setNum(0);}}randomCreateCard(2);}/** 监听Touch事件*/private void setListener(){setOnTouchListener(new OnTouchListener() {private float staX, staY, endX, endY;@Overridepublic boolean onTouch(View v, MotionEvent event) {switch(event.getAction()){case MotionEvent.ACTION_DOWN:staX = event.getX();staY = event.getY();break;case MotionEvent.ACTION_UP:endX = event.getX();endY = event.getY();boolean swiped = false;//记录是否有效滑动了//水平移动更多if(Math.abs(endX - staX) > Math.abs(endY - staY)){if(endX - staX > 10){if(swipeRight()){swiped = true;}}else if(endX - staX < -10){if(swipeLeft()){swiped = true;}}}else{if(endY - staY < -10){if(swipeUp()){swiped = true;}}else if(endY - staY > 10){if(swipeDown()){swiped = true;}}}//滑动后创建新块,并检查当前状态是否能滑动if(swiped){randomCreateCard(1);if(!canSwipe()){gameOver();}}break;}return true;}});}/** 返回该次滑动是否有效(有卡片移动或合并)*/private boolean swipeUp(){boolean flag = false;for(int j = 0; j < 4; ++j){int ind = 0;//从上往下依次处理for(int i = 1; i < 4; ++i){//如果是存在数字的,往上遍历if(cards[i][j].getNum() != 0){for(int ii = i - 1; ii >= ind; --ii){//如果这块是空的,将数字上移if(cards[ii][j].getNum() == 0){cards[ii][j].setNum(cards[i][j].getNum());cards[i][j].setNum(0);i--;//上移flag = true;}//如果这块是相同的数,合并,合并的块不能一下合并两次,更新ind,不再遍历合并的块else if(cards[ii][j].getNum() == cards[i][j].getNum()){cards[ii][j].setNum((cards[i][j].getNum() * 2));cards[i][j].setNum(0);flag = true;ind = ii + 1;//已经合过,该点不再合成MainActivity.mainActivity.addScore(cards[ii][j].getNum() / 2);//播放合并动画playMergeAnimation(ii, j);break;}//上面的块数字不同,退出循环else break;}}}}return flag;}private boolean swipeDown(){boolean flag = false;for(int j = 0; j < 4; ++j){int ind = 4;for(int i = 2; i >= 0; --i){if(cards[i][j].getNum() != 0){for(int ii = i + 1; ii < ind; ++ii){if(cards[ii][j].getNum() == 0){cards[ii][j].setNum(cards[i][j].getNum());cards[i][j].setNum(0);flag = true;i++;}else if(cards[ii][j].getNum() == cards[i][j].getNum()){cards[ii][j].setNum((cards[i][j].getNum() * 2));cards[i][j].setNum(0);flag = true;ind = ii;MainActivity.mainActivity.addScore(cards[ii][j].getNum() / 2);playMergeAnimation(ii, j);break;}else break;}}}}return flag;}private boolean swipeLeft(){boolean flag = false;for(int i = 0; i < 4; ++i){int ind = 0;for(int j = 1; j < 4; ++j){if(cards[i][j].getNum() != 0){for(int jj = j - 1; jj >= ind; --jj){if(cards[i][jj].getNum() == 0){cards[i][jj].setNum(cards[i][j].getNum());cards[i][j].setNum(0);flag = true;j--;}else if(cards[i][jj].getNum() == cards[i][j].getNum()){cards[i][jj].setNum((cards[i][j].getNum() * 2));cards[i][j].setNum(0);flag = true;ind = jj + 1;MainActivity.mainActivity.addScore(cards[i][jj].getNum() / 2);playMergeAnimation(i, jj);break;}else break;}}}}return flag;}private boolean swipeRight(){boolean flag = false;for(int i = 0; i < 4; ++i){int ind = 4;for(int j = 2; j >= 0; --j){if(cards[i][j].getNum() != 0){for(int jj = j + 1; jj < ind; ++jj){if(cards[i][jj].getNum() == 0){cards[i][jj].setNum(cards[i][j].getNum());cards[i][j].setNum(0);flag = true;j++;}else if(cards[i][jj].getNum() == cards[i][j].getNum()){cards[i][jj].setNum((cards[i][j].getNum() * 2));cards[i][j].setNum(0);flag = true;ind = jj;MainActivity.mainActivity.addScore(cards[i][jj].getNum() / 2);playMergeAnimation(i, jj);break;}else break;}}}}return flag;}/***如果存在空白块,或者相邻的数字相同的块,则可以继续滑动*/private boolean canSwipe(){for(int i = 0; i < 4; ++i){for(int j = 0; j < 4; ++j){if(cards[i][j].getNum() == 0){return true;}else if(i != 3 && cards[i][j].getNum() == cards[i + 1][j].getNum()){return true;}else if(j != 3 && cards[i][j].getNum() == cards[i][j + 1].getNum()){return true;}}}return false;}private void addCards(int width, int height){Card c;for(int i = 0; i < 4; ++i){for(int j = 0; j < 4; ++j){c = new Card(getContext());addView(c, width, height);cards[i][j] = c;}}}private void gameOver(){Toast.makeText(getContext(), "游戏结束", Toast.LENGTH_SHORT).show();}private int GetCardWidth() {//获取屏幕信息DisplayMetrics displayMetrics = getResources().getDisplayMetrics();//根据布局,GameView是占屏幕宽度的90%,除以4就是卡片边长return (int)((displayMetrics.widthPixels * 0.9f) / 4);}/** 递归随机,玄学复杂度,期望递归次数小于 16 次,偷了个懒* 最好是把可用方块加入到一个列表中,然后在列表中随机*/private void randomCreateCard(int cnt){Random random = new Random();int r = random.nextInt(4);int c = random.nextInt(4);//该处已经存在数字,重新随机r, cif(cards[r][c].getNum() != 0){randomCreateCard(cnt);return;}int rand = random.nextInt(10);if(rand >= 2) rand = 2;else rand = 4;cards[r][c].setNum(rand);//播放创建动画playCreateAnimation(r, c);if(cnt >= 2){randomCreateCard(cnt - 1);}}/** 播放创建新块动画*/private void playCreateAnimation(int r, int c){AnimationSet animationSet = new AnimationSet(true);//旋转RotateAnimation anim = new RotateAnimation(0,360,RotateAnimation.RELATIVE_TO_SELF,0.5f, RotateAnimation.RELATIVE_TO_SELF,0.5f);anim.setDuration(250);anim.setRepeatCount(0);anim.setInterpolator(new LinearInterpolator());//缩放ScaleAnimation anim2 = new ScaleAnimation(0,1,0,1,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);anim2.setDuration(250);anim2.setRepeatCount(0);animationSet.addAnimation(anim);animationSet.addAnimation(anim2);cards[r][c].startAnimation(animationSet);}/** 播放合并动画*/private void playMergeAnimation(int r, int c){ScaleAnimation anim = new ScaleAnimation(1,1.2f,1,1.2f,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);anim.setDuration(150);anim.setRepeatCount(0);anim.setRepeatMode(Animation.REVERSE);cards[r][c].startAnimation(anim);}
}
Card.java
代表一个格子
package pers.hurric.game2048;import android.content.Context;
import android.view.Gravity;
import android.widget.FrameLayout;
import android.widget.TextView;import androidx.annotation.NonNull;import java.util.HashMap;
import java.util.Map;public class Card extends FrameLayout {TextView textView;private int num;static Map<Integer, Integer> backgroundColorIdMap = new HashMap<>();static Map<Integer, Integer> textColorIdMap = new HashMap<>();static {textColorIdMap.put(0, R.color.textColor0);textColorIdMap.put(2, R.color.textColor2);textColorIdMap.put(4, R.color.textColor4);backgroundColorIdMap.put(0, R.color.backgroundColor0);backgroundColorIdMap.put(2, R.color.backgroundColor2);backgroundColorIdMap.put(4, R.color.backgroundColor4);backgroundColorIdMap.put(8, R.color.backgroundColor8);backgroundColorIdMap.put(16, R.color.backgroundColor16);backgroundColorIdMap.put(32, R.color.backgroundColor32);backgroundColorIdMap.put(64, R.color.backgroundColor64);backgroundColorIdMap.put(128, R.color.backgroundColor128);backgroundColorIdMap.put(256, R.color.backgroundColor256);backgroundColorIdMap.put(512, R.color.backgroundColor512);backgroundColorIdMap.put(1024, R.color.backgroundColor1024);}public Card(@NonNull Context context) {super(context);textView = new TextView(context);textView.setGravity(Gravity.CENTER);textView.setText(getNum() + "");textView.setTextSize(50);setNum(0);LayoutParams lp = new LayoutParams(-1, -1);lp.setMargins(10, 10, 10, 10);addView(textView, lp);}public int getNum() {return num;}public void setNum(int num) {this.num = num;textView.setText(num + "");//数字改变时,同时改变改变字体大小和颜色changeColor(num);changeSize(num);}private void changeSize(int num){if(num >= 1024){textView.setTextSize(25);}else if(num >= 128){textView.setTextSize(35);}else if(num >= 16){textView.setTextSize(42);}else{textView.setTextSize(50);}}private void changeColor(int num){if(num >= 8){textView.setTextColor(getResources().getColor(R.color.textColorCommon));}else{textView.setTextColor(getResources().getColor(textColorIdMap.get(num)));}if(num >= 2048){textView.setBackgroundColor(getResources().getColor(R.color.backgroundColorBiggerThan2048));}else{textView.setBackgroundColor(getResources().getColor(backgroundColorIdMap.get(num)));}}
}