主要涉及知识点:
·dataBinding的使用
·ViewModel的使用
·Fragment的创建
·代码逻辑
·本地化
·横屏适配
需求说明:
·实现20以内随机数的加减法运算
·实现欢迎、问答、胜利、失败4个页面的跳转
·4个页面需要实现必要的功能(将在每个界面讲解时说明)
界面展示:
学习视频源自 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
分别创建:欢迎界面、问答界面、胜利界面、失败界面
欢迎界面
界面介绍:
imageView 控件显示app界面
textView 控件 ①显示 app名称 ②显示最高纪录
button 控件 用于进去问答界面
问答界面
界面介绍:
textView控件 实现的功能:
①显示当前得分
②随机算法的产生(已设置为20以内的加减)
③显示用户输出的答案
button控件 实现的功能
①通过点击输出答案
②消除已输出的答案
③确定提交答案
胜利界面
界面介绍:
创造新纪录,但出现失败时跳转为该界面
imageView控件 显示笑脸
textView控件 ①显示一段激励的话②显示新的记录并显示分数
button控件 返回欢迎界面
失败界面
界面介绍:
未创造新纪录并且答错题时跳转到该界面
imageView控件 显示哭脸
textView控件 ①显示挑战失败②显示分数
button控件 返回欢迎界面
操作保存的数据
创建 ViewModel 文件,继承于 AndroidViewModel ,这样就可以直接使用 getApplication() 和 getApplicationContext() 来操作数据
永久存储数据
使用 SavedStateHandle 来永久存储数据
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();
}
}
失败界面
与胜利界面同理
ActionBar 返回箭头
在问答界面,左上方添加一个返回按钮,点击后跳出提示,选择是否确认,点击OK返回欢迎界面,点击Cancel 取消
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标签添加变量和类
最后对右上角的最高分进行绑定
问答界面
该界面需要绑定的有:
当前分数、左边数字、中间运算符、右边数字
同理,转为 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">
对自身来说:利用ViewModel 来管理 Activity和 Fragement中的数据,以及数据存储方法的使用,页面功能的实现,JetPack 方法库中方法的使用存在很大的问题。
十分感谢B站良心UP主
链接: https://space.bilibili.com/137860026/.