iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
Mobile Development

Android Studio開發過程和介紹系列 第 29

【DAY 29】 將這個月所學集合成一個APP!(程式完善篇)

  • 分享至 

  • xImage
  •  

前言

這一篇將會是實作的最後一篇,本篇會講解登入畫面跟放置功能列表的介面,以及MainActivity的設定,最後這裡附上本次實作的GitHub

strings

    <string name="weather_function">天氣狀況</string>
    <string name="calculator_function">計算機</string>
    <string name="game_function">小遊戲</string>

新增function的名稱,在後面會使用name來抓取資料

SharedPreference

public class SharedPref {
    private SharedPreferences sharedPreferences;
    private SharedPreferences.Editor editor;
    public SharedPref(Context context){
        sharedPreferences = context.getSharedPreferences(
                context.getResources().getString(R.string.app_name),Context.MODE_PRIVATE);
        editor = sharedPreferences.edit();
    }

    public void setAct(String account){
        editor.putString("act",account).commit();
    }
    public void setPwd(String password){
        editor.putString("pwd",password).commit();
    }

    public String getAct(){return sharedPreferences.getString("act","error");}
    public String getPwd(){return  sharedPreferences.getString("pwd","error");}
}

MainActivity

這個部分就跟在Day26設定的一樣,這邊會簡單的帶過怎麼寫,詳細解釋可以看Day26

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener

首先要implements Navigation Drawer的監聽器

    private DrawerLayout drawerLayout;
    private NavigationView navigationView;
    private Toolbar toolbar;

接著將該宣告的物件宣告

    setFindById();
    setNavigationDrawer(savedInstanceState);

onCreate設定的部分

  • setNavigationDrawer

    setSupportActionBar(toolbar);
    navigationView.setNavigationItemSelectedListener(this);

    ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar,R.string.Nav_Open
                ,R.string.Nav_Close);
    drawerLayout.addDrawerListener(toggle);
    toggle.syncState();

    if (savedInstanceState == null){
        getSupportFragmentManager().beginTransaction().replace(R.id.frameLayout, new LoginFragment()).commit();
        navigationView.setCheckedItem(R.id.login);
    }

這邊就是Navigation Drawer的基本設定,包括:Navigation Drawer的點擊事件設定、剛開始介面的設定、ToolBar的設定

  • Navigation Drawer的點擊事件

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        if (item.getItemId() == R.id.login){
            getSupportFragmentManager().beginTransaction().replace(R.id.frameLayout, new LoginFragment()).commit();
            //當選到login選項時,就跳到login的分頁
        } else if (item.getItemId() == R.id.home) {
            getSupportFragmentManager().beginTransaction().replace(R.id.frameLayout, new HomeFragment()).commit();
            //當選到home選項時,就跳到home的分頁
        } else if (item.getItemId() == R.id.logout) {
            Toast.makeText(this,"以登出",Toast.LENGTH_SHORT).show();
            getSupportFragmentManager().beginTransaction().replace(R.id.frameLayout, new LoginFragment()).commit();
            //當選到logout時就跳回login的頁面,並且顯是以登出
        }
        drawerLayout.closeDrawer(GravityCompat.START);//設定像開始的地方關閉Navigation Drawer
        return true;
    }

當Navigation Drawer的選項被點擊時,就會呼叫這個方法,這裡就是設定會去判斷選到的選項的id,依據不同id就做不同的事,並且設定Navigation Drawer關閉的時候關閉方向是往左側關閉

  • onBackPressed

    @Override
    public void onBackPressed() {
        if (drawerLayout.isDrawerOpen(GravityCompat.START)){
            drawerLayout.closeDrawer(GravityCompat.START);
        }else{
            super.onBackPressed();
        }
    }

這個部分事在改寫當使用者按下手機的返回後執行的事,如果沒做修改當使用者按下返回鍵就會將App關閉
多了這個指令就可以設定成當選單是展開狀態時,按下返回鍵只會關閉選單而不是整個App關閉

LoginFragment

  • 成果展示


這個部分我使用了MVP架構,因此這裡要先建立LoginContract、LoginPresenter

  • LoginContract

public interface LoginContract {
    interface view{
        void accountInformationSuccess(boolean success);
        void successfulSetAccountInformation(boolean success);
    }
    interface presenter{
        void checkAccountInformation(String account, String password);
        void setAccountInformation(String account, String password);
    }
}
  • LoginPresenter

