阶段综合应用—口算测试APP

口算测试app

主要涉及知识点:

·dataBinding的使用
·ViewModel的使用
·Fragment的创建
·代码逻辑
·本地化
·横屏适配

需求说明:

·实现20以内随机数的加减法运算
·实现欢迎、问答、胜利、失败4个页面的跳转
·4个页面需要实现必要的功能(将在每个界面讲解时说明)

界面展示:

阶段综合应用—口算测试APP_第1张图片

环境准备

学习视频源自 B站良心UP主,链接在文章结尾

使用dataBinding

使用之前需要在 build.gradle(Moudel:app)-android 中添加配置

dataBinding.enabled = true

使用viewModel

使用之前需要在 build.gradle(Model:app)-dependencies 中添加配置

implementation ‘androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-rc02’

创建Fragment

分别创建:欢迎界面、问答界面、胜利界面、失败界面

界面搭建

欢迎界面
阶段综合应用—口算测试APP_第2张图片
界面介绍:
imageView 控件显示app界面
textView 控件 ①显示 app名称 ②显示最高纪录
button 控件 用于进去问答界面

问答界面
阶段综合应用—口算测试APP_第3张图片
界面介绍:
textView控件 实现的功能:
①显示当前得分
②随机算法的产生(已设置为20以内的加减)
③显示用户输出的答案
button控件 实现的功能
①通过点击输出答案
②消除已输出的答案
③确定提交答案

胜利界面
阶段综合应用—口算测试APP_第4张图片
界面介绍:
创造新纪录,但出现失败时跳转为该界面
imageView控件 显示笑脸
textView控件 ①显示一段激励的话②显示新的记录并显示分数
button控件 返回欢迎界面

失败界面
阶段综合应用—口算测试APP_第5张图片
界面介绍:
未创造新纪录并且答错题时跳转到该界面
imageView控件 显示哭脸
textView控件 ①显示挑战失败②显示分数
button控件 返回欢迎界面

连接导航文件逻辑图(Navigation)
阶段综合应用—口算测试APP_第6张图片

逻辑代码

操作保存的数据
创建 ViewModel 文件,继承于 AndroidViewModel ,这样就可以直接使用 getApplication() 和 getApplicationContext() 来操作数据

永久存储数据
使用 SavedStateHandle 来永久存储数据

MyViewModel

public class MyViewModel extends AndroidViewModel {
       //继承于AndroidViewModel
    private SavedStateHandle handle;
    private static String KEY_HIGH_SCORE = "key_high_score";  //记录最高分
    private static String KEY_LEFT_NUMBER = "key_left_number";  //问答界面左边的数字
    private static String KEY_RIGHT_NUMBER = "key_right_number";  //问答界面右边的数字
    private static String KEY_OPERATOR = "key_operator";  //问答界面两数字间的运算符
    private static String KEY_ANSWER = "key_answer";      //运算结果
    private static String SAVE_SHP_DATA_NAME = "save_shp_data_name";   //SharePreferences
    private static String KEY_CURRENT_SCORE = "key_current_score";  //当前的分数
    boolean win_flag = false;    //用于判断是否胜利
    public MyViewModel(@NonNull Application application, SavedStateHandle handle) {
      //构造器
        super(application);
        //最高分将被永久存储,如果没有存储,则进行初始化
        if (!handle.contains(KEY_HIGH_SCORE)) {
     
            SharedPreferences shp = getApplication().getSharedPreferences(SAVE_SHP_DATA_NAME, Context.MODE_PRIVATE);
            handle.set(KEY_HIGH_SCORE,shp.getInt(KEY_HIGH_SCORE,0));
            handle.set(KEY_LEFT_NUMBER,0);
            handle.set(KEY_RIGHT_NUMBER,0);
            handle.set(KEY_OPERATOR,"+");
            handle.set(KEY_ANSWER,0);
            handle.set(KEY_CURRENT_SCORE,0);
        }
        this.handle = handle;
    }
    public MutableLiveData<Integer>getLeftNumber(){
     
        return handle.getLiveData(KEY_LEFT_NUMBER);
    }
    public MutableLiveData<Integer>getRightNumber(){
     
        return handle.getLiveData(KEY_RIGHT_NUMBER);
    }
    public MutableLiveData<String>getOperator() {
     
        return handle.getLiveData(KEY_OPERATOR);
    }
    public MutableLiveData<Integer>getHighScore(){
     
        return handle.getLiveData(KEY_HIGH_SCORE);
    }
    public MutableLiveData<Integer>getCurrentScore(){
     
        return handle.getLiveData(KEY_CURRENT_SCORE);
    }
    public MutableLiveData<Integer>getAnswer(){
     
        return handle.getLiveData(KEY_ANSWER);
    }

