選項菜單其實就是位於右上角的一個隱藏菜單,通過點擊右上角的豎三點或文字來創建菜單,點擊菜單項可以觸發菜單項點擊事件。
本來選項式菜單和彈出菜單、上下文菜單應該一起寫,但是因為我想把我之前寫的的listView的購物車例子和選項菜單結合,所以篇幅過長就單獨寫了
廢話不多說,先上圖。
如上圖所示分別是菜單項:管理、完成、退出。
OptionMenu的創建步驟:
1、在app/res/menu下新建 Menu Resource File
2、選項菜單不需要綁定的View,但是需要Activity重寫onCreateOptionsMenu()和onOptionsItemSelected()方法
3、在onCreateOptionsMenu(Menu menu)里,用MenuInflater的inflate()方法可以將Menu Resource File填充到Menu對象中
4、在onOptionsItemSelected(MenuItem item)里,寫點擊菜單項的事件。
我這次的例子要比我上一篇的Android:ListView有了一些改進:1、加入了ViewHolder讓listView更快的顯示速度。2、不再使用Map記錄CheckBox的狀態,改用javaBean做記錄,並增加一個對用戶“觸屏點擊”的判斷,來解決CheckBox在上下滑動時狀態改變的問題。3、增加了選項式菜單optionMenu,可以批量刪除ListView里的數據項。4、增加了“全選”CheckBox和底部的實時顯示商品總價的TextView。(我這次改進是照着某寶購物車的樣子模仿的,可能還是有不足的地方,希望大家不要笑話我)
准備展示商品的圖片,圖片粘貼到app/res/drawable。文字描述以及圖片ID可以放到app/res/values/strings.xml里

<resources> <string name="app_name">OptionMenu</string> <string-array name="detailList"> <item>寶寶夏裝2020新款洋氣男孩衣服套裝幼兒童裝夏季短裙背帶褲兩件套</item> <item>拇指魚兒童衛衣2019秋季童裝拼色長袖T恤套頭打底衫小童圓領上衣</item> <item>女童套裝童裝2020夏季新款韓版可愛貓咪蓬蓬裙兒童紗裙公主短裙</item> <item>女童洋氣網紅套裝2020新款韓版兒童裝短袖兩件套女孩時髦運動夏裝</item> <item>女童連衣裙2020新款夏季洋氣裝兒童寶寶小孩公主背心裙親子美女裝</item> <item>童裝男童夏天短袖套裝夏裝2020新款中大童小兒童網紅帥洋氣韓版潮</item> <item>童裝兩件套2020夏裝女童套裝新款夏季女背帶裙洋氣2短袖襯衫寶寶</item> <item>2020卡通動漫新款童裝夏牛仔背帶褲套裝男童短袖t恤潮部落</item> </string-array> <integer-array name="imageID"> <item>@drawable/clothes1</item> <item>@drawable/clothes2</item> <item>@drawable/clothes3</item> <item>@drawable/clothes4</item> <item>@drawable/clothes5</item> <item>@drawable/clothes6</item> <item>@drawable/clothes7</item> <item>@drawable/clothes8</item> </integer-array> </resources>
首先明確我們需要什么樣的列表項,如下圖這個列表項是由4個控件組成:CheckBox、ImageView、TextView、TextView
於是我們開始設計我們的列表布局,新建Layout Resource File

<?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" android:layout_width="match_parent" android:layout_height="wrap_content"> <CheckBox android:id="@+id/checkBox" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:id="@+id/imageView" android:layout_width="110dp" android:layout_height="110dp" android:padding="5dp" app:srcCompat="@mipmap/ic_launcher" /> <TextView android:id="@+id/tv_details" android:layout_width="150dp" android:layout_height="110dp" android:padding="5dp" android:layout_gravity="center"/> <TextView android:id="@+id/tv_price" android:layout_width="0dp" android:layout_height="110dp" android:layout_weight="1" android:gravity="center" android:textStyle="bold" /> </LinearLayout>
為了更好地傳遞這一個列表項的信息,我們需要一個JavaBean,來打包這些信息以方便傳遞。
這里CheckBox只有兩種狀態,應該是Boolean類型;ImageView顯示的圖片在資源文件里是Int型;后面兩個TextView里的數據都是String類型

