前言
Android期末作业,估摸着也花了整整5天。里面可能会缺少某些细节,如果跟着做有不会的评论就行,每天都会看,尽力解答。
功能
待办专注计时音乐天气实现步骤
一、底部菜单栏切换页
1.添加依赖
dependencies { implementation 'com.google.android.material:material:1.2.1'}
2.在res资源文件夹下新建一个menu文件夹,创建底部导航的菜单布局文件
创建对应数量的item,为每个菜单栏选项给每个item定义title(标题),icon(图标)<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_task" android:icon="@drawable/menu_task" android:title="事项"/> <item android:id="@+id/menu_accounts" android:icon="@drawable/menu_task" android:title="专注"/> <item android:id="@+id/menu_absorbed" android:icon="@drawable/menu_task" android:title="音乐"/> <item android:id="@+id/menu_weather" android:icon="@drawable/menu_task" android:title="每日先知"/></menu>
3.在activity_main布局页面引入 com.google.android.material.bottomnavigation.BottomNavigationView 控件
控件属性:
app:labelVisibilityMode="labeled"取消定义三个以上按钮文字不显示的效果app:itemBackground="@null" 取消水波纹的效果app:itemIconTint设置图标的颜色app:itemTextColor设置字体的颜色app:menu="@menu/bottom_navi_menu"将menu引入<?xml version="1.0" encoding="utf-8"?><RelativeLayout 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" xmlns:app="http://schemas.android.com/apk/res-auto" tools:context=".MainActivity"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="60dp" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:layout_width="match_parent" android:layout_height="60dp" android:layout_alignParentBottom="true" app:labelVisibilityMode="labeled" app:itemBackground="@null" app:menu="@menu/bottom_navi_menu" /></RelativeLayout>
4.依次创建每个页面的Fragment类及布局文件,如Task页面
<!-- task_fragment.xml --><?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="Task PAGE" android:textSize="40dp" android:gravity="center" /></LinearLayout>
// TaskFragment.javapublic class TaskFragment extends Fragment { //重写onCreateView, fragment绑定布局文件 @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.task_fragment, container, false); return view; }}
5.在MainActivity.java中进行设置BottomNavigation选择监听事件对fragment进行管理。
public List<Fragment> fragmentList = new ArrayList<>();private FragmentManager fragmentManager;// 底部导航栏模块public void InitBottomNavigation() { // 添加五个fragment实例到fragmentList,以便管理 fragmentList.add(new TaskFragment()); fragmentList.add(new AbsorbedFragment()); fragmentList.add(new MusicFragment()); fragmentList.add(new WeatherFragment()); //建立fragment管理器 fragmentManager = getSupportFragmentManager(); //管理器开启事务,将fragment实例加入管理器 fragmentManager.beginTransaction() .add(R.id.FragmentLayout, fragmentList.get(0), "TASK") .add(R.id.FragmentLayout, fragmentList.get(1), "ABSOTBED") .add(R.id.FragmentLayout, fragmentList.get(2), "MUSIC") .add(R.id.FragmentLayout, fragmentList.get(3), "WEATHER") .commit(); //设置fragment显示初始状态 fragmentManager.beginTransaction() .show(fragmentList.get(1)) .hide(fragmentList.get(0)) .hide(fragmentList.get(2)) .hide(fragmentList.get(3)) .commit(); //设置底部导航栏点击选择监听事件 BottomNavigationView bottomNavigationView = findViewById(R.id.BottomNavigation); bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { @SuppressLint("NonConstantResourceId") @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { // return true : show selected style // return false: do not show switch (item.getItemId()) { case R.id.menu_task: ShowFragment(0); return true; case R.id.menu_accounts: ShowFragment(1); return true; case R.id.menu_absorbed: ShowFragment(2); return true; case R.id.menu_weather: ShowFragment(3); return true; default: Log.i(TAG, "onNavigationItemSelected: Error"); break; } return false; } });}public void ShowFragment(int index) { fragmentManager.beginTransaction() .show(fragmentList.get(index)) .hide(fragmentList.get((index + 1) % 4)) .hide(fragmentList.get((index + 2) % 4)) .hide(fragmentList.get((index + 3) % 4)) .commit();}
二、天气显示界面
1、添加依赖(用于获取和解析天气数据)
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
2、获取天气API接口,这里以临海市为例。使用OkHttp请求天气数据,使用Log打印测试是否能成功获取
public void RefreshWeatherData() { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(weatherUrl).build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { e.printStackTrace(); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { String weatherJson = response.body().string(); Weather weather = new Gson().fromJson(weatherJson, Weather.class); Log.i(TAG, "onResponse: "+weatherJson); } }); }
3、Json数据获取成功后,根据Json数据的结构建立Weather类用于解析Json数据。
// class Weatherpublic class Weather { private String city;//城市名 private String update_time;//更新时间 private List<DayData> data;//每天的天气数据列表,data.get(0)为当天数据 /* getter and setter */}// class DayDatapublic class DayData { private String wea;//天气状况 private String tem;//当前温度 private String tem1;//最高温 private String tem2;//最低温 private String humidity; //湿度 private String air_level;//空气质量等级 private String air_tips;//空气质量小提示 /* getter and setter */}
4、由于OkHttp的请求是在子线程中进行的,需要使用Handler消息队列机制将解析出来的Weather实例发送到主线程用以显示在界面上。
//消息处理类public class MyHandler extends Handler { @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); //what == 1 天气消息 if (msg.what == 1) ShowWeatherInfo((Weather) msg.obj); }}public void ShowWeatherInfo(Weather weather) { String city = weather.getCity(); String wea = weather.getData().get(0).getWea(); String maxTem = weather.getData().get(0).getTem1(); String minTem = weather.getData().get(0).getTem2(); String tem = weather.getData().get(0).getTem(); String humidity = "湿度 " + weather.getData().get(0).getHumidity(); String air_level = "空气指数 " + weather.getData().get(0).getAir_level(); // tem tem1 tem2 city wea rain pm image ((TextView) findViewById(R.id.cityView)).setText(city); ((TextView) findViewById(R.id.weaView)).setText(wea); ((TextView) findViewById(R.id.mmtemView)).setText( String.format("%s° / %s°", minTem.substring(0, minTem.length() - 1), maxTem.substring(0, maxTem.length() - 1))); ((TextView) findViewById(R.id.temView)).setText(tem.substring(0, tem.length() - 1) + "°"); ((TextView) findViewById(R.id.humidityView)).setText(humidity); ((TextView) findViewById(R.id.levelView)).setText(air_level); ShowWeatherImage(wea);//根据天气状况wea显示对应的天气图片,这里不详细说明,使用switch就行 }
5、别忘了在OkHttp请求完成时发送消息
public void RefreshWeatherData() { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(weatherUrl).build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { e.printStackTrace(); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { String weatherJson = response.body().string(); Weather weather = new Gson().fromJson(weatherJson, Weather.class); Message message = new Message(); message.what = 1; message.obj = weather; myHandler.sendMessage(message); } });}
6、优化xml布局
三、待办事项界面
这里由于ListView是放在Fragment中的,所以直接在MainAcitivity.java中设置适配器可能会出现数据没法显示的bug。所以我直接把从数据库获取数据,Adapter的定义,ListView设置适配器的模块搬到了TaskFragment.java中。
1.在task.xml中添加ListView,先不用设置UI样式,先把数据拿到并显示在界面上
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/taskText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="事项"/> <ListView android:id="@+id/taskListView" android:layout_width="match_parent" android:layout_height="match_parent"/></LinearLayout>
2.创建task_item.xml布局文件(这里注意线性布局的方向及宽高,以保证task_item能放在ListView中)
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/task_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/black" android:textSize="30dp" android:text="TextView" /></LinearLayout>
3.新建TaskItem类,存放事项数据
package com.example.daily.tasks;public class TaskItem { private int id; private String content; private String type; private int status; public TaskItem(int id, String type, String content, int status){ this.id = id; this.type = type; this.content = content; this.status = status; } // 自行添加Get和Set方法}
4.在TaskFragment.java中创建SQLite数据库并获取待办事项的数据
public class TaskFragment extends Fragment { private static final String TAG = TaskFragment.class.getName(); private List<TaskItem> taskList = new ArrayList<>(); @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.task, container, false); ReadTaskDataFromSQL(); //测试数据获取是否正常 for(TaskItem item : taskList){ Log.i(TAG, "taskList "+item.getId()+" "+item.getContent()); } return view; } //读取数据库并将数据存到taskList public void ReadTaskDataFromSQL(){ MySQLiteOpenHelper openHelper = new MySQLiteOpenHelper(getActivity()); SQLiteDatabase readDatabase = openHelper.getReadableDatabase(); Cursor cursor = readDatabase.query( "task", new String[]{"id", "type", "content", "status"}, null,null,null,null,null ); while(cursor.moveToNext()){ TaskItem task = new TaskItem( cursor.getInt(0), cursor.getString(1), cursor.getString(2), cursor.getInt(3) ); taskList.add(task); } } //创建SQLite数据库 public class MySQLiteOpenHelper extends SQLiteOpenHelper{ public MySQLiteOpenHelper(@Nullable Context context) { super(context, "Daily.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { Log.i(TAG, "onCreate: sqlite"); //创建待办事项数据表 String create_sql = "create table task(" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "content varchar(50), " + "type varchar(50), " + "status int);"; db.execSQL(create_sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {} }}
5.数据获取正常以后,建立ListView适配器。这里涉及到缓存convertView的使用,使用convertView可以防止每创建一个item时就解析一个布局,这样效率肯定不高。convertView是Android提供的用于缓存的View,在第一次渲染item时,将将解析出来的View放入缓存convertView,在下一次渲染item的时候,判断convertView是否为空即可。
public class TaskAdapter extends BaseAdapter{ @Override public int getCount() { //测试getCount返回值是否正常 Log.i(TAG, "getCount: "+taskList.size()); return taskList.size(); } @Override public Object getItem(int position) { return taskList.get(position); } @Override public long getItemId(int position) { return taskList.get(position).getId(); } @Override public View getView(int position, View convertView, ViewGroup parent) { //测试getView是否执行 Log.i(TAG, "getView: "+position); ViewHolder viewHolder; TaskItem task = (TaskItem) getItem(position); if(convertView == null){ viewHolder = new ViewHolder(); convertView = LayoutInflater.from(getActivity()).inflate(R.layout.task_item, null); viewHolder.taskItemTextView = convertView.findViewById(R.id.task_content); convertView.setTag(viewHolder); }else{ viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.taskItemTextView.setText(task.getId()+" "+task.getContent()); return convertView; }}public class ViewHolder{ TextView taskItemTextView;}
6.在onCreateView中设置ListView的适配器
private List<TaskItem> taskList = new ArrayList<>();private TaskAdapter taskAdapter;private ListView taskListView;@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.task, container, false); taskListView = view.findViewById(R.id.taskListView); taskAdapter = new TaskAdapter(); taskListView.setAdapter(taskAdapter); ReadTaskDataFromSQL(); return view;}
7.设计每一条待办事项的布局样式,如图所示,布局设计就不放原码了,使用多个线性布局的嵌套,gravity,margin属性即可实现。
img:task-2.jpg
8.根据待办事项的状态显示不同按钮,并标记待办事项的重要程度。
public void ShowTaskContent(View convertView, TaskItem task){ //显示事项内容 TextView content = ((ViewHolder) convertView.getTag()).taskContent; int status = task.getStatus(); content.setText(task.getContent()); //事项已完成 中划线 灰色 if(status == 1){ content.getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG); content.setTextColor(getResources().getColor(R.color.GRAY, null)); } //事项未完成 无中划线 黑色 if(status == 0){ content.getPaint().setFlags(0); content.setTextColor(getResources().getColor(R.color.black, null)); } //事项失败 无中划线 灰色 if(status == -1){ content.getPaint().setFlags(0); content.setTextColor(getResources().getColor(R.color.GRAY, null)); } }public void ShowTaskLevel(View convertView, int level){ // 显示事项重要级别 level : 0~3 四个优先级 Ⅰ Ⅱ Ⅲ Ⅳ TextView levelText = ((ViewHolder) convertView.getTag()).taskLevel; if(level == 0){ levelText.setText("Ⅰ"); levelText.setTextColor(getResources().getColor(R.color.level_0, null)); } if(level == 1){ levelText.setText("Ⅱ"); levelText.setTextColor(getResources().getColor(R.color.level_1, null)); } if(level == 2){ levelText.setText("Ⅲ"); levelText.setTextColor(getResources().getColor(R.color.level_2, null)); } if(level == 3){ levelText.setText("Ⅳ"); levelText.setTextColor(getResources().getColor(R.color.level_3, null)); } }
9.在顶部添加五个TextView作为分类查看事项菜单,点击某一分类即可查看该分类下的所有事项,并修改被点击TextView 的样式。
/** 菜单栏模块 **/public void SetTypeMenuOnClick(View view){ typeMenuList.add((TextView) view.findViewById(R.id.TypeMenu_default)); typeMenuList.add((TextView) view.findViewById(R.id.TypeMenu_work)); typeMenuList.add((TextView) view.findViewById(R.id.TypeMenu_study)); typeMenuList.add((TextView) view.findViewById(R.id.TypeMenu_life)); int[] color = { getResources().getColor(R.color.defaultColor, null), getResources().getColor(R.color.workColor, null), getResources().getColor(R.color.studyColor, null), getResources().getColor(R.color.lifeColor, null), }; for(int i=0; i<4 ;i++){ int finalI = i; //分类索引值 typeMenuList.get(i).setOnClickListener(v -> { // 点击分类的一项后设置样式 typeMenuList.get(finalI).setTextColor(Color.BLACK); typeMenuList.get(finalI).setBackgroundColor(Color.WHITE); typeMenuList.get((finalI+1) % 4).setBackgroundColor(color[(finalI+1) % 4]); typeMenuList.get((finalI+1) % 4).setTextColor(Color.WHITE); typeMenuList.get((finalI+2) % 4).setBackgroundColor(color[(finalI+2) % 4]); typeMenuList.get((finalI+2) % 4).setTextColor(Color.WHITE); typeMenuList.get((finalI+3) % 4).setBackgroundColor(color[(finalI+3) % 4]); typeMenuList.get((finalI+3) % 4).setTextColor(Color.WHITE); // 显示某一类待办数据,这里筛选taskList即可 List<TaskItem> typeTaskList = new ArrayList<>(); String[] types = {"全部", "工作","学习","生活"}; /* 分类索引值 0 全部 1 工作 2 学习 3 生活 */ // 点击工作 学习 生活时分类 // TypeNow 是一个全局变量,表示当前的分类 TypeNow = types[finalI]; Log.i(TAG, "SetTypeMenuOnClick: "+TypeNow); ReadTaskFromDatabase(); }); }}
10.task.xml布局右上角加入一个switch控件用以隐藏已完成事项。
//隐藏已完成Switch Switch hideCompletedTaskSwitch = view.findViewById(R.id.HideCompletedTaskView); hideCompletedTaskSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(isChecked) isHideCompleted = true; else isHideCompleted = false; // isHideCompleted 是一个全局变量,表示当前是否隐藏已完成事项 ReadTaskFromDatabase(); } });
完成9,10步之后就需要修改读取数据库的模块,加入TypeNow和isHideCompleted变量加以控制。
public void ReadTaskFromDatabase(){ if (taskList.size()!=0) { taskList.clear(); } Cursor cursor = readDatabase.query( "task", new String[]{"id", "type", "level","content", "info", "status"}, null, null, null, null, null ); //隐藏,有分类 if(isHideCompleted && !TypeNow.equals("全部")){ //只获取未完成事项 while(cursor.moveToNext()){ if((cursor.getInt(5) == 0 ) && (cursor.getString(1).equals(TypeNow))){ TaskItem task = new TaskItem( cursor.getInt(0), cursor.getString(1), cursor.getInt(2), cursor.getString(3), cursor.getString(4), cursor.getInt(5) ); taskList.add(task); } } } //不隐藏,有分类 if(!isHideCompleted && !TypeNow.equals("全部")){ while(cursor.moveToNext()){ if(cursor.getString(1).equals(TypeNow)){ TaskItem task = new TaskItem( cursor.getInt(0), cursor.getString(1), cursor.getInt(2), cursor.getString(3), cursor.getString(4), cursor.getInt(5) ); taskList.add(task); } } } //隐藏,不分类 if(isHideCompleted && TypeNow.equals("全部")){ while(cursor.moveToNext()){ if(cursor.getInt(5) == 0){ TaskItem task = new TaskItem( cursor.getInt(0), cursor.getString(1), cursor.getInt(2), cursor.getString(3), cursor.getString(4), cursor.getInt(5) ); taskList.add(task); } } } else{ while(cursor.moveToNext()){ TaskItem task = new TaskItem( cursor.getInt(0), cursor.getString(1), cursor.getInt(2), cursor.getString(3), cursor.getString(4), cursor.getInt(5) ); taskList.add(task); } }// 别忘了通知ListView适配器数据变化 taskAdapter.notifyDataSetChanged();}
11、添加事项,这里使用的是在整个RelativeLayout布局中添加一个ImageView作为添加事项的按钮,并定义点击事件,点击时弹出对话框,在对话框中输入添加事项的信息。
自定义对话框需要先设计一个layout布局文件add_task_dialog.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:orientation="vertical" android:paddingLeft="15dp" android:paddingRight="15dp" android:paddingBottom="20dp" android:paddingTop="10dp" android:layout_height="wrap_content"> <TextView android:text="添加事项" android:textColor="@color/black" android:textSize="25dp" android:layout_width="match_parent" android:gravity="center" android:layout_height="50dp"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="15dp" android:paddingRight="15dp" android:orientation="horizontal"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:layout_marginRight="15dp" android:text="事项" /> <EditText android:id="@+id/addTaskContentEdit" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:ems="10" android:inputType="textPersonName" android:text="" /> </LinearLayout> <RelativeLayout android:layout_width="match_parent" android:layout_marginTop="10dp" android:paddingLeft="15dp" android:paddingRight="15dp" android:layout_height="wrap_content"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/textView2" android:layout_width="160dp" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:text="事项分类" /> <RadioGroup android:id="@+id/typeRadioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content"> <RadioButton android:id="@+id/radioButton8" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/workColor" android:text="工作" /> <RadioButton android:id="@+id/radioButton7" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/studyColor" android:text="学习" /> <RadioButton android:id="@+id/radioButton6" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/lifeColor" android:text="生活" /> <RadioButton android:id="@+id/radioButton5" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/defaultColor" android:text="不分类" /> </RadioGroup> </LinearLayout> <LinearLayout android:layout_width="160dp" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:orientation="vertical"> <TextView android:id="@+id/textView3" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:text="重要级别" /> <RadioGroup android:id="@+id/levelRadioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content"> <RadioButton android:id="@+id/radioButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_0" android:text="0 重要且紧急" /> <RadioButton android:id="@+id/radioButton2" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_1" android:text="1 重要但不紧急" /> <RadioButton android:id="@+id/radioButton3" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_2" android:text="2 不重要但紧急" /> <RadioButton android:id="@+id/radioButton4" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_3" android:text="3 不重要且不紧急" /> </RadioGroup> </LinearLayout> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:paddingLeft="15dp" android:paddingRight="15dp" android:orientation="horizontal"> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:layout_marginRight="15dp" android:text="备注" /> <EditText android:id="@+id/addTaskInfoEdit" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:text="" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:layout_marginTop="10dp" android:orientation="horizontal"> <Button android:id="@+id/cancelAddButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="100dp" android:text="取消" /> <Button android:id="@+id/confirmAddButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="确定" /> </LinearLayout></LinearLayout>
12、定义一个方法,实现弹出添加事项界面的对话框,并设置确认和取消按钮的点击事件,确认按钮即添加该事项到数据库并显示
public void ShowAddTaskDialog(){ //获取添加事项布局实例 View addView = getLayoutInflater().inflate(R.layout.add_task_dialog, null); // 将该布局添加到对话框 final AlertDialog addDialog = new AlertDialog.Builder(getActivity()).setView(addView).create(); addDialog.show(); //获取对话框中的布局控件 Button cancelButton = (Button) addView.findViewById(R.id.cancelAddButton); Button confirmButton = (Button) addView.findViewById(R.id.confirmAddButton); EditText contentEdit = (EditText) addView.findViewById(R.id.addTaskContentEdit); EditText infoEdit = (EditText) addView.findViewById(R.id.addTaskInfoEdit); RadioGroup typeGroup = (RadioGroup) addView.findViewById(R.id.typeRadioGroup); RadioGroup levelGroup = (RadioGroup) addView.findViewById(R.id.levelRadioGroup); typeGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { } }); levelGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { } }); //确定按钮 confirmButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 获取输入的事项内容和备注 String addContent = contentEdit.getText().toString(); String addInfo = infoEdit.getText().toString(); //RadioGroup的选择项 RadioButton typeSelectBtn = (RadioButton) addView.findViewById(typeGroup.getCheckedRadioButtonId()); String addType = typeSelectBtn.getText().toString(); RadioButton levelSelectBtn = (RadioButton) addView.findViewById(levelGroup.getCheckedRadioButtonId()); int addLevel = Integer.parseInt(levelSelectBtn.getText().toString().substring(0,1)); //插入数据库 InsertTaskToDatabase( new TaskItem(addType, addLevel, addContent, addInfo, 0) ); addDialog.dismiss(); } }); // 取消按钮 cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { addDialog.dismiss(); } });}
13、然后在添加事项的点击事件中调用ShowAddTaskDialog()即可
//添加事项的按钮ImageView addTaskImage = (ImageView) view.findViewById(R.id.addTaskImage);addTaskImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ShowAddTaskDialog(); }});
14.长按某条事项弹出对话框,显示事项信息,可以修改,删除,标记失败。和添加事项的对话框实现原理相同,这里不详细说明,给出代码供参考
<!-- task_info_dialog.xml --><?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:orientation="vertical" android:paddingLeft="15dp" android:paddingRight="15dp" android:paddingBottom="20dp" android:paddingTop="10dp" android:layout_height="wrap_content"> <TextView android:text="事项信息" android:textColor="@color/black" android:textSize="25dp" android:layout_width="match_parent" android:gravity="center" android:layout_height="50dp"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="15dp" android:paddingRight="15dp" android:orientation="horizontal"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:layout_marginRight="15dp" android:text="事项" /> <EditText android:id="@+id/addTaskContentEdit" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:ems="10" android:inputType="textPersonName" android:text="" /> </LinearLayout> <RelativeLayout android:layout_width="match_parent" android:layout_marginTop="10dp" android:paddingLeft="15dp" android:paddingRight="15dp" android:layout_height="wrap_content"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/textView2" android:layout_width="160dp" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:text="事项分类" /> <RadioGroup android:id="@+id/typeRadioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content"> <RadioButton android:id="@+id/workButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/workColor" android:text="工作" /> <RadioButton android:id="@+id/studyButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/studyColor" android:text="学习" /> <RadioButton android:id="@+id/lifeButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/lifeColor" android:text="生活" /> <RadioButton android:id="@+id/defaultButton" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/defaultColor" android:text="全部" /> </RadioGroup> </LinearLayout> <LinearLayout android:layout_width="160dp" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:orientation="vertical"> <TextView android:id="@+id/textView3" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:text="重要级别" /> <RadioGroup android:id="@+id/levelRadioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content"> <RadioButton android:id="@+id/level0Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_0" android:text="0 重要且紧急" /> <RadioButton android:id="@+id/level1Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_1" android:text="1 重要但不紧急" /> <RadioButton android:id="@+id/level2Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_2" android:text="2 不重要但紧急" /> <RadioButton android:id="@+id/level3Button" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/level_3" android:text="3 不重要且不紧急" /> </RadioGroup> </LinearLayout> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:paddingLeft="15dp" android:paddingRight="15dp" android:orientation="horizontal"> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@color/black" android:layout_marginRight="15dp" android:text="备注" /> <EditText android:id="@+id/addTaskInfoEdit" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:text="" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:layout_marginTop="20dp" android:orientation="horizontal"> <LinearLayout android:layout_width="wrap_content" android:layout_marginRight="60dp" android:orientation="vertical" android:layout_height="wrap_content"> <ImageView android:id="@+id/deleteTaskButton" android:layout_width="wrap_content" android:layout_height="40dp" android:adjustViewBounds="true" android:src="@drawable/delete_icon" /> <TextView android:layout_width="40dp" android:layout_height="wrap_content" android:textColor="@color/black" android:gravity="center" android:textSize="15dp" android:layout_marginTop="5dp" android:text="删除"/> </LinearLayout> <LinearLayout android:layout_width="wrap_content" android:orientation="vertical" android:layout_height="wrap_content"> <ImageView android:id="@+id/failTaskButton" android:layout_width="40dp" android:layout_height="40dp" android:adjustViewBounds="true" android:src="@drawable/fail_icon" /> <TextView android:layout_width="40dp" android:layout_height="wrap_content" android:textColor="#d81e06" android:gravity="center" android:textSize="15dp" android:layout_marginTop="5dp" android:text="失败"/> </LinearLayout> <LinearLayout android:layout_width="wrap_content" android:layout_marginLeft="60dp" android:orientation="vertical" android:layout_height="wrap_content"> <ImageView android:id="@+id/modifyTaskButton" android:layout_width="40dp" android:layout_height="40dp" android:adjustViewBounds="true" android:src="@drawable/modify_icon" android:text="修改" /> <TextView android:layout_width="40dp" android:layout_height="wrap_content" android:textColor="@color/purple_500" android:gravity="center" android:textSize="15dp" android:layout_marginTop="5dp" android:text="修改"/> </LinearLayout> </LinearLayout></LinearLayout>
public void ShowTaskInfoDialog(TaskItem task){ // 获取传入的事项数据 String content = task.getContent(); String type = task.getType(); int level = task.getLevel(); String info = task.getInfo(); //获取布局 View infoView = getLayoutInflater().inflate(R.layout.task_info_dialog, null); final AlertDialog infoDialog = new AlertDialog.Builder(getActivity()).setView(infoView).create(); infoDialog.show(); //获取对话框中的布局控件 EditText contentEdit = (EditText) infoView.findViewById(R.id.addTaskContentEdit); EditText infoEdit = (EditText) infoView.findViewById(R.id.addTaskInfoEdit); RadioGroup typeGroup = (RadioGroup) infoView.findViewById(R.id.typeRadioGroup); RadioGroup levelGroup = (RadioGroup) infoView.findViewById(R.id.levelRadioGroup); ImageView deleteImage = (ImageView) infoView.findViewById(R.id.deleteTaskButton); ImageView modifyImage = (ImageView) infoView.findViewById(R.id.modifyTaskButton); ImageView failImage = (ImageView) infoView.findViewById(R.id.failTaskButton); //显示task事项信息 contentEdit.setText(content); infoEdit.setText(info); SetTypeRadioGroupSelected(typeGroup, type); SetLevelRadioGroupSelected(levelGroup, level); //删除按钮 deleteImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { DeleteTaskToDatabase(task); infoDialog.dismiss(); } }); //失败按钮 failImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { task.setStatus(-1); UpDateTaskToDatabase(task); //别忘记关闭对话框 infoDialog.dismiss(); } }); //修改按钮 modifyImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 获取输入的事项内容和备注 String modifyContent = contentEdit.getText().toString(); String modifyInfo = infoEdit.getText().toString(); //RadioGroup的选择项 RadioButton typeSelectBtn = (RadioButton) infoView.findViewById(typeGroup.getCheckedRadioButtonId()); String modifyType = typeSelectBtn.getText().toString(); RadioButton levelSelectBtn = (RadioButton) infoView.findViewById(levelGroup.getCheckedRadioButtonId()); int modifyLevel = Integer.parseInt(levelSelectBtn.getText().toString().substring(0,1)); task.setContent(modifyContent); task.setInfo(modifyInfo); task.setType(modifyType); task.setLevel(modifyLevel); UpDateTaskToDatabase(task); //别忘记关闭对话框 infoDialog.dismiss(); } }); }
//在适配器的getView中,设置每条事项的长按事件:调用ShowTaskInfoDialog弹出对话框显示事项的内容convertView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { ShowTaskInfoDialog(task); return false; }});
四、专注计时界面
计时的原理是使用Android四大组件之一的Service开启计时线程,并每隔一秒钟发送一次本地广播通知主界面更新布局。
1、创建服务类TimeService,继承自Service。这里在Service类里面定义了一个TimeThread自定义线程类,用以方便线程的挂起和恢复。
public class TimeService extends Service { private static final String TAG = TimeService.class.getName(); //计时秒数 private int second = 0; public int getSecond() { return second; } public void setSecond(int second) { this.second = second; } @Nullable @Override public IBinder onBind(Intent intent) { return new LocalBinder(); } @Override public void onCreate() { Log.i(TAG, "TimeService onCreate: "); super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "TimeService onStartCommand: "); //创建计时线程实例 timeThread = new TimeThread(); timeThread.start(); isRunning = true; return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { Log.i(TAG, "TimeService onDestroy: "); super.onDestroy(); } @Override public boolean onUnbind(Intent intent) { Log.i(TAG, "TimeService onUnbind: "); return super.onUnbind(intent); } //用于返回本地服务 public class LocalBinder extends Binder{ public TimeService getService(){ return TimeService.this; } } public class TimeThread extends Thread{ private final Object lock = new Object(); private boolean pause = false; /** * 调用该方法实现线程的暂停 */ void pauseThread(){ Log.i(TAG, "pauseTimeThread: "); pause = true; } /* 调用该方法实现恢复线程的运行 */ void resumeThread(){ Log.i(TAG, "resumeTimeThread: "); pause = false; synchronized (lock){ lock.notify(); } } /** * 这个方法只能在run 方法中实现,不然会阻塞主线程,导致页面无响应 */ void onPause() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public void run() { super.run(); try { while(true){ //当pause为true时,调用onPause挂起该线程 TimeUnit.SECONDS.sleep(1); while(pause) { onPause(); } second++; SendSecondBroadcast(); Log.i(TAG, "run: "+second); } } catch (InterruptedException e) { e.printStackTrace(); } } }}
2、在AndroidManifast注册TimeService类
3、在AbsorbedFragment中绑定服务,运行测试service是否连接成功
public void BindTimeService(){ Intent intent = new Intent(getActivity(), TimeService.class); ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { localBinder = (TimeService.LocalBinder) service; if(localBinder.getService() != null){ Log.i(TAG, "onServiceConnected: time service connected"); } } @Override public void onServiceDisconnected(ComponentName name) { Log.i(TAG, "onServiceDisconnected: "); } }; getActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE);}
4、给开始计时按钮添加点击事件,运行测试TimeThread是否每隔一秒打印一次
Intent intent = new Intent();intent.setClass(getActivity(), TimeService.class);getActivity().startService(intent);
5、运行成功后,添加暂停,继续,取消按钮,运行测试观察打印信息是否正常
暂停点击事件:localBinder.getService().PauseTime();继续点击事件:localBinder.getService().ResumeTime();取消点击事件:localBinder.getService().CancelTime(); //TimeService中用于在MainActivity调用的方法 public void PauseTime(){ timeThread.pauseThread(); isRunning = false; } public void ResumeTime(){ timeThread.resumeThread(); isRunning = true; } public void CancelTime(){ timeThread.pauseThread(); second = 0; }
6、创建本地广播,用以接收TimeThread发送的秒数,并更新布局界面
//注册接收计时秒数的本地广播IntentFilter timeIntentFilter = new IntentFilter();timeIntentFilter.addAction("SECONDS_CHANGED");BroadcastReceiver timeBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int second = localBinder.getService().getSecond(); ShowTimeSecond(second); }};LocalBroadcastManager.getInstance(getActivity()) .registerReceiver(timeBroadcastReceiver, timeIntentFilter);
7、在TimeThread的run方法中每一秒发送一次本地广播,运行测试是否正常
@Overridepublic void run() { super.run(); try { while(true){ //当pause为true时,调用onPause挂起该线程 TimeUnit.SECONDS.sleep(1); while(pause) { onPause(); } second++; SendSecondBroadcast(); } } catch (InterruptedException e) { e.printStackTrace(); }}
8、显示专注计时的记录,使用SQLite数据库实现,和待办事项界面一样,添加完成专注计时的按钮,点击事件为添加计时信息的字符串到数据库。
五、音乐界面
实现原理,使用Service组件和MediaPlayer。点击音乐列表的某条音乐时,在服务中开启MediaPlayer播放音乐,并每隔一秒种发送一次本地广播(内容为当前已播放的秒数),设置界面中的进度条。并给进度条设置拖动的事件,将对应的播放进度传给MediaPlayer跳转至对应的进度。
1、定义Music类,包含音乐名,文件
public class Music { private String name; private File file; // getter and setter }
2、 获取本地音乐文件
由于API 29以后getExternalStorageDirectory()被废弃,所以直接采用指定的路径获取MP3音乐文件。
public void ShowMusicList(){ File musicStorage = new File("/storage/11E9-360F/Music"); File[] musicFiles = musicStorage.listFiles(new FilenameFilter(){ @Override public boolean accept(File dir, String name) { return name.endsWith(".mp3"); } }); for(int i=0; i<musicFiles.length; i++){ Music music = new Music(); music.setName(musicFiles[i].getName()); music.setFile(musicFiles[i]); musicList.add(music); }}
3、将音乐名使用ListView列表显示
public class MusicAdapter extends BaseAdapter{ @Override public int getCount() { return musicList.size(); } @Override public Object getItem(int position) { return musicList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { TextView musicNameText; Music music = (Music) getItem(position); if(convertView == null){ convertView = getLayoutInflater().inflate(R.layout.music_item, null); musicNameText = (TextView) convertView.findViewById(R.id.musicNameText); convertView.setTag(musicNameText); }else{ musicNameText = (TextView) convertView.getTag(); } musicNameText.setText(music.getName()); return convertView; } }
musicListView = (ListView) view.findViewById(R.id.musicListView);musicAdapter = new MusicAdapter();musicListView.setAdapter(musicAdapter);
4、这里我为了方便,播放音乐直接放在了TimeService中,并把这个服务名改为了MyService。
先绑定服务,获取localBinder
//绑定服务Intent intent = new Intent(getActivity(), MyService.class);ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { localBinder = (MyService.LocalBinder) service; Log.i(TAG, "onServiceConnected: "); } @Override public void onServiceDisconnected(ComponentName name) { Log.i(TAG, "onServiceDisconnected: "); }};getActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE);
5、在MyService中写入播放音乐的方法
public void servicePlayMusic(Music music) { try { if(mediaPlayer == null){ mediaPlayer = new MediaPlayer(); } mediaPlayer.stop(); mediaPlayer.reset();// 避免点击第二首音乐后同时播放 mediaPlayer.setDataSource(music.getFile().getAbsolutePath()); // 保持prepare和start同步执行 mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mediaPlayer.start(); musicTimeThread = new MusicTimeThread(); musicTimeThread.start(); } }); }catch (IOException e){ e.printStackTrace(); }}
6、给ListView的每一个item布局添加点击事件,实现音乐播放。测试是否能正常播放
convertView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { localBinder.getService().servicePlayMusic(music); }});
7、在MyService中创建一个新的线程类,用于每隔一秒钟获取一次音乐的播放进度,原理和专注页面的计时线程相同。
public class MusicTimeThread extends Thread{ private final Object lock = new Object(); private boolean pause = false; /** * 调用该方法实现线程的暂停 */ void pauseThread(){ Log.i(TAG, "pauseTimeThread: "); pause = true; } /* 调用该方法实现恢复线程的运行 */ void resumeThread(){ Log.i(TAG, "resumeTimeThread: "); pause = false; synchronized (lock){ lock.notify(); } } /** * 这个方法只能在run 方法中实现,不然会阻塞主线程,导致页面无响应 */ void onPause() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public void run() { super.run(); try { while(true){ //当pause为true时,调用onPause挂起该线程 TimeUnit.SECONDS.sleep(1); while(pause) { onPause(); } Log.i(TAG, "run: "+mediaPlayer.getCurrentPosition()); } } catch (InterruptedException e) { e.printStackTrace(); } }}
8、在MusicFragment中注册一个用于接收音乐播放进度和播放总时长的本地广播,在MusicTimeThread中每隔一秒发送一次播放进度和总时长
public void RegisterProgressLocalBroadcast(){ IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("PROGRESS"); BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int duration = intent.getIntExtra("duration", 0); int current = intent.getIntExtra("current", 0); Log.i(TAG, "onReceive: "+duration+" "+current); } }; LocalBroadcastManager.getInstance(getActivity()).registerReceiver(broadcastReceiver, intentFilter);}
9、在MyService中写一个方法,用于发送当前播放进度和总时长的本地广播。并在run方法中每一秒钟发送一次。观察打印台信息,测试是否能够正常发送和接收广播。
public void serviceSendProgressBroadcast(){ // 发送当前进度的本地广播 Intent intent = new Intent(); intent.setAction("PROGRESS"); // 总时长 ms intent.putExtra("duration", mediaPlayer.getDuration()); // 当前播放进度 ms intent.putExtra("current", mediaPlayer.getCurrentPosition()); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); }// MusicTimeThread类中的run()@Overridepublic void run() { super.run(); try { while(true){ //当pause为true时,调用onPause挂起该线程 TimeUnit.SECONDS.sleep(1); while(pause) { onPause(); } serviceSendProgressBroadcast(); } } catch (InterruptedException e) { e.printStackTrace(); }}
10、在MusicFragment对应的布局中加入进度条ProgressBar,并在左边显示当前播放时间,在右端显示总时长。然后在接收到本地广播的时候将播放进度current和总时长duration显示出来。
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/musicListView" android:layout_width="match_parent" android:layout_height="match_parent"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:gravity="center" android:orientation="horizontal"> <TextView android:id="@+id/currentText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20dp" android:text="00:00" /> <ProgressBar android:id="@+id/musicProgressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" /> <TextView android:id="@+id/durationText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20dp" android:text="00:00" /> </LinearLayout></RelativeLayout>
public void RegisterProgressLocalBroadcast(){ IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("PROGRESS"); BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int duration = intent.getIntExtra("duration", 0); int current = intent.getIntExtra("current", 0); Log.i(TAG, "onReceive: "+duration+" "+current); //在广播接收事件时显示布局 ShowMusicProgress(duration, current); } }; LocalBroadcastManager.getInstance(getActivity()).registerReceiver(broadcastReceiver, intentFilter);}public void ShowMusicProgress(int duration, int current){ currentText.setText(""+current); durationText.setText(""+duration); progressBar.setMax(duration); progressBar.setProgress(current);}
11、显示的时长是毫秒数,我们需要定义一个方法将其转换成 00:00 的时间格式。由于ProgressBar组件不能拖动进度,这里换成了SeekBar。
public String handleMusicTime(int ms){ int min = (ms/1000) / 60; int sec = (ms/1000) % 60; String mm = String.valueOf(min); String ss = String.valueOf(sec); if(min<10){ mm = "0"+mm; } if(sec<10){ ss = "0"+ss; } return mm+":"+ss; }
public void ShowMusicProgress(int duration, int current){ currentText.setText(handleMusicTime(current)); durationText.setText(handleMusicTime(duration)); musicSeekBar.setMax(duration); musicSeekBar.setProgress(current); }
12、在MyService中写入方法,用于改变播放进度
public void setMediaPlayerProgress(int current){ Log.i(TAG, "setMediaPlayerProgress: "); mediaPlayer.seekTo(current);}
13、设置musicSeekBar的停止拖动事件,停止拖动时将进度传递给MyService改变播放进度
public void SetMusicSeekBarChangedListener(){ musicSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) { localBinder.getService().setMediaPlayerProgress(seekBar.getProgress()); } });}
14、实现自动播放下一首。将servicePlayMusic方法的参数改为音乐列表和第一首音乐的位置。MediaPlayer中有一个完成播放时的监听事件setOnCompletionListener,在该事件中调用传入的音乐列表的下一首就可以了,(注意对列表长度取余,否则会报超出范围的异常)。
另外,在每一首播放结束时,应该先暂停计时线程,在下一首播放时恢复计时线程。考虑到第一首播放时计时线程还未创建,应该做一个非空判断。
public void servicePlayMusic(List<Music> musicList, int start) { try { int size = musicList.size(); if(mediaPlayer == null){ mediaPlayer = new MediaPlayer(); } mediaPlayer.stop(); mediaPlayer.reset(); mediaPlayer.setDataSource(musicList.get(start).getFile().getAbsolutePath()); mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mediaPlayer.start(); if(musicTimeThread == null){ musicTimeThread = new MusicTimeThread(); musicTimeThread.start(); }else{ musicTimeThread.resumeThread(); } } }); mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { servicePlayMusic(musicList, (start+1)%size ); musicTimeThread.pauseThread(); } }); }catch (IOException e){ e.printStackTrace(); }}
15、添加暂停和继续按钮(同一个按钮),实现对播放的暂停和继续
在MyService中写入方法,用以暂停和继续播放(注意非空判断)
public void servicePauseMusic(){ if(mediaPlayer != null && mediaPlayer.isPlaying()){ mediaPlayer.pause(); }}public void serviceResumeMusic(){ if(mediaPlayer!=null){ mediaPlayer.start(); }}
给按钮设置点击事件
public void SetPauseResumeImageOnClick(){ musicPauseResumeImage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(localBinder.getService().musicIsPlaying()){ localBinder.getService().servicePauseMusic(); musicPauseResumeImage.setImageResource(R.drawable.resume_time); }else{ localBinder.getService().serviceResumeMusic(); musicPauseResumeImage.setImageResource(R.drawable.pause_time); } } });}
16、同样的,实现取消播放按钮事件,调用mediaPlayer.stop()
17、优化布局,完成!
项目源码在这:https://github.com/Lzh-Axq/adnroid-curriculum-design