选项菜单其实就是位于右上角的一个隐藏菜单,通过点击右上角的竖三点或文字来创建菜单,点击菜单项可以触发菜单项点击事件。
本来选项式菜单和弹出菜单、上下文菜单应该一起写,但是因为我想把我之前写的的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() }
好了,那么我的购物车就做好了。