public class CartBean { private boolean checked=false; private int imageID; private String details; private float price; public CartBean(int imageID, String details, float price) { this.imageID = imageID; this.details = details; this.price = price; } public boolean isChecked() { return checked; } public void setChecked(boolean checked) { this.checked = checked; } public int getImageID() { return imageID; } public void setImageID(int imageID) { this.imageID = imageID; } public String getDetails() { return details; } public void setDetails(String details) { this.details = details; } public float getPrice() { return price; } public void setPrice(float price) { this.price = price; } }
一個列表項=一個JavaBean;那么一組列表項=List<JavaBean>。為了把這個我們打包好的數據傳給ListView,我們還需要一個適配器Adapter,那么新建一個java class做適配器
關於我自定義的Adapter的介紹:
- 繼承BaseAdapter,需要重寫getCount()、getItem()、getItemId()、getView()這四個方法,前三個與List<JavaBean>數據有關,最后一個與每一行數據項顯示有關,因為ListView是一行行顯示,所以每顯示一行都會調用一次getView方法。
- 因為adapter需要確定在什么上下文環境(ListView所在的環境)適配什么數據,所以adapter的構造方法傳入了這兩個參數。
- 因為每一項都是一個JavaBean,調用的方法也都一樣,所以直接在適配器的getView方法里定義控件,包括填充控件信息、觸發的事件等。
- 當ListView的項數較多一屏無法完全顯示的時候,在上下滑動ListView的過程中,划入屏幕的項會通過getView得到,而划出屏幕的項的視圖會被回收,這個被回收的項的視圖就是convertView。我們觀察getView()方法時會發現,里面有兩種方法:inflate()將布局文件填充到某上下文環境、findViewById()通過id得到控件,這兩個方法的參數是固定的值,也就是說,對每一個列表項來說都要在調用getView()時重復使用這兩種方法。那么在划入划出時,這種重復操作必然影響顯示效果,為了更快渲染ListView,我們用ViewHolder來避免findViewById的重復使用。
- ViewHolder其實是一個java內部類,只有屬性沒有方法。當covertView為空,也就是說沒有被回收的項,那么就需要每次在getView方法里調用inflate和findViewById方法,當listView上下滑動時,convertView不為空,於是可以通過convertView的標志位得到ViewHolder對象,ViewHolder對象的屬性就是我們需要找到的控件。
- getView(int position, View convertView, ViewGroup parent),這個方法里的三個參數:position--當前項的位置;converView--被回收的項;parent--項的父視圖,這里指ListView。而ListView的parent就是ListView所在的視圖,即R.layout.activity_main.xml(ListView在該布局文件)填充的視圖。
- 復選框設置的監聽器里重寫的onCheckedChanged(CompoundButton buttonView, boolean isChecked)方法,其中buttonView.isPressed()可以判斷是否有“點擊”,以此來區分人為的點擊帶來的CheckBox狀態改變。於是,我利用這一特點,當人為點擊造成變化的isChecked值,就傳入CartBean的"checked",然后CheckBox的實際狀態由這個“checked”決定。這樣避免了由於ListView上下滑動過程中,由於回收了項而無法記錄CheckBox狀態(因為回收再利用時會恢復初始的未選中的狀態)。
- 全選CheckBox也有buttonView.isPressed()的判斷,但不是為了解決回收再利用的狀態改變。而是為了在人為點擊勾選了“全選”后,又取消列表的某一項的復選框時,“全選”自動取消勾選,但這種非人為的取消不會引起“全選”取消時商品總價=0的操作。