   void generator(){
          //生成题目
        int LEVEL = 20;  //设置题目限制
        Random random = new Random();
        int x,y;
        x = random.nextInt(LEVEL) + 1;  //从1到LEVEL-1的随机数
        y = random.nextInt(LEVEL) + 1;
        if (x%2==0) {
     
            getOperator().setValue("+");   //x为偶数则运算符为“+”
            if (x>y) {
     
                getAnswer().setValue(x);    //将较大的一个数做答案,并表示出加数与被加数
                getLeftNumber().setValue(y);
                getRightNumber().setValue(x - y);
            } else {
     
                getAnswer().setValue(y);
                getLeftNumber().setValue(x);
                getRightNumber().setValue(y - x);
            }
        } else {
           //x为奇数则运算符为“-”
            getOperator().setValue("-");
            if (x>y) {
     
                getAnswer().setValue(x - y);
                getLeftNumber().setValue(x);
                getRightNumber().setValue(y);
            } else {
     
                getAnswer().setValue(y - x);
                getLeftNumber().setValue(y);
                getRightNumber().setValue(x);
            }
        }
    }

   @SuppressWarnings("ConstantConditions")
    void save() {
     
        SharedPreferences shp = getApplication().getSharedPreferences(SAVE_SHP_DATA_NAME,Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = shp.edit();
        editor.putInt(KEY_HIGH_SCORE,getHighScore().getValue());
        editor.apply();
    }

   @SuppressWarnings("ConstantConditions")
    void answerCorrect(){
        //问题答对时
        getCurrentScore().setValue(getCurrentScore().getValue() + 1 );  //当前分数 +1
        if (getCurrentScore().getValue() > getHighScore().getValue()) {
         //判断是否超过了最高分
            getHighScore().setValue(getCurrentScore().getValue());
            win_flag = true;
        }
        generator();  //生成新题目
    }
}

欢迎界面

欢迎界面需要通过点击按钮实现进入问答界面

 @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
     
        final MyViewModel myViewModel;
        myViewModel = ViewModelProviders.of(requireActivity(),new SavedStateViewModelFactory(requireActivity().getApplication(),requireActivity())).get(MyViewModel.class);
        FragmentTitleBinding binding;
        binding = DataBindingUtil.inflate(inflater,R.layout.fragment_title,container,false);
        binding.setData(myViewModel);
        binding.setLifecycleOwner(requireActivity());
        binding.button.setOnClickListener(new View.OnClickListener() {
     
            @Override
            public void onClick(View v) {
     
                NavController controller = Navigation.findNavController(v);//获取导航控制器
                controller.navigate(R.id.action_titleFragment_to_questionFragment);//通过控制器跳转
                myViewModel.getCurrentScore().setValue(0);
                myViewModel.generator();
            }
        });
        return binding.getRoot();
        // Inflate the layout for this fragment
        //return inflater.inflate(R.layout.fragment_title, container, false);
    }
}

问答界面

问答界面需要实现①点击按钮显示数字②点击按钮删除数字③判断输入的数字是否与答案相同来决定跳转页面的不同(失败或成功)

   @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
     
        final MyViewModel myViewModel;
        myViewModel = ViewModelProviders.of(requireActivity(), new SavedStateViewModelFactory(requireActivity().getApplication(), requireActivity())).get(MyViewModel.class);
        //myViewModel.generator();  出题
        //myViewModel.getCurrentScore().setValue(0);  //重新开始置零
        final FragmentQuestionBinding binding;
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_question, container, false);
        binding.setData(myViewModel);
        binding.setLifecycleOwner(requireActivity());
        final StringBuilder builder = new StringBuilder();
        View.OnClickListener listener = new View.OnClickListener() {
     
            @Override
            public void onClick(View v) {
       //按下按键显示数字与清零键
                switch (v.getId()) {
     
                    case R.id.button0:
                        builder.append("0");
                        break;
                    case R.id.button1:
                        builder.append("1");
                        break;
                    case R.id.button2:
                        builder.append("2");
                        break;
                    case R.id.button3:
                        builder.append("3");
                        break;
                    case R.id.button4:
                        builder.append("4");
                        break;
                    case R.id.button5:
                        builder.append("5");
                        break;
                    case R.id.button6:
                        builder.append("6");
                        break;
                    case R.id.button7:
                        builder.append("7");
                        break;
                    case R.id.button8:
                        builder.append("8");
                        break;
                    case R.id.button9:
                        builder.append("9");
                        break;
                    case R.id.buttonClear:  //清零键
                        builder.setLength(0);  //字符串清零
                        break;
                }
                if (builder.length() == 0) {
     
                    binding.textView9.setText(getString(R.string.input_indicator));
                } else {
     
                    binding.textView9.setText(builder.toString());
                }

};

        binding.button0.setOnClickListener(listener);
        binding.button1.setOnClickListener(listener);
        binding.button2.setOnClickListener(listener);
        binding.button3.setOnClickListener(listener);
        binding.button4.setOnClickListener(listener);
        binding.button5.setOnClickListener(listener);
        binding.button6.setOnClickListener(listener);
        binding.button7.setOnClickListener(listener);
        binding.button8.setOnClickListener(listener);
        binding.button9.setOnClickListener(listener);
        binding.buttonClear.setOnClickListener(listener);

       binding.buttonSubmit.setOnClickListener(new View.OnClickListener() {
     
            @SuppressWarnings("ConstantConditions")
            @Override
            public void onClick(View v) {
     
                if (builder.length() == 0) {
     
                    builder.append("-1");
                }
                if (Integer.valueOf(builder.toString()).intValue() == myViewModel.getAnswer().getValue()) {
     
                    myViewModel.answerCorrect();
                    builder.setLength(0);
                    binding.textView9.setText(R.string.answer_corrrect_message);
                    //builder.append(getString(R.string.answer_corrrect_message));
                } else {
     
                    NavController controller = Navigation.findNavController(v);
                    if (myViewModel.win_flag) {
     
                        controller.navigate(R.id.action_questionFragment_to_winFragment);
                        myViewModel.win_flag = false;
                        myViewModel.save();
                    } else {
     
                        controller.navigate(R.id.action_questionFragment_to_loseFragment);
                    }
                }

           }
        });
        return binding.getRoot();
        // Inflate the layout for this fragment
        //return inflater.inflate(R.layout.fragment_question, container, false);
    }

}

