Android开发自定义View实现数字与图片无缝切换的2048

来源:互联网 发布:太上老君化胡为佛 知乎 编辑:程序博客网 时间:2024/05/15 01:37

本博客地址:http://blog.csdn.net/talentclass_ctt/article/details/51952378

最近在学自定义View,无意中看到鸿洋大神以前写过的2048(附上他的博客地址http://blog.csdn.net/lmj623565791/article/details/40020137),看起来很不错,所以自己在他的基础上做一个加强版的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<GameItem> 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 中找到控件,设置各种监听和处理

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    android:paddingBottom="8dp"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="8dp"    tools:context=".MainActivity">    <LinearLayout        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="center_horizontal"        android:gravity="center_vertical">        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:textColor="@color/colorPrimary"            android:textSize="18sp"            android:text="当前得分:" />        <TextView            android:id="@+id/id_score"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:textSize="32sp"            android:text="0"            android:textColor="@color/colorAccent"            android:textStyle="bold"/>    </LinearLayout>    <View        android:layout_width="match_parent"        android:layout_height="2dp"        android:background="@color/colorPrimary" />    <com.talentclass.numberimage2048.GameLayout        android:id="@+id/id_game2048"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:padding="10dp" />    <LinearLayout        android:layout_width="wrap_content"        android:layout_height="42dp"        android:layout_marginBottom="12dp">        <Button            android:id="@+id/id_type"            android:layout_width="wrap_content"            android:layout_height="42dp"            android:background="@drawable/shape"            android:textColor="@color/white"            android:text="图片模式"/>        <View            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_weight="1"/>        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:textSize="18sp"            android:textColor="@color/colorPrimary"            android:text="最高分:"/>        <TextView            android:id="@+id/id_max_score"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:textSize="24sp"            android:textColor="@color/red"            android:textStyle="bold"            android:text="0" />    </LinearLayout>    <Button        android:id="@+id/id_restart"        android:layout_width="match_parent"        android:layout_height="42dp"        android:background="@drawable/shape"        android:textColor="@color/white"        android:text="不服重来"        android:layout_gravity="bottom" /></LinearLayout>
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();    }}

其实源代码我注释也写的很详细,大家可以下载,相信一看就懂的。

最后附上完整源代码:源代码



0 0
原创粉丝点击