public class MyListViewAdapter extends BaseAdapter { private Context cxt;//上下文環境 private List<CartBean> itemList;//列表數據 private float TotalPrice=0;//商品總價 private TextView tv_totalPrice;//底部顯示總價textView private CheckBox all_CheckBox;//全選復選框 private int P_checked_NUm=0;//得到已勾選的商品的數量 public MyListViewAdapter(Context cxt, List<CartBean> itemList) { this.cxt = cxt; this.itemList = itemList; } public void setTotalPrice(float totalPrice) { TotalPrice = totalPrice; } public float getTotalPrice() { return TotalPrice; } @Override public int getCount() { return itemList.size(); } @Override public Object getItem(int position) { return itemList.get(position); } @Override public long getItemId(int position) { return position; } public static class ViewHolder{//自定義的內部類ViewHolder CheckBox mcheckBox; ImageView mimageView; TextView mtextView1; TextView mtextView2; } @Override public View getView(final int position, View convertView, ViewGroup parent) { ViewHolder holder; if(convertView==null){ convertView = View.inflate(cxt,R.layout.listview_item_layout,null); holder=new ViewHolder(); holder.mcheckBox=convertView.findViewById(R.id.checkBox); holder.mimageView=convertView.findViewById(R.id.imageView); holder.mtextView1=convertView.findViewById(R.id.tv_details); holder.mtextView2=convertView.findViewById(R.id.tv_price); //getView()中參數的parent是listView,那么listView的parent就是listView所在的視圖 ViewGroup primaryView=(ViewGroup)parent.getParent(); all_CheckBox=primaryView.findViewById(R.id.checkBox2);//全選CheckBox tv_totalPrice=primaryView.findViewById(R.id.textView);//底部實時顯示總價的textView convertView.setTag(holder); }else{ holder=(ViewHolder)convertView.getTag(); } holder.mimageView.setImageResource(itemList.get(position).getImageID()); holder.mtextView1.setText(itemList.get(position).getDetails()); holder.mtextView2.setText("¥ "+ itemList.get(position).getPrice()); holder.mcheckBox.setChecked(itemList.get(position).isChecked()); //以下是列表項的CheckBox的勾選狀態改變觸發的事件,計算商品總價 holder.mcheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (!buttonView.isPressed()) return;//這句一定要有,為了讓每一列表項划入划出時導致的checkBox的狀態改變不會觸發價格計算 //在Bean數據包中記錄復選框的勾選/取消勾選的狀態 itemList.get(position).setChecked(isChecked); if (isChecked) { P_checked_NUm++; TotalPrice += itemList.get(position).getPrice(); } else { P_checked_NUm--; TotalPrice -= itemList.get(position).getPrice(); } //當勾選的項等於列表的所有項數,將自動勾選“全選",否則取消"全選" if(P_checked_NUm==itemList.size()){ all_CheckBox.setChecked(true); }else{ all_CheckBox.setChecked(false); } tv_totalPrice.setText("¥ "+TotalPrice); } });//end_holder.mcheckBox.setOnCheckedChangeListener() //以下是全選CheckBox勾選事件 all_CheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(!buttonView.isPressed())return; //當不是人為點擊使全選發生狀態改變時(如全選狀態下,取消勾選某一項商品或一項項全部勾選),不觸發以下操作 if(isChecked){ for(int i=0;i<itemList.size();i++){ if(!itemList.get(i).isChecked()){//未勾選的項改為勾選 P_checked_NUm=itemList.size(); itemList.get(i).setChecked(true); TotalPrice+=itemList.get(i).getPrice(); }//已勾選的項不用改變 } }else{ for(int i=0;i<itemList.size();i++){ if(itemList.get(i).isChecked()) {//已勾選的項改為未勾選 P_checked_NUm=0; itemList.get(i).setChecked(false); TotalPrice -= itemList.get(i).getPrice(); }//未勾選的項不用改變 } } notifyDataSetChanged();//通知適配器,列表數據已更新 tv_totalPrice.setText("¥ "+TotalPrice); } });//end_all_CheckBox.setOnCheckedChangeListener() return convertView; } }
然后就是我們的重點(並不是)!Option Menu !
在app/res/menu下新建option_menu.xml
- app:showAsAction="always" :讓菜單項以文字的形式直接顯示在右上角
- “完成”這個菜單項一開始是隱藏的,只有當點擊“管理”后,“完成”菜單項才會顯示

<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/manage" android:title="管理" app:showAsAction="always" /> <item android:id="@+id/finish" android:title="完成" android:visible="false" app:showAsAction="always" /> <item android:id="@+id/quit" android:title="退出" /> </menu>
突然發現還沒有創建ListView這個控件,其實是在Activity的布局文件里設置
如下圖,是一個購物車頁面,ListView在上,底部分別是CheckBox、TextView、Button
- 這里的ListView一定要設置高度為“match_parent”,我開始設成“wrap_content”,結果發現getView方法會被反復調用,我找了一天才找到這個問題。
- 兩個Button都有android:background="@drawable/button_bg",其實是一個自定義的drawable文件作為按鈕背景,具體方法看:Android:Button,這里不粘貼按鈕背景的xml代碼了。

<?xml version="1.0" encoding="utf-8"?> <FrameLayout 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=".MainActivity"> <!--ListView的layout_height只能是match_parent--> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="50dp"> </ListView> <FrameLayout android:layout_width="match_parent" android:layout_height="50dp" android:layout_gravity="bottom"> <CheckBox android:id="@+id/checkBox2" android:layout_width="wrap_content" android:layout_height="match_parent" android:text="全選" /> <TextView android:id="@+id/textView" android:layout_width="100dp" android:layout_height="match_parent" android:textColor="#ff0000" android:gravity="center" android:layout_gravity="right" android:textSize="20sp" android:text="¥ 0.0" android:layout_marginRight="100dp" /> <Button android:id="@+id/jiezhang" android:layout_width="100dp" android:layout_height="match_parent" android:layout_gravity="right" android:textColor="#ffffff" android:background="@drawable/button_bg" android:text="結賬離開" /> </FrameLayout> <Button android:id="@+id/deleteButton" android:layout_width="match_parent" android:layout_height="50dp" android:visibility="invisible" android:layout_gravity="bottom" android:textColor="#ff0000" android:textSize="20dp" android:background="@drawable/del_button_bg" android:text="刪除" /> </FrameLayout>
關於MainActivity.java的介紹:
- 從strings.xml讀取數據的參考文章:Android開發values目錄里定義數組、顏色、文本、尺寸xml配置文件並且獲取數據
- ListView設置適配器需要:先新建Adapter對象,並通過構造方法傳入一組JavaBean的數據列表;再讓ListView調用setAdapter方法設置適配器。
- 選項式菜單需要Activity重寫兩個方法:onCreateOptionsMenu(Menu menu)和onOptionsItemSelected(@NonNull MenuItem item),我的菜單比較簡單,僅僅是為隱藏或顯示底部的刪除按鈕。
- 我給刪除按鈕加了事件,讓ListView的數據列表remove被勾選的項,先記錄列表項的position,然后倒序刪除列表項(因為刪除后后面各項位置會前移,從后往前刪不用擔心這個,因為刪除后只影響被刪除項之后的項,不影響前面的項)(最后別忘了刪完把商品總價清零)參考文章:Android:ListView批量刪除
- 最后的showMyToast()方法是自定義顯示時間長短的Toast 參考文章:如何將Toast的顯示時間隨意設置

public class MainActivity extends AppCompatActivity { private Button deleteButton;//為了設置隱藏的全局刪除按鈕 private MenuItem manageItem,finishItem;//為了設置可視/不可視的全局菜單項對象 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView tv_TotalPrice=findViewById(R.id.textView);//底部顯示總價的textView ListView mylistView=findViewById(R.id.listView);//顯示購物清單的listView deleteButton=findViewById(R.id.deleteButton);//底部的刪除按鈕 final List<CartBean> beansData=init();//初始化數據 final MyListViewAdapter myAdapter=new MyListViewAdapter(this,beansData);//適配器與數據適配 mylistView.setAdapter(myAdapter);//Listview與適配器適配 deleteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { CartBean bean; List<Integer> position_checkedItem=new ArrayList<>();//記錄被勾選項的位置 int i; //倒敘遍歷myBeans,選中的列表項先記錄位置到position_checkedItem for(i=beansData.size()-1;i>=0;i--){ bean=beansData.get(i); if(bean.isChecked()){ position_checkedItem.add(i); } } //被記錄位置的列表項刪掉 if(position_checkedItem.size()!=beansData.size()) { for (int position : position_checkedItem) { beansData.get(position).setChecked(false); beansData.remove(position); } }else{ beansData.clear(); } //刪除時需要勾選商品,會觸發價格計算,所以當點擊刪除按鈕后,需要把價格清零 myAdapter.setTotalPrice(0); tv_TotalPrice.setText("¥ "+myAdapter.getTotalPrice()); myAdapter.notifyDataSetChanged(); Toast toast=Toast.makeText(getApplicationContext(),"已刪除",Toast.LENGTH_SHORT); showMyToast(toast,1000); } });//刪除按鈕 }//end_onCreate() private List init(){//初始化數據 List<CartBean> myBeans=new ArrayList<>(); //從資源文件,得到商品圖片ID TypedArray typedArray =getResources().obtainTypedArray(R.array.imageID); int[] P_imagesID=new int[typedArray.length()]; for(int i=0;i< typedArray.length();i++){ P_imagesID[i]=typedArray.getResourceId(i,0); } typedArray.recycle(); //從資源文件,得到商品描述 String[] P_details =getResources().getStringArray(R.array.detailList); //float不能放在資源文件里,我就這樣定義吧,以后學了數據庫可以從數據庫得到這些數據 float[] P_prices = new float[]{78.0f, 160.0f, 67.5f, 188.0f, 189.0f, 79.0f, 38.0f, 68.0f}; //添加數據列表List CartBean item; for(int i = 0; i< P_imagesID.length; i++){ item=new CartBean(P_imagesID[i], P_details[i], P_prices[i]); myBeans.add(item); } return myBeans; }//end_init() @Override public boolean onCreateOptionsMenu(Menu menu) { //得到menu里的各菜單項對象 this.getMenuInflater().inflate(R.menu.option_menu,menu); manageItem=menu.findItem(R.id.manage); finishItem=menu.findItem(R.id.finish); return true; }//end_onCreateOptionMenu() @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { switch(item.getItemId()){ /* * 1.點擊管理菜單項,出現底部刪除按鈕 * 2.點擊完成菜單項,隱藏底部刪除按鈕 * 3.點擊退出菜單項,退出購物頁面*/ case R.id.manage: manageItem.setVisible(false); finishItem.setVisible(true); deleteButton.setVisibility(View.VISIBLE); break; case R.id.finish: manageItem.setVisible(true); finishItem.setVisible(false); deleteButton.setVisibility(View.INVISIBLE); break; case R.id.quit: MainActivity.this.finish(); break; } return true; }//end_onOptionItemSelected() //以下是自定義Toast顯示時間的方法 public void showMyToast(final Toast toast, final int cnt) { final Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { toast.show(); } }, 0, 3000); new Timer().schedule(new TimerTask() { @Override public void run() { toast.cancel(); timer.cancel(); } }, cnt ); }//end_showMyToast() }
好了,那么我的購物車就做好了。