胜利界面

胜利界面需要点击按钮返回欢迎界面

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
     
        // Inflate the layout for this fragment
        MyViewModel myViewModel = ViewModelProviders.of(requireActivity(),new SavedStateViewModelFactory(requireActivity().getApplication(),requireActivity())).get(MyViewModel.class);
        FragmentWinBinding binding;
        binding = DataBindingUtil.inflate(inflater,R.layout.fragment_win,container,false);
        binding.setData(myViewModel);
        binding.setLifecycleOwner(requireActivity());
        binding.button11.setOnClickListener(new View.OnClickListener() {
     
            @Override
            public void onClick(View v) {
     
                Navigation.findNavController(v).navigate(R.id.action_winFragment_to_titleFragment);
            }
        });
        return binding.getRoot();
    }

}

失败界面

与胜利界面同理

MainActivity

ActionBar 返回箭头

在问答界面,左上方添加一个返回按钮,点击后跳出提示,选择是否确认,点击OK返回欢迎界面,点击Cancel 取消

阶段综合应用—口算测试APP_第7张图片

public class MainActivity extends AppCompatActivity {
     
    NavController controller;
    MyViewModel myViewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myViewModel = ViewModelProviders.of(this,new SavedStateViewModelFactory(getApplication(),this)).get(MyViewModel.class);
        controller = Navigation.findNavController(this,R.id.fragment);
        NavigationUI.setupActionBarWithNavController(this,controller);//界面上方添加返回箭头,此时无实际效果
    }

   @Override
    public boolean onSupportNavigateUp() {
      //给返回箭头添加功能
        if (controller.getCurrentDestination().getId() == R.id.questionFragment) {
       //进入问答界面出现返回箭头
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle(getString(R.string.quit_dialog_title));//显示返回箭头提示语
            builder.setPositiveButton(R.string.dialog_positive_message, new DialogInterface.OnClickListener() {
       //点击OK时
                @Override
                public void onClick(DialogInterface dialog, int which) {
     
                    myViewModel.getCurrentScore().setValue(0);
                    controller.navigateUp();
                }
            });
            builder.setNegativeButton(R.string.dialog_negative_message, new DialogInterface.OnClickListener() {
       //点击取消时
                @Override
                public void onClick(DialogInterface dialog, int which) {
     

               }
            });
            AlertDialog dialog = builder.create();
            dialog.show();
        } else if (controller.getCurrentDestination().getId() == R.id.titleFragment) {
       //判断是不是欢迎界面,是则退出
            finish();
        } else {
       //其他界面直接返回欢迎界面
            controller.navigate(R.id.titleFragment);
        }

       return super.onSupportNavigateUp();
    }
   @Override
    public void onBackPressed() {
     
        onSupportNavigateUp();
    }  //按下BACK键,返回上一步
}

拦截BACK键
BACK键默认功能返回上一步,我们可以拦截BACK键。修改功能

    @Override
    public void onBackPressed() {
     
        onSupportNavigateUp();
    }  //按下BACK键,返回上一步
}

数据绑定

在各个页面自动生成的 fragment.xml文件中进行操作

欢迎界面:

该页面需要进行数据绑定的只有右上角显示的最高分
首先将布局转化为 data binding layout:
然后在data标签添加变量和类
阶段综合应用—口算测试APP_第8张图片
最后对右上角的最高分进行绑定
阶段综合应用—口算测试APP_第9张图片
问答界面