public class LoginPresenter implements LoginContract.presenter{
    private LoginContract.view view;
    private SharedPref pref;
    public LoginPresenter(LoginContract.view view, Context context){
        this.view = view;
        pref = new SharedPref(context);
    }
    @Override
    public void checkAccountInformation(String account, String password) {
        String act = pref.getAct(); String pwd = pref.getPwd();
        Log.d("test", "checkAccountInformation: "+act+"\n"+pwd);
        if (act.equals(account) && pwd.equals(password)) view.accountInformationSuccess(true);
        else view.accountInformationSuccess(false);
    }

    @Override
    public void setAccountInformation(String account, String password) {
        if (account.isEmpty() || password.isEmpty()) view.successfulSetAccountInformation(false);
        else {
            pref.setAct(account);  pref.setPwd(password);
            view.successfulSetAccountInformation(true);
        }
    }
}

因為這次將帳號密碼的相關設定寫到Presenter裡面,所以這裡就要使用到SharedPreference,但是因為SharedPreference在初始化時要用到context,所以這裡的建構元就要要求調用的介面也傳入context,這樣在初始化時才可以直接使用

  • LoginFragment

public class LoginFragment extends Fragment implements LoginContract.view{
    private EditText et_act,et_pwd;
    private Button btn_register,btn_login;
    private Dialog dialog;
    private LoginPresenter presenter;
    //宣告

就跟前面介紹的MVP架構一樣,這裡要implements 接口的view

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dialog = new Dialog(getContext());
        presenter = new LoginPresenter(this,getContext());
    }//初始化

接著在onCreate進行初始化

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_login, container, false);
    }//連結布局檔

綁定布局

  • setListener

    private void setListener() {
        btn_login.setOnClickListener(view -> presenter.checkAccountInformation(
                et_act.getText().toString(),et_pwd.getText().toString()));
        //按下 登入 按鈕後,就將輸入的帳號密碼丟到Presenter幫忙檢查,往下看到accountInformationSuccess
        btn_register.setOnClickListener(view -> setRegisterDialog());
        //按下 註冊 按鈕後,就開啟註冊的對話框,往下看到setRegisterDialog
    }//設定監聽器

按下登入按鈕時就呼叫presenter裡面的檢查帳號密碼的方法
按下註冊按鈕就呼叫設定對話框的方法,這邊先看到當檢查帳密傳回成功的訊息後會做的事

  • accountInformationSuccess

    @Override
    public void accountInformationSuccess(boolean success) {
        if (success){//如果帳戶資料比對成功,就創建一個Home的Fragment並且將LoginFragment替換掉
            Toast.makeText(getContext(),"成功登入",Toast.LENGTH_SHORT).show();
            // 創建 HomeFragment 實例
            Fragment homeFragment = new HomeFragment();

            // 使用 FragmentManager 替換當前的 Fragment
            FragmentTransaction transaction = getFragmentManager().beginTransaction();
            transaction.replace(R.id.frameLayout, homeFragment);
            transaction.addToBackStack(null); // 如果需要加入回退堆疊,可以使用 addToBackStack
            transaction.commit();
        }else Toast.makeText(getContext(),"帳號或密碼錯誤",Toast.LENGTH_SHORT).show();
        //比對結果為失敗,就用Toast告知帳號密碼輸入錯誤
    }//繼承Contract的view後得到的方法,這個方法在點擊 登入 按鈕後,會將比對帳密的成果回傳到view,也就是這裡

用if判斷當傳回true時就執行跳轉Fragment的工作,傳回false就用Toast提醒帳密輸入錯誤

  • setRegisterDialog

    private void setRegisterDialog() {
        dialog.setContentView(R.layout.register_dialog);
        EditText act_register = dialog.findViewById(R.id.act_register);
        EditText pwd_register = dialog.findViewById(R.id.pwd_register);
        CheckBox checkBox = dialog.findViewById(R.id.checkbox_register);
        Button btn_create = dialog.findViewById(R.id.btn_create);
        //綁定dialog的頁面跟綁定dialog的物件id

        btn_create.setEnabled(false);
        checkBox.setChecked(false);
        //將建立按鈕跟checkbox初始化
        dialog.show();
        //設定完成將對話框顯示出來

        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                if (checkBox.isChecked()) btn_create.setEnabled(true);//如果checkbox有打勾,就解鎖 註冊 按鈕
                else btn_create.setEnabled(false);//反之就鎖定 註冊 按鈕
            }
        });//checkbox的監聽器

        btn_create.setOnClickListener(view -> presenter.setAccountInformation(//將資料丟到Presenter幫忙判斷是否有不合個的帳密出現(空白)
                act_register.getText().toString(),pwd_register.getText().toString()));//註冊按鈕的點擊事件
    }//設定註冊的對話框

