手把手教你完成Android期末大作业多功能应用型APP

前言

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.java
public 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 Weather

public class Weather {
    private String city;		//城市名
    private String update_time;	//更新时间
    private List<DayData> data;	//每天的天气数据列表,data.get(0)为当天数据

    /* 
    	getter and setter 
    */
}

// class DayData
public 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 Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.task, 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
        );
        
        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
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.task, 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,
        getResources().getColor(R.color.studyColor,
        getResources().getColor(R.color.lifeColor,
    };

    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", "level","content", "info",
        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.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.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方法中每一秒发送一次本地广播,运行测试是否正常

@Override
public 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, 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, 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()
@Override
public 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, "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

原文地址:https://blog.csdn.net/Dae_Lzh

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具
文章浏览阅读2.1k次。初学Android游戏开发的朋友,往往会显得有些无所适从,他们常常不知道该从何处入手,每当遇到自己无法解决的难题时,又往往会一边羡慕于 iPhone下有诸如Cocos2d-iphone之类的免费游戏引擎可供使用,一边自暴自弃的抱怨Android平台游戏开发难度太高,又连个像样的游 戏引擎也没有,甚至误以为使用Java语言开发游戏是一件费力不讨好且没有出路的事情。事实上,这种想法完全是没有必_有素材的游戏引擎
文章浏览阅读3.2k次,点赞2次,收藏2次。2014年12月从csdn专家福利获得的一本书《Android游戏开发技术实战详解》,尘封了一年多的时间,今天才翻开来看。我认识中的Android,提到Android最先浮现在我脑海中的是那可爱的机器人图标:这个Logo是由Ascender公司设计的,诞生于2010年,其设计灵感源于男女厕所门上的图形符号(真的是灵感无处不在),于是布洛克绘制了一个简单的机器人,它的躯干就像锡罐的形状,头上还有两根_智能手机的特点有哪些?
文章浏览阅读8.1k次,点赞9次,收藏11次。首先,Android是不是真的找工作越来越难呢?这个可能是大家最关心的。这个受大的经济环境以及行业发展前景的影响,同时也和个人因素有关。2016-08-26近期一方面是所在的公司招聘Java开发人员很难招到合适的,投简历的人很少;而另一方面,经常听身边的人说Android、iOS方面找工作不好找,特别是没什么经验的,经验比较少的!说是不好找,但在我家所在的吉林省省会长春,会Unity3D+Maya_android 开发和asp.net哪个好 site:blog.csdn.net
文章浏览阅读6.1k次。在上篇“走进Android开发的世界,HelloWorld”,我们创建了一个Android 项目 HelloWorld,并演示了如何通过USB连接手机查看运行效果;而如果没有手机或没有对应型号的手机,又想做对应型号(屏幕尺寸、Android系统版本)的适配,应该怎么办呢?这时Android模拟器就派上用场了。Android模拟器Android SDK自带一个移动模拟器。它是一个可以运行在你电脑上的_安卓移动开发软件怎样预览
文章浏览阅读8.9k次。Google IO 2017 上宣布,将Kotlin语言作为安卓开发的官方语言。Kotlin由JetBrains公司开发,与Java 100%互通,并具备诸多Java尚不支持的新特性。谷歌称还将与JetBrains公司合作,为Kotlin设立一个非盈利基金会。Kotlin 是一个基于 JVM 的静态类型编程语言,Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JV_kotlin为什么被嫌弃
文章浏览阅读9.6w次,点赞17次,收藏35次。有些情况下,不方便使用断点的方式来调试,而是希望在控制台打印输出日志,使用过Eclipse的同学都知道Java可以使用 System.out.println(""); 来在控制台打印输出日志,但是在android studio中却是不行的,还是有差别的,那应该用什么呢?android.util.Log在调试代码的时候我们需要查看调试信息,那我们就需要用Android Log类。android.ut_andirod.studio 为什么不在控制台打印输出
文章浏览阅读8.2k次,点赞2次,收藏8次。在上篇“走进Android开发的世界,HelloWorld”,我们创建了一个Android 项目 HelloWorld,并演示了如何通过USB连接手机查看运行效果;这里讲一下如何为应用添加一个按钮,并为按钮添加Click单击事件处理程序,显示/隐藏另一个按钮。添加按钮在HelloWorld项目的基础上,打开界面布局文件:activity_main.xml切换到Design(设计)模式;在组件But_activity_main.xml按钮隐藏
文章浏览阅读2.9k次,点赞3次,收藏9次。android 开发工具主流的还是Android Studio,当然也有很多人喜欢用Eclipse,也有人喜欢用IntelliJ IDEA ;还有Xamarin这种只需要编写一次代码,可以编译多种平台可运行的强大工具。但是它又真的强大吗?就我看来没有,身边很多人还是在用Android Studio、XCode开发应用,没见谁在用Xamarin之类的工具。系统要求WindowsMicrosoft®_android开发下载安装
文章浏览阅读4.2k次,点赞7次,收藏26次。你知道Hello World程序的由来吗?对于大多数编程语言的学习来说,真正入门的一课就是 Hello World!会而不难,难而不会。虽然很多人写过关于Android开发Hello World的文章,但随着时间的推移,开发工具、技术的进步,可能有些已经过时了。我就记录一下当下我所经历的第一个Android APP HelloWorld。一、准备1、开发环境参考:Android Studio 下载_android helloworld textview 句柄获取
这篇“android轻量级无侵入式管理数据库自动升级组件怎么实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定...
今天小编给大家分享一下Android实现自定义圆形进度条的常用方法有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文...
这篇文章主要讲解了“Android如何解决字符对齐问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Android...
这篇文章主要介绍“Android岛屿数量算法怎么使用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Android岛屿数量算...
本篇内容主要讲解“Android如何开发MQTT协议的模型及通信”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Andro...