主頁 > 移動端開發 > 手把手教你完成Android期末大作業(多功能應用型APP)

手把手教你完成Android期末大作業(多功能應用型APP)

2021-12-23 09:53:05 移動端開發

前言

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 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
@Override
public 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方法中每一秒發送一次本地廣播,運行測驗是否正常

@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, 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()
@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, 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://www.uj5u.com/yidong/390661.html

標籤:其他

上一篇:IPv6與VoIP(第二部分)

下一篇:10分鐘用Flutter寫出摸魚桌面版App

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more