---
# System prepended metadata

title: 期中報告：購物車
tags: [android]

---

期中報告：購物車
===
###### tags: `android`

android目錄：https://hackmd.io/W9hqd3gSTkWGfsWhRIDyDA
點心菜單：https://hackmd.io/8ducRnXHTrWR21KDnhRMAg

購物車排版
---

| 排列 | 畫面 | 
| -------- | -------- | 
|![](https://i.imgur.com/eFFu01m.png)| ![](https://i.imgur.com/ekfZPNz.png)|


以下是xml檔－根佈局LinearLayout以及所有控件：
```xml=
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ShoppingCartActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:background="#FFAAD5">

        <TableLayout
            android:id="@+id/table"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:stretchColumns="1"/>
    </ScrollView>

    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:stretchColumns="*">

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/totalPrice"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_span="3"
                android:gravity="center"
                android:text="總金額：NT$0"
                android:textSize="24sp" />
        </TableRow>

        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <Button
                android:id="@+id/shop"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:onClick="toShop"
                android:text="回商店"
                android:textSize="18sp" />

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="space"
                android:visibility="invisible" />

            <Button
                android:id="@+id/checkout"
                (...)/>
        </TableRow>

    </TableLayout>

</LinearLayout>
```

EditText
---

<h3>1. 啟動時不自動成為焦點</h3>

```xml=
<LinearLayout
    android:focusable="true"
    android:focusableInTouchMode="true">
```

將EditText的父組件設定focusableInTouchMode="true"，這樣該View或Layout就可以被Focus到，本來想使用clearfocus來移除焦點，但無效用是因為：它實際上是有效的，但啟動時Android會由上往下依序尋找可聚焦的物件重新設置焦點，而第一個符合的依舊是EditText，因此使他的上層符合條件就可以了。

移除焦點：
```java=
edittext.clearFocus();
```
重新獲得焦點：
```java=
//開啟軟鍵盤的其中一個方法
this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
editText.requestFocus(); //獲得焦點並顯示光標
```

<h3>2. 點擊輸入框外部自動關閉鍵盤1</h3>

也就是輸入框有焦點的時候彈出軟鍵盤，失去焦點的時候隱藏軟鍵盤。

首先除了上層LinearLayout要增加focusable和InTouchMode="true"兩條屬性外，再加入clickable屬性，讓它可被點擊。
```xml=
<LinearLayout
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true">
```
增加一個監聽EditText焦點改變介面後再分配給多個EditText：
```java=
editText.setOnFocusChangeListener(etFocusListener);
```
```java=
private EditText.OnFocusChangeListener etFocusListener = new EditText.OnFocusChangeListener() {
    @Override
    public void onFocusChange(View view, boolean hasFocus) {
        //若失去焦點
        if (!hasFocus) {
            //獲取軟鍵盤物件
            InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
            //隱藏它
            im.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
        }
    }
};
```
當點擊輸入框的外部，也就是其父控件LinearLayout包含的區塊，焦點就會轉移到LL去，輸入框失去焦點時會自動隱藏光標和線條高亮的效果。
```
android:clickable="true"
android:focusableInTouchMode="true"
```
如果將這兩個屬性設置給任一控件(如按鈕)都可以達到使輸入框失去焦點的效果。

而這個方法的缺點是：難道我要給每一個控件及組件都設置該屬性？這樣才能確保使用者點擊任何一個地方都能使EditText失去焦點進而達到這個效果，而且當我聚焦一個輸入框時再點擊另一個輸入框，鍵盤會隱藏再開啟，這是一個沒必要的動作。

註：開啟軟鍵盤及獲取輸入法的開啟狀態
```java=
im.showSoftInput(view, InputMethodManager.SHOW_FORCED); //開啟軟鍵盤
boolean isOpen = im.isActive(); //isOpen為true，則表示輸入法開啟
```


| 顯示軟鍵盤屬性 | 隱藏軟鍵盤屬性 | 
| -------- | -------- |
| ![](https://i.imgur.com/j78myXw.png) | ![](https://i.imgur.com/jfjh3Pt.png) ||






<h3>3. 點擊輸入框外部自動關閉鍵盤2</h3>

:::    success
這個作法配合舊的即時計算價格寫法(點擊CheckBox讓EditText失去焦點)：
- 勾選和取消CheckBox時都會移除EditText的焦點，關閉鍵盤
- 點擊另一個輸入框不會關閉鍵盤
- 除了使用習慣不同以外咩有Bug
:::

在根佈局與上層LinearLayout加入這三行，這樣就會在點擊空白處時把焦點移到其上。
```xml=
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:clickable="true">
</LinearLayout>
```
```xml=
<LinearLayout
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true">
</LinearLayout>
```

:::info
上層LinearLayout一定要加，才能點擊空白處移除焦點，如果不加根佈局的三行，有時勾選列表下方的商品，移除焦點時會使上面的EditText取得焦點，那再點擊上面的CheckBox就會出現錯誤讓移除焦點沒有作用，一樣會發生重複計算（觸發EditText文字監聽）的情況。
:::

覆寫dispatchTouchEvent這個function，對事件進行攔截：

```java=
/**
 * 在 Activity 的 dispatchTouchEvent 方法中進行一系列判斷，
 * 點擊界面中的任何非 EditText 的部分，軟鍵盤都會收起來，
 * 不需要再具體的對每一個 EditText 進行處理。
 * @param ev
 * @return
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        // 獲得當前得到焦點的View，一般情况下就是EditText（特殊情况就是軌跡球或者實體鍵盤會移動焦點）
        View view = getCurrentFocus();

        //★使用isShouldHideInput(view, event)進行判斷
        //★判斷點擊的控件是否為EditText，若不是就要隱藏鍵盤
        if (isShouldHideInput(view, ev)) {
            hideSoftInput(view.getWindowToken());
        }
    }
    //必不可少，否則就不會調用dispatch下面的onTouchEvent，所有事件都沒有onTouchEvent了，事件不會繼續進行
    //super.調用父類的成員函式，傳遞動作事件
    return super.dispatchTouchEvent(ev);
}
```
隐藏軟鍵盤的其中一種方法：
```java=
private void hideSoftInput(IBinder token) {
    if (token != null) {
        InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        im.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS);
    }
}
```


<h3>4. 判斷點擊的控件是否為editText</h3>

通過這個可以判斷控件a是否為b類型。
```java=
a instanceof b
```

思路：
1. 遍歷根節點中的子控件，如果某个子控件區域包含點擊座標，停止遍歷根目錄的子控件，遞迴調用找到子控件，直到找到頂層View。
2. 如果遍歷最後一層結束，ViewGroup中沒有找到子控件，就返回ViewGroup/View。

首先Android布局的根節點是DecorView，並呈現為多叉樹結構；每個頂層View都是一個葉節點。

背景知識：
Android的dispatch事件時是從最後一個子節點開始傳遞，下圖的ViewGroup.java的dispatchTouchEvent函式在遍歷子View時，是從最後一個子View開始分發事件的。

![](https://i.imgur.com/NiC3QLW.png)

:::warning
如果當前ViewGroup是LinearLayout，從前向後遍歷也行；但如果是FrameLayout或RelativeLayout就必須從後向前，因為後面的子View可以覆蓋前面的子View。
:::

對於Android界面可以從DecorView開始查找，DecorView就是整個界面的最頂層View，
一般情况下DecorView内部會包含一個豎直方向的LinearLayout，這個LinearLayout里面有上下兩個部分，上面是標題欄，下面是内容欄。

如果點擊的座標x,y在控件的範圍內則繼續向下遞迴，為了找到葉節點(即最裡面的View)需要深度優先；其中.getChildCount()函式返回的是直接子元素的個數。

```java=
/**
 * 找到用戶點擊座標的控件，來判斷是否隱藏鍵盤，因為當用戶點擊EditText控件時沒必要隱藏
 * @param v
 * @param event
 * @return
 */
private boolean isShouldHideInput(View v, MotionEvent event) {
    int X = (int)event.getX();
    int Y = (int)event.getY();
    //★若當前焦點是EditText
    if (v != null && (v instanceof EditText)) {
        // ★如果點擊發生在EditText上則忽略，不隱藏鍵盤
        if (getViewByPosition(getWindow().getDecorView(),X,Y) instanceof EditText) {
            return false;
        } else {
            return true;
        }
    }
    //如果焦點不是EditText則忽略，這個發生在視圖剛繪製完，第一個焦點不在EditView上，和用戶用軌跡球選擇其他的焦點
    return false;
}

private View getViewByPosition(View view, int x,  int y) {
    if (view == null) {
        return null;
    }

    //得到該頂層View的位置區塊
    int[] location = new int[2];
    view.getLocationInWindow(location);

    int left = location[0];
    int top  = location[1];
    int right = left + view.getWidth();
    int bottom = top + view.getHeight();

    if (view instanceof ViewGroup) { //如果當前是ViewGroup容器
        //獲取該ViewGroup子元素的個數
        int childCount = ((ViewGroup)view).getChildCount();
        //深度優先， 從最後一個子節點開始遍歷(從後向前)，如果找到就返回該View。先遞迴判斷子View
        if (childCount > 0) {
            for (int i = childCount - 1; i >= 0; i--) {
                View topView = getViewByPosition(((ViewGroup)view).getChildAt(i), x, y);
                if (topView != null) {
                    return topView;
                }
            }
        }

        //子View都沒找到匹配的， 再判斷自己
        if (left < x && top < y && right > x && bottom > y) {
            return view;   //當前ViewGroup就是topView
        } else {
            return null; //沒找到匹配的
        }
    } else { //當前是View
        if (left < x && top < y && right > x && bottom > y) {
            return view;   //當前View就是頂層View
        } else {
            return null; //沒找到匹配的
        }
    }
}
```

<h3>5. 輸入時將不需要的TableRow隱藏</h3>

- 先宣告變數→ private TableRow TR;
- 在onCreate中→ TR = (TableRow) findViewById(R.id.TR);
- 接下來 ↓ EditText.OnFocusChangeListener
```java=
private EditText n1;
```
```java=
(n1 = (EditText) findViewById(R.id.num1)).setOnFocusChangeListener(etFocusListener);
```
```java=
private final EditText.OnFocusChangeListener etFocusListener = new EditText.OnFocusChangeListener() {
    @Override
    public void onFocusChange(View view, boolean hasFocus) {
        if (hasFocus) {
            //此處為獲得焦點時的處理內容
            //焦點在EditText上，隱藏TableRow
            TR.setVisibility(View.GONE);
        }else{
            //此處為失去焦點時的處理內容
            //焦點不在EditText上，顯示TableRow
            TR.setVisibility(View.VISIBLE);
        }
    }
};
```

BTW，動態的把某行隱藏：
```
TableLayout.setColumnCollapsed(colimnIndex,isCollapsed) 
```

| 非輸入時隱藏小鍵盤，顯示TableRow | 輸入時顯示小鍵盤，隱藏TableRow | 
| -------- | -------- | 
|![](https://i.imgur.com/zsjcVda.png)|![](https://i.imgur.com/cVVCOKx.png)| 

<h3>6. 監聽輸入行為</h3>

```java=
editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        //輸入變化前執行
    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        //輸入文本發生變化時執行
    }

    @Override
    public void afterTextChanged(Editable editable) {
        //輸入文本後執行
    }
});
```


即時計算價格
---

要做到即時處理也就是要讓使用者在做每個會影響到價格的操作時都要改變價格，會影響到價格的操作有三個：
- 勾選/取消商品的主選項CheckBox
- 勾選/取消商品的附加選項CheckBox
- 改變EditText輸入框中的商品數量

在onCreate中做主選項、附加選項、輸入框的監聽。

::: warning
這裡的edittext.setText("設定文字")都會觸發edittext的TextChangedListener監聽事件。
竟然會觸發，那就都交給EditTextChangePrice函式去計算價格~O(^ _ ^)O
:::


1. 主選項checkbox的OnCheckedChangeListener監聽事件：
```java=
checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
        //獲取輸入字串
        String inputStr = edittext.getText().toString();

        //點擊位置非輸入框~移除焦點(鍵盤關閉、顯示TR)
        edittext.clearFocus();

        //若主選項有勾
        if(checkbox.isChecked()){
            //顯示附加選項
            subCheckbox.setVisibility(View.VISIBLE);
            //如果未輸入數量，就設為1
            if (inputStr.equals("")){
                edittext.setText("1");
            }else{
                edittext.setText(inputStr);
            }

        }else{
            //隱藏及取消附加選項
            subCheckbox.setVisibility(View.INVISIBLE);
            subCheckbox.setChecked(false);

            edittext.setText("");
        }
    }
});
```

2. 附加選項checkbox的OnCheckedChangeListener監聽事件：

```java=
subCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
            edittext.setText(edittext.getText().toString());
    }
});
```
3. 輸入框edittext的TextChangedListener監聽事件：

```java=
edittext.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}

    @Override
    public void afterTextChanged(Editable editable) {
        tempPrice = etChangePrice(tempPrice);
    }

    public int etChangePrice(int temp) {
        //輸入數量
        int inputStr;

        //第一個數字不能是0，若字串有內容才能擷取子字串做判斷
        if (edittext.getText().length() > 0) {
            if (edittext.getText().toString().substring(0, 1).equals("0")) {
                //♦會遞迴呼叫自己
                edittext.setText("1");
            }
        }

        //★先把之前的價格刪除
        price -= temp;

        //獲取輸入數量
        if (edittext.getText().toString().equals("")) {
            inputStr = 1;
        } else {
            inputStr = Integer.parseInt(edittext.getText().toString());
        }

        //★重新計算
        //計算主選項的價格
        temp = p * inputStr;
        //計算附加選項的價格
        if (subCheckbox.isChecked()) {
            temp += addP * inputStr;
        }

        //★有勾選就加入price，沒有就不加
        if (checkbox.isChecked()) {
            price += temp;
        } else {
            temp = 0;
        }
        
        //更新顯示價格
        priceOutput.setText("小計NT$" + price);
        return temp;
    }
});
```

♦在判斷第一個字輸入0後，遞迴呼叫自己的部分：
:::info
//輸入0
00:32:41.537 31658-31658/package I/My: In etChangePrice
//設定為1
00:32:41.546 31658-31658/package I/My: In etChangePrice
//執行etChangePrice
00:32:41.546 31658-31658/package I/My: In etChangePrice major isChecked, price = 30
00:32:41.547 31658-31658/package I/My: Over etChangePrice, temp = 30
//繼續接下來的code
00:32:41.547 31658-31658/package I/My: In etChangePrice major isChecked, price = 30
00:32:41.547 31658-31658/package I/My: Over etChangePrice, temp = 30
:::

如果給每個CheckBox和EditText都做監聽和處理，code會長爆，所以將處理寫成兩個函式，一個是CheckBox選取狀態改變，一個是EditText的輸入行為變化，再將監聽事件改為**switch case同時監聽多個控件**。

<h3>CheckBoxChangePrice</h3>

主選項CheckBox和附加選項CheckBox的改變價格函式。
```
CheckBoxChangePrice(CheckBox 主選項, EditText 輸入框, int 價格, int 暫存價格, boolean 勾選的是否為附加選項, CheckBox... 附加選項陣列)
```
```java=
private CheckBox f1, f1_c1, f1_c2, f2, f2_c1;
private EditText n1, n2;

private int price = 0;
private TextView priceOutput;

private int addP = 10, p1 = 80, p2 = 40;
private int tempPrice1,tempPrice2;
```
```java=
public int cbChangePrice(CheckBox f, EditText n, int p, int temp, boolean isAdd, CheckBox... addFList){
    int inputStr;

    //★點擊CheckBox，就把EditText的焦點移除
    //♦避免觸發EditText監聽，執行etChangePrice，重複加入價格
    n.clearFocus();

    //若輸入框為空值，設值為1，才可以獲取inputStr
    if (n.getText().toString().equals("")){
        n.setText("1");
    }

    inputStr = Integer.parseInt(n.getText().toString());
    
    //★點擊的是主選項，為勾選狀態
    if (!isAdd & f.isChecked()){
        //顯示附加選項
        for (CheckBox checkbox : addFList) {
            checkbox.setVisibility(View.VISIBLE);
        }

        temp = p * inputStr; //主選項金額
        price += temp;
    }
    //★點擊的是附加選項
    else if (isAdd & f.isChecked()){
        //先把之前的價格刪除
        price -= temp;

        //重新計算該商品的主選項+附加選項價格
        temp = p * inputStr;
        
        for (CheckBox checkbox : addFList) {
            if (checkbox.isChecked()){
                temp += addP *inputStr;
            }
        }

        //加入重新計算後的價格
        price += temp;
    }
    //★點擊的是主選項，取消主選項的勾選狀態
    else if(!isAdd &!f.isChecked()){
        //隱藏和取消附加選項
        for (CheckBox checkbox : addFList) {
            if (checkbox.isChecked()){
                checkbox.setChecked(false);
            }
            checkbox.setVisibility(View.INVISIBLE);
        }
        
        //減掉該商品的價格
        price -= temp;
        n.setText("");
    }

    priceOutput.setText("小計NT$"+price);
    return temp;
}
```

::: warning
如果我的焦點在EditText上，這時點擊任一CheckBox，EditText的焦點也不會消失，這樣CheckBoxChangePrice函式在設值給EditText時就會觸發EditText的文字狀態改變監聽。
因此在勾選CheckBox的時候把EditText的焦點移除，給EditText設值就不會觸發到EditText的文字監聽。
:::

♦以下是錯誤發生的情況 ↓ 加入了兩次價格(1)、(2)，但tempPrice中只有一次的價格。
:::info
//將EditText文字設為1
22:34:38.590 12237-12237/(...) I/My: In cbChangePrice n.setText("1")
//執行etChangePrice，將計算的價格加入price中(1)
22:34:38.594 12237-12237/(...) I/My: In etChangePrice if(f.isChecked())
//回傳了tempPrice，♦cbChangePrice接收不到這個已加入price的數字
22:34:38.595 12237-12237/(...) I/My: Over etChangePrice
//在cbChangePrice中又計算了一次後將其加入price(2)，回傳價格給tempPrice
22:34:38.595 12237-12237/(...) I/My: Over cbChangePrice
:::


<h3>EditTextChangePrice</h3>

輸入框EditText的改變價格函式。
```
EditTextChangePrice(CheckBox 主選項, EditText 輸入框, int 價格, int 暫存價格, CheckBox... 附加選項陣列)
```
```java=
public int etChangePrice(CheckBox f, EditText n, int p, int temp, CheckBox... addFList){
    int inputNum;

    //第一個數字不能是0，若字串有內容才能擷取子字串做判斷
    if (n.getText().length() > 0){
        if (n.getText().toString().substring(0,1).equals("0")){
            n.setText("1");
        }
    }

    if(f.isChecked()){
        //減掉原本的價格
        price -= temp;

        //計算有勾選附加選項的價格
        for (CheckBox checkbox : addFList) {
            if (checkbox.isChecked()){
                p += addP;
            }
        }
        
        //若有輸入數量就將價格更新為 (新增了附加選項金額的價格)*(數量)
        if(n.getText().length() > 0){
            inputNum = Integer.parseInt(n.getText().toString());
        }
        //若沒有輸入數量，但是有勾選主選項，所以等於1個商品的價格
        else{
            inputNum = 1;
        }

        temp = p * inputNum;

        //重新加入價格
        price += temp;
        priceOutput.setText("小計NT$"+price);
    }
    
    return temp;
}
```

兩個函式都要返回temp來改變**tempPrice**，它是每個產品各自的總價格，當不勾選該產品時直接減去它的總價格就行。

<h3>CheckBox.OnCheckedChangeListener</h3>

創建OnCheckedChangeListener，然後將該cbCheckedListener分配給多個checkbox對象。
```java=
private List<CheckBox> checkBoxList = new ArrayList<CheckBox>();
```
```java=
checkBoxList.add(f1 = (CheckBox) findViewById(R.id.food1));
f1_c1 = (CheckBox) findViewById(R.id.f1_c1);
f1_c2 = (CheckBox) findViewById(R.id.f1_c2);

f1.setOnCheckedChangeListener(cbCheckedListener);
f1_c1.setOnCheckedChangeListener(cbCheckedListener);
f1_c2.setOnCheckedChangeListener(cbCheckedListener);
```
```java=
private CompoundButton.OnCheckedChangeListener cbCheckedListener = new CompoundButton.OnCheckedChangeListener() {

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        switch (buttonView.getId()) {
            case R.id.food1:
                tempPrice1 = cbChangePrice(f1,n1,p1, tempPrice1, false, f1_c1, f1_c2);
                break;
            case R.id.f1_c1:
            case R.id.f1_c2:
                tempPrice1 = cbChangePrice(f1,n1,p1, tempPrice1,true,f1_c1, f1_c2);
                break;
            case R.id.food2:
                tempPrice2 = cbChangePrice(f2,n2,p2, tempPrice2,false, f2_c1);
                break;
            case R.id.f2_c1:
                tempPrice2 = cbChangePrice(f2,n2,p2, tempPrice2,true, f2_c1);
                break;
            default:
                break;
        }
    }
};
```


<h3>EditText.TextChangedListener</h3>

創建TextWatcher，然後將該textChange分配給多個EditText對象。
```java=
n1 = (EditText) findViewById(R.id.num1);
n2 = (EditText) findViewById(R.id.num2);

n1.addTextChangedListener(textChange);
n2.addTextChangedListener(textChange);
```
```java=
private TextWatcher textChange = new TextWatcher(){
    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
    }

    @Override
    public void afterTextChanged(Editable editable) {
        switch (getCurrentFocus().getId()) {
            case R.id.num1:
                tempPrice1 = etChangePrice(f1, n1, p1, tempPrice1, f1_c1, f1_c2);
                break;
            case R.id.num2:
                tempPrice2 = etChangePrice(f2, n2, p2, tempPrice2, f2_c1);
                break;
            default:
                break;
        }
    }
};
```

<h3>LogCat</h3>

在editText.setText()這邊頻頻出錯，後來在學校發現是android studio模擬器的問題，API28的模擬器不行，API28以下的經測試可以，它應該是在set的時候找不到該editText元件，大概是又觸發到了監聽甚麼的，因為在主畫面做實驗是可以set的。

控制台訊息的使用方式：
```java=
Log.i("myTag", "in the func");
```

在6.Logcat中可以查看所有訊息，上方有過濾器可以過濾這幾種訊息。

Log.v(TAG,Message) : Verbose 記錄詳細訊息
Log.d(TAG,Message) : Debug 除錯
Log.i(TAG ,Message) : INFO 資訊
Log.w(TAG, Message): Warning 警告
Log.e(TAG,Message) : Error 錯誤

![](https://i.imgur.com/fn03Vbb.png)


購物車清單
---
這是在將商品加入購物車後，進入購物車頁面中要顯示的購買清單，這個清單要能在每個商品頁面中做存取(點心、牛排菜單的加入購物車按鈕)，所以寫一個全域變數類別來做處理，而存取該類別變數的操作要使用函式呼叫。

這個全域類別中包含三個變數：
- 要結帳的總金額totalPrice
- 已加入購物車的食物數量列表FoodBuyList
- 每個食物的價格列表FoodBuyPrice

我們需要從食物的名稱去找到它有幾個，也需要從食物名稱去獲取它的價格，所以這個列表要使用字典Dictionary型態，其繼承自java.util.Hashtable<K, V>類別，元素包含一對鍵Key與值Value，在這裡Key就是食物名稱，Value就是數量或價格。

```
totalPrice
FoodBuyList<商品名稱, 數量>
FoodBuyPrice<商品名稱, 價格>
```

常用的函式有：
```
dictionary.put(key, value) //加入新元素
dictionary.remove(key) //根據鍵來刪除該元素
dictionary.get(key) //根據鍵來獲取該元素的值
```


<h3>GlobalVariable</h3>

```java=
public class GlobalVariable extends Application {
    private int totalPrice=0;//要傳送的數字
    private Dictionary FoodBuyList = new Hashtable();
    private Dictionary FoodBuyPrice = new Hashtable();

    //設定 總金額
    public void setTotalPrice(int totalPrice) {
        this.totalPrice = totalPrice;
    }

    //顯示 總金額
    public int getTotalPrice() {
        return totalPrice;
    }

    //修改 購物清單
    public void addFoodBuyList(String foodName,int foodNum) {
        FoodBuyList.put(foodName, foodNum);
    }

    //刪除 購物清單
    public void removeFoodBuyList() {
        FoodBuyList = new Hashtable();
    }

    //取得 購物清單
    public Dictionary getFoodBuyList() {
        return FoodBuyList;
    }

    //修改 食物價格
    public void addFoodBuyPrice(String foodName,int foodPrice) {
        FoodBuyPrice.put(foodName, foodPrice);
    }
    //取得 食物價格
    public Dictionary getFoodPrice() {
        return FoodBuyPrice;
    }

}
```

<h3>私有變數</h3>

在還未按下加入購物車時勾選的產品會先存在一個私有購買列表中，商品取消勾選時會從私有購買列表中移除，該私有購買列表的總價格也會先存在私有價格中。而一旦勾選某商品，該商品的價格就會記錄在私有價格列表中，不會做移除的動作。

```
price
foodNumList<商品名稱, 數量>
foodPriceList<商品名稱, 價格>
```
```java=
private int price = 0;
private Dictionary<String, Integer> foodNumList = new Hashtable<>();
private Dictionary<String, Integer> foodPriceList = new Hashtable<>();
```
CheckBoxChangePrice( )：
```java=
//★要存進購物車清單中的商品名稱
String foodName = f.getText().toString();
//★附加選項數量 計算有附加選項的商品價格 以將它加入價格列表
//有加料的商品價格：主選項金額+附加選項數量*10
int addFNum = 0;

//移除EditText焦點
n.clearFocus();
//判斷輸入框空值->獲取輸入字串
(...)

//點擊的是主選項，為勾選狀態
if (!isAdd & f.isChecked()){
    //顯示附加選項&計算價格
    (...)

    //★put該商品數量(價格)進私有購買列表(價格列表)中
    foodNumList.put(foodName,inputStr);
    foodPriceList.put(foodName,p);
}
//點擊的是附加選項
else if (isAdd & f.isChecked()){
    //先把之前的價格刪除後->重新計算該商品的價格後
    (...)

    for (CheckBox checkbox : addFList) {
        if (checkbox.isChecked()){
            (...)
            
            //★商品名稱加上附加選項名稱後成為一個新商品
            foodName += checkbox.getText().toString();
            //★該商品的加入配料數量(附加選項數量)
            addFNum += 1;
        }
    }
    //加入重新計算後的價格
    (...)

    //★刪除原本同系的key後加入新key
    for (Enumeration<String> i = foodNumList.keys(); i.hasMoreElements();){
        //遍歷食物數量列表，將每一個商品名稱放進foodKey變數中
        String foodKey = i.nextElement();
        //對超出字串索引的情況做限制
        if (foodKey.length() >= f.getText().toString().length()){
            //判斷該商品名稱的前面幾個字(主選項名稱長度)是否與主選項名稱相同
            if (foodKey.substring(0, f.getText().toString().length()).equals(f.getText().toString())){
                //移除該key
                foodNumList.remove(foodKey);
            }
        }
    }
    
    //★加入新key：put有加料的商品數量(價格)進私有購買列表(價格列表)中
    foodNumList.put(foodName, inputStr);
    foodPriceList.put(foodName,p+ addP *addFNum);
}
//點擊的是主選項，取消主選項與附加選項的勾選狀態
else if(!isAdd &!f.isChecked()){
    //隱藏和取消附加選項後->減掉該商品的價格
    (...)

    //★移除暫存清單中的所有同系商品
    for (Enumeration<String> i = foodNumList.keys(); i.hasMoreElements();){
        String foodKey = i.nextElement();
        if (foodKey.length() >= f.getText().toString().length()){
            if (foodKey.substring(0, f.getText().toString().length()).equals(f.getText().toString())){
                foodNumList.remove(foodKey);
            }
        }
    }
}

(return...)
```
EditTextChangePrice( )：
```java=
//★要存進購物車清單中的商品名稱
String foodName = f.getText().toString();

//第一個數字不能是0，若字串有內容才能擷取子字串做判斷
(...)

if(f.isChecked()){
    //減掉原本的價格後->計算有勾選附加選項的價格
    (...)

    for (CheckBox checkbox : addFList) {
        if (checkbox.isChecked()){
            (...)
            
            //★如果有勾附加選項，更新商品名稱
            foodName += checkbox.getText().toString();
        }
    }

    //若有輸入數量就將價格更新為 (新增了附加選項金額的價格)*(數量)
    if(n.getText().length() > 0){
        (...)
        
        //★更新私有購買列表中的商品數量
        foodNumList.put(foodName, inputNum);
    }
    //若沒有輸入數量，但是有勾選主選項，所以等於1個商品的價格p
    else{(...)}

    //計算價格*數量
    (...)
    //重新加入價格
    (...)
}

(return...)
```


<h3>加入購物車</h3>

在按下加入購物車時會將私有變數中的元素匯出到全域變數中，並且清空私有購買列表和重設私有價格，清空的動作只要在加入購物車時清空CheckBox，就會觸發勾選狀態改變的監聽器。
```java=
private Button cal;
```
```java=
cal = (Button) findViewById(R.id.cal_btn);
```
```java=
cal.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        GlobalVariable User = (GlobalVariable)getApplicationContext();

        //★獲取全域總價格，修改後再設定回去
        int totalPrice = User.getTotalPrice();
        totalPrice += price;
        User.setTotalPrice(totalPrice);

        //獲取全域購買列表，取得已存在的商品名稱做加入數量的動作
        Dictionary foodBuyList = User.getFoodBuyList();

        String foodName;
        int foodNum, foodPrice;

        //★將私有列表中的元素匯出至全域列表中
        for (Enumeration<String> i = foodNumList.keys(); i.hasMoreElements();){
            foodName = i.nextElement();
            foodNum = foodNumList.get(foodName);
            foodPrice = foodPriceList.get(foodName);
            
            //若全域列表foodBuyList中不存在該商品就直接加入
            if (foodBuyList.get(foodName)==null){
                User.addFoodBuyList(foodName, foodNum);
                //設定食物價格列表
                User.addFoodBuyPrice(foodName, foodPrice);
            }
            //若全域列表foodBuyList中存在該商品
            //加入的值變更為(已存在的商品數量)+(私有清單中的商品數量)
            else{
                User.addFoodBuyList(foodName,(int) foodBuyList.get(foodName)+foodNum);
            }
        }

        //★遍歷集合中的checkBox,將有選取的CheckBox取消
        for (CheckBox checkbox : checkBoxList) {
            if (checkbox.isChecked()){
                checkbox.setChecked(false);
            }
        }
        
    }

});
```

Rewrite即時計算價格
---

原本遇到的問題是如果不在點擊CheckBox時移除EditText的焦點，給它設值時就會觸發文字狀態改變監聽事件，造成重複加入price。

:::success
但我不想在要勾選CheckBox時移除EditText的焦點(要輸入時還要再點一次輸入框有木有)：
- 勾選CheckBox時，它的輸入框會自動獲得焦點，開啟鍵盤
- 取消CheckBox時，它的輸入框會自動移除焦點，關閉鍵盤
- 聚焦在某個EditText時點擊另一個EditText不會關閉鍵盤
:::

::: warning
計算價格都呼叫EditTextChangePrice去做處理，每次都重新計算價格，也就是都要觸發EditText的文字狀態改變監聽，而CheckBoxChangePrice就不需回傳tempPrice。
:::


<h3>ReCheckBoxChangePrice</h3>

一樣是兩個函式，一個是CheckBox選取狀態改變，一個是EditText的輸入行為變化，差別在於CheckBoxChangePrice函式不須回傳值：
```
CheckBoxChangePrice(CheckBox 主選項, EditText 輸入框, int 價格, boolean 勾選的是否為附加選項, CheckBox... 附加選項陣列)
```

```java=
public void cbChangePrice(CheckBox f, EditText n, int p, boolean isAdd, CheckBox... addFList){
    //獲取輸入字串
    String inputStr = n.getText().toString();
    //要存進購物車清單中的商品名稱、附加選項數量
    (...)

    //★若輸入框為空值，設值為1
    if (n.getText().toString().equals("")){
        inputStr = "1";
    }

    //點擊的是主選項，為勾選狀態
    if (!isAdd & f.isChecked()){
        //顯示附加選項
        for (CheckBox checkbox : addFList) {
            checkbox.setVisibility(View.VISIBLE);
        }

        //★通過要求焦點，使用EditText.setText()來觸發文字監聽
        n.requestFocus();
        n.setText(inputStr);
        
        //put該商品數量(價格)進私有購買列表(價格列表)中
        (...)
    }
    //點擊的是附加選項
    else if (isAdd & f.isChecked()){
        //★計算價格不能寫在這個裡面
        for (CheckBox checkbox : addFList) {
            if (checkbox.isChecked()){
                //商品名稱加上附加選項名稱後成為一個新商品
                //該商品的加入配料數量(附加選項數量)
                (...)
            }

            //★要求焦點，計算價格
            n.requestFocus();
            n.setText(inputStr);
        }

        //刪除原本同系的key後加入新key
        (...)
    }
    //★點擊的是主選項，取消主選項的勾選狀態
    else if(!isAdd &!f.isChecked()){
        //隱藏和取消附加選項
        for (CheckBox checkbox : addFList) {
            if (checkbox.isChecked()){
                checkbox.setChecked(false);
            }
            checkbox.setVisibility(View.INVISIBLE);
        }

        //★減掉該商品的價格
        n.requestFocus();
        n.setText("");
        //★取消選擇，移除焦點(關閉鍵盤)
        n.clearFocus();

        //移除暫存清單中的所有同系商品
        (...)
    }

}
```
需要注意的是，不管點擊主選項還是附加選項，都要去判斷當時輸入框是否為空值，不能設定空值給EditTextChangePrice，它的動作會將temp設為0，取消主選項時刪不掉已加入的價格，所以若是空值要設定"1"給它計算正確的temp。
以及點擊附加選項的區塊，不能把計算價格寫在"附加選項迴圈->判斷已勾選的選項"裡面，因為取消勾選的時候就不會算了拉QAQ，不會更新未勾選的價格。

<h3>ReCheckBox.OnCheckedChangeListener</h3>

CheckBox監聽事件改為：
```java=
private CompoundButton.OnCheckedChangeListener cbCheckedListener = new CompoundButton.OnCheckedChangeListener() {

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        switch (buttonView.getId()) {
            case R.id.food1:
                cbChangePrice(f1,n1,p1, false, f1_c1, f1_c2);
                break;
            case R.id.f1_c1:
            case R.id.f1_c2:
                cbChangePrice(f1,n1,p1, true,f1_c1, f1_c2);
                break;
            (...)
        }
    }
}
```

<h3>ReEditTextChangePrice</h3>

```java=
public int etChangePrice(CheckBox f, EditText n, int p, int temp, CheckBox... addFList){
    //要存進購物車清單中的商品名稱
    String foodName = f.getText().toString();
    int inputNum;

    //第一個數字不能是0，若字串有內容才能擷取子字串做判斷
    if (n.getText().length() > 0){
        if (n.getText().toString().substring(0,1).equals("0")){
            //會遞迴呼叫自己
            n.setText("1");
        }
    }

    //★先把之前的價格刪除
    price -= temp;

    if(f.isChecked()){
        //計算有勾選附加選項的價格
        for (CheckBox checkbox : addFList) {
            if (checkbox.isChecked()){
                p += addP;

                //如果有勾附加選項，更新商品名稱
                foodName += checkbox.getText().toString();
            }
        }

        //若有輸入數量就將價格更新為 (新增了附加選項金額的價格)*(數量)
        if(n.getText().length() > 0){
            inputNum = Integer.parseInt(n.getText().toString());

            //更新私有購買列表中的商品數量
            foodNumList.put(foodName, inputNum);
        }
        //若沒有輸入數量，但是有勾選主選項，所以等於1個商品的價格
        else{
            inputNum = 1;
        }

        //★重新計算價格*數量
        temp = p * inputNum;
        //★重新加入價格
        price += temp;
    }else{
        temp = 0;
    }

    //更新顯示價格
    priceOutput.setText("小計NT$"+price);
    return temp;
}
```
**EditTextChangePrice的回傳值就是tempPrice：每個商品各自的總價格。**
要注意的是當主選項已勾選和未勾選都要更新顯示的價格，未勾選時是更新減掉後的價格。
```
priceOutput.setText("小計NT$"+price)
```

<h3>ReEditText.OnFocusChangeListener</h3>

因為需要設定**有焦點->開啟鍵盤**，不然點擊外部的時候會因為"點擊外部關閉鍵盤"的code而關閉鍵盤，所以先宣告變數im代表軟鍵盤物件，然後獲取該物件。
```java=
private InputMethodManager im;
//onCreate
im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
```
在下方的isShouldHideInput函式中設定了點擊CheckBox不算外部，點擊CheckBox類型元件不會有反應(關閉鍵盤)，因為我想要點擊它時自動聚焦EditText可以勾選商品後就修改數量。

```
- 勾選CheckBox->要求焦點，計算價格
- 取消CheckBox->移除焦點
CheckBox的狀態會觸發EditText的焦點改變，所以在OnFocusChangeListener中做開啟/關閉鍵盤的操作
```
```java=
private final EditText.OnFocusChangeListener etFocusListener = new EditText.OnFocusChangeListener() {
    @Override
    public void onFocusChange(View view, boolean hasFocus) {
        if (hasFocus) {
            //此處為獲得焦點時的處理內容
            //焦點在EditText上，隱藏TableRow
            TR.setVisibility(View.GONE);
            im.showSoftInput(view, 1);
        }else{
            //此處為失去焦點時的處理內容
            //焦點不在EditText上，顯示TableRow
            TR.setVisibility(View.VISIBLE);
            im.hideSoftInputFromWindow(view.getWindowToken(),0);
        }
    }
};
```


<h3>ReDispatchTouchEvent</h3>

```java=
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.i("My","In dispatchTouchEvent");
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        // 獲得當前得到焦點的View，一般情况下就是EditText（特殊情况就是軌跡球或者實體鍵盤會移動焦點）
        View view = getCurrentFocus();

        //使用isShouldHideInput(view, event)進行判斷
        //★判斷點擊的控件是否為EditText/CheckBox，若不是就要隱藏鍵盤
        if (isShouldHideInput(view, ev)) {
            hideSoftInput(view.getWindowToken());
            //★點擊的不是EditText/CheckBox，移除ET焦點
            view.clearFocus();
        }
    }
    //super.調用父類的成員函式，傳遞動作事件
    return super.dispatchTouchEvent(ev);
}

private boolean isShouldHideInput(View v, MotionEvent event) {
    int X = (int)event.getX();
    int Y = (int)event.getY();
    //若當前焦點是EditText
    if (v != null && (v instanceof EditText)) {
        // ★如果點擊發生在EditText/CheckBox上則忽略，不隱藏鍵盤
        View getView = getViewByPosition(getWindow().getDecorView(),X,Y);
        if (getView instanceof EditText || getView instanceof CheckBox) {
            return false;
        } else {
            return true;
        }
    }
    //如果焦點不是EditText則忽略，這個發生在視圖剛繪製完，第一個焦點不在EditView上，和用戶用軌跡球選擇其他的焦點
    return false;
}
```
其中隱藏鍵盤的方法：
```java=
private void hideSoftInput(IBinder token) {
    if (token != null) {
        im.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS);
    }
}
```


動態產生表格
---

```java=
//宣告table變數
private TableLayout table;

//在onCreate中
table = (TableLayout) findViewById(R.id.table); //獲取TableLayout控件

GlobalVariable User = (GlobalVariable)getApplicationContext();

totalPrice = User.getTotalPrice();

foodlist = User.getFoodBuyList();
foodprice = User.getFoodPrice();

//以加入購物車的foodlist購物清單產生表格
for (Enumeration i = foodlist.keys(); i.hasMoreElements();){
    table.addView(generateRow(i.nextElement().toString()));
}
```
```java=
public TableRow generateRow(String foodname){
    TableRow row = new TableRow(this);
    TableLayout.LayoutParams RowParams = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    RowParams.setMargins(15,10,15,10);
    row.setLayoutParams(RowParams);
    row.setBackgroundResource(R.drawable.white_background);

    //food name
    TextView name = new TextView(this);
    name.setTextSize(18);
    name.setText(foodname);

    //food price
    TextView price = new TextView(this);
    price.setTextSize(18);
    price.setText("$"+foodprice.get(foodname));

    //minus button
    FrameLayout minusFrame = new FrameLayout(this);
    ImageButton minus = new ImageButton(this);
    minus.setBackgroundResource(R.drawable.minus);
    minus.setScaleType(ImageView.ScaleType.FIT_XY);
    minusFrame.addView(minus);
    minus.getLayoutParams().width = 60;
    minus.getLayoutParams().height = 70;

    //food num
    TextView num = new TextView(this);
    num.setTextSize(18);
    num.setText(foodlist.get(foodname).toString());

    //add button
    FrameLayout addFrame = new FrameLayout(this);
    ImageButton add = new ImageButton(this);
    add.setBackgroundResource(R.drawable.add);
    add.setScaleType(ImageView.ScaleType.FIT_XY);
    addFrame.addView(add);
    add.getLayoutParams().width = 70;
    add.getLayoutParams().height = 70;

    //trash button
    FrameLayout trashFrame = new FrameLayout(this);
    ImageButton trash = new ImageButton(this);
    trash.setBackgroundResource(R.drawable.trashcan);
    trash.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
    trashFrame.addView(trash);
    trashFrame.setPadding(20,10,40,10);
    trash.getLayoutParams().width = 50;
    trash.getLayoutParams().height = 50;

    row.addView(name);
    row.addView(price);
    row.addView(minusFrame);
    row.addView(num);
    row.addView(addFrame);
    row.addView(trashFrame);

    return row;
}
```

| 空空如也 | 產生的表格 | 
| -------- | -------- | 
|![](https://i.imgur.com/CBJHfOg.png)|![](https://i.imgur.com/CDZc51O.png)| 


<h3>結帳按鈕</h3>

```java=
public void checkout(View view) {
    table.setVisibility(View.GONE);
    GlobalVariable User = (GlobalVariable)getApplicationContext();

    User.removeFoodBuyList();

    Toast.makeText(this, "結帳成功！您已花了"+totalPrice+"元", Toast.LENGTH_LONG).show();
    totalPrice=0;
    User.setTotalPrice(totalPrice);
    total.setText("總金額：NT$" + totalPrice);
}
```

左滑右滑切換頁
---
:::danger

:::

+-和刪除商品
---
:::danger

:::

連SQLite資料庫
---
:::danger

:::