首先綁定在Dialog裡面的物件id,接著將按鈕設定成不能點選的狀態,以及將CheckBox設定成為選取的狀態
接著CheckBox設定成當CheckBox是選取狀態時就把按鈕解鎖
最後註冊按鈕下後會呼叫presenter的檢查要註冊的帳號密碼的方法

    @Override
    public void successfulSetAccountInformation(boolean success) {
        if (success) dialog.dismiss();//比對註冊的帳密格式是否正確後,會回傳是否成功的訊息,成功就將對話框關閉
        else Toast.makeText(getContext(),"帳號或密碼不能為空白",Toast.LENGTH_SHORT).show();//反之就用Toast說明原因
    }//判斷註冊的帳密是否符合規定

接著當註冊的帳密被檢驗為合乎規定後,就會回傳true,這裡接收到後就將對話框關閉,反之就說明失敗的原因

HomeFragment

  • 成果展示


這個部分的主體為一個RecyclerView,而功能會以CardView包裝起來並放到RecyclerView顯示,並且使用了浮動式按鈕點下後就會跳出一個Dialog,Dialog裡面有CheckBox,當CheckBox選擇狀態時就為陣列加入被選中的功能的資料,按下新增按鈕就更新RecyclerView,以此來增加或減少RecyclerView裡面資料的數量

public class HomeFragment extends Fragment {
    private FloatingActionButton floatingActionButton;
    private Dialog dialog;
    private ArrayList<String> mArrayList;
    private ArrayList<Integer> picture;
    private RecyclerView recyclerView;
    private HomeListAdapter homeListAdapter;

宣告,因為有圖片跟功能名稱這兩個資料要傳到RecyclerView,所以新增了兩個陣列來儲存相關的資料

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mArrayList = new ArrayList<>();
        picture = new ArrayList<>();
        dialog = new Dialog(getContext());
        //初始化
    }

初始化

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_home, container, false);
        //綁定介面
    }

綁定介面

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        setFindById(view);//綁定物件id
        setListener();//設定監聽器
    }

onViewCreated的設定

    private void setListener() {
        floatingActionButton.setOnClickListener(view -> setDialog());
        //這裡的浮動式按鈕做的不是關閉介面,而是設定Dialog
    }//設定物件的監聽氣

監聽器設定

  • setDialog

        dialog.setContentView(R.layout.addfunction_dialog);
        CheckBox weather = dialog.findViewById(R.id.weather_function);
        CheckBox game = dialog.findViewById(R.id.game_function);
        CheckBox calc = dialog.findViewById(R.id.calculator_function);

        Button add = dialog.findViewById(R.id.add_btn);
        //對話框物件的id綁定

        dialog.setCanceledOnTouchOutside(false);
        //將對話框設定為按對話框的區域也不會關閉,因此一定要按下新增按鈕才可以關閉
        if (mArrayList.contains("天氣狀況")) weather.setChecked(true);
        if (mArrayList.contains("計算機")) calc.setChecked(true);
        if (mArrayList.contains("小遊戲")) game.setChecked(true);
        //先判定之前有沒有將function的資料放進陣列裡面,有的話就讓checkbox設定成以打勾

        dialog.show();
        //顯示對話框