该界面需要绑定的有:
当前分数、左边数字、中间运算符、右边数字
同理,转为 data binding layout 添加变量 类

绑定当前分数:

android:text="@{@string/current_score(data.currentScore)}"

绑定左边数字:

android:text="@{String.valueOf(safeUnbox(data.leftNumber))}"

绑定中间运算符:

android:text="@{data.operator}"

绑定右边数字:

android:text="@{String.valueOf(safeUnbox(data.rightNumber))}"

胜利界面

该界面需要绑定的只有新纪录分数的显示
同理,data binding layout 添加变量、类

android:text="@{@string/win_score_message(data.highScore)}"

失败界面

该界面需要绑定的只有当前分数的显示
同理,data binding layout 添加变量、类

android:text="@{@string/lose_score_message(data.currentScore)}"

本地化

创建所需要语言的字符串版本
点击 strings.xml -> 点击右上角 Open editor -> 点击地球图标 ->添加Chinese

语言版本
英语版本

<resources>
    <string name="app_name">CalculationTest</string>

    <!-- TODO: Remove or change this placeholder text -->
    <string name="hello_blank_fragment" translatable="false">Hello blank fragment</string>
    <string name="title_message">Calculation Test</string>
    <string name="title_image_info" translatable="false">title image</string>
    <string name="title_button_messsage">Enter</string>
    <string name="high_score_message">High Score:%d</string>
    <string name="button0" translatable="false">0</string>
    <string name="button1" translatable="false">1</string>
    <string name="button2" translatable="false">2</string>
    <string name="button3" translatable="false">3</string>
    <string name="button4" translatable="false">4</string>
    <string name="button5" translatable="false">5</string>
    <string name="button6" translatable="false">6</string>
    <string name="button7" translatable="false">7</string>
    <string name="button8" translatable="false">8</string>
    <string name="button9" translatable="false">9</string>
    <string name="buttonClear" translatable="false">C</string>
    <string name="buttonSubmit">OK</string>
    <string name="equal_symbol" translatable="false">=</string>
    <string name="question_mark" translatable="false">\?</string>
    <string name="current_score">Score:%d</string>
    <string name="input_indicator">Your Answer:</string>
    <string name="lose_image_message" translatable="false">lose image</string>
    <string name="win_image_message" translatable="false">win image</string>
    <string name="lose_message">You Lose!</string>
    <string name="win_message">You Win!</string>
    <string name="lose_score_message">Your Score:%d</string>
    <string name="win_score_message">New Record:%d</string>
    <string name="button_back_to_title">Back</string>
    <string name="answer_corrrect_message">Correct!Go On!</string>
    <string name="quit_dialog_title">Are you sure to quit?</string>
    <string name="dialog_positive_message">OK</string>
    <string name="dialog_negative_message">Cancel</string>
    <string name="title_nav_message">Welcome</string>
    <string name="question_nav_message">Testing</string>
    <string name="win_nav_message">Win</string>
    <string name="lose_nav_message">Lose</string>
</resources>

中文版本

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">口算测试</string>
    <string name="answer_corrrect_message">回答正确!请继续!</string>
    <string name="buttonSubmit">确定</string>
    <string name="button_back_to_title">返回</string>
    <string name="current_score">得分:%d</string>
    <string name="dialog_negative_message">取消</string>
    <string name="dialog_positive_message">确定</string>
    <string name="high_score_message">最高记录:%d</string>
    <string name="input_indicator">请开始答题:</string>
    <string name="lose_score_message">你的得分:%d</string>
    <string name="quit_dialog_title">确定离开?</string>
    <string name="title_button_messsage">进入</string>
    <string name="title_message">口算测试</string>
    <string name="win_message">挑战成功!</string>
    <string name="win_score_message">创造新记录:%d</string>
    <string name="lose_message">挑战失败</string>
    <string name="title_nav_message">欢迎</string>
    <string name="question_nav_message">测试</string>
    <string name="win_nav_message">胜利</string>
    <string name="lose_nav_message">失败</string>
</reso

横屏适配

大部分app在竖屏状态下格局清晰,但旋转后界面会十分奇怪,因此需要对横屏状态下设置一个新的布局——横屏版本
当然,可以设置不可旋转

<activity android:name=".MainActivity"
            android:screenOrientation="portrait">

横屏布局
阶段综合应用—口算测试APP_第10张图片

总结

对自身来说:利用ViewModel 来管理 Activity和 Fragement中的数据,以及数据存储方法的使用,页面功能的实现,JetPack 方法库中方法的使用存在很大的问题。
十分感谢B站良心UP主
链接: https://space.bilibili.com/137860026/.

你可能感兴趣的:(Android,app,安卓,android)