首先綁定在Dialog的介面以及物件id,接著原本Dialog在點擊對話框之外的區域時會直接關閉Dialog,但是在這次實作這麼做會讓更新RecyclerView時有問題,所以索性就設定必須要按下新增按鈕才可以關閉Dialog,強制讓RecyclerView可以更新
接著設定CheckBox如果陣列裡面已經有功能的資料存在了,就將該CheckBox設定為選取狀態,最後再將dialog顯示出來

    add.setOnClickListener(view -> {
        setRecyclerView(mArrayList, picture);
        //按下按鈕後就將跟新過的資料傳給RecyclerView設定
        homeListAdapter.notifyDataSetChanged();
        //按下按鈕後就提醒RecyclerView更新內容
        dialog.dismiss();
        //按下按鈕後就關閉對話框
        Toast.makeText(getContext(),"功能列表以更新",Toast.LENGTH_SHORT).show();
        //用Toast提醒功能列表已更新
    });//新增按鈕的點擊事件

新增按鈕的點擊事件,按下後將資料傳給RecyclerView並更新RecyclerView,接著關閉對話框

        weather.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                if (weather.isChecked()) {//當天氣checkBox有被點擊後,就檢查現在的狀態,如果是打勾狀態就將資料丟給各自代表的陣列
                    int functionResourceId = getResources().getIdentifier("weather_function","string",getContext().getPackageName());
                    mArrayList.add(getResources().getString(functionResourceId));
                    //這次使用抓String的name的方式來指定加入的資料
                    picture.add(R.drawable.weather);
                    //將天氣相關的圖片加進picture
                }else {
                    mArrayList.remove("天氣狀況");
                    //當checkBox被點擊時,結果當下狀態不是打勾,就所以
                    int index = picture.indexOf(R.drawable.weather);
                    if (index != -1) {
                        picture.remove(index);
                    }
                }
            }
        });//checkBox的設定

CheckBox的設定,當CheckBox的選項選取狀態有變動時,先判斷是否為選取狀態,接著把對應的資料加進對應的陣列,功能的名稱在string撰寫再以名稱查找資料,並加進陣列之中,圖片的資料則是直接讀取放在drawable中對應的圖片的id
當CheckBox是取消選取選項時就將對應的資料從陣列中移除,剩下的計算機和小遊戲的CheckBox也是一樣的設計,這邊就跳過不展示了

  • setRecyclerView

    private void setRecyclerView(ArrayList<String> mArrayList, ArrayList<Integer> picture) {
        homeListAdapter = new HomeListAdapter(mArrayList, picture, getContext());
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
        recyclerView.setAdapter(homeListAdapter);
    }

RecyclerView的設定,像裡面傳入兩個儲存資料的陣列,還有傳入context,傳入context是為了要執行跳轉頁面(Intent)的指令

HomeListAdapter

public class HomeListAdapter extends RecyclerView.Adapter<HomeListAdapter.ViewHolder> {
    private ArrayList mArrayList;
    private ArrayList<Integer> pictureList;
    private Context context;
    public HomeListAdapter(ArrayList<String> arrayList, ArrayList<Integer> picture, Context context){
        this.mArrayList = arrayList;
        this.pictureList = picture;
        this.context = context;
    }

RecyclerView的基本設定,以及建構元的設定

    public class ViewHolder extends RecyclerView.ViewHolder {
        private ImageView imageView;
        private TextView textView;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.function_image);
            textView = itemView.findViewById(R.id.function_name);
        }
    }

綁定物件id

    @NonNull
    @Override
    public HomeListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recyclerview_item,parent,false);
        return new ViewHolder(view);
    }

綁定RecyclerView的item

        holder.textView.setText(mArrayList.get(position).toString());
        holder.imageView.setImageResource(pictureList.get(position));

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent();
                switch (mArrayList.get(position).toString()){
                    case "天氣狀況":
                        intent.setClass(context, WeatherFunction.class);
                        break;
                    case "計算機":
                        intent.setClass(context, CalculatorFunction.class);
                        break;
                    case "小遊戲":
                        intent.setClass(context, GameFunction.class);
                        break;
                }
                context.startActivity(intent);
            }
        });

設定將陣列內的資料填到物件,並且設置點擊事件,點擊後先判斷點擊的CardView的選項為哪一個,就跳到哪一個功能的介面

    @Override
    public int getItemCount() {
        return mArrayList.size();
    }

最後就是依據傳入的陣列長度設定RecyclerView的長度

以上就是整個實作的講解,下一篇就是最後一篇,那我們就最後一篇再見囉~


上一篇
【DAY 28】 將這個月所學集合成一個APP!(功能設計篇)
下一篇
【DAY 30】完賽感言
系列文
Android Studio開發過程和介紹30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言