摘要:需求分析在饿了么首页中我们能看到这样的布局如下图。导航点是使用文件画的,灰色为未选中,绿色为选中。因为默认显示的是第一页,所以我们默认第一个的导航点是选中的。
1、需求分析
在饿了么首页中我们能看到这样的布局(如下图)。红框内是一个可以左右滑动的页面,每一个页面类似于九宫格,都有可供点击图标。对于这样的布局,我在网上找了很久都没有找到相关的名称,所以我这里暂时叫它导航页吧。
最近公司的项目就要求我实现一个这样的布局,但是我们的图标并不是想饿了么这样是固定的,所以在饿了么的布局上还要加一个效果:在图标数目无法排满两行时,就只显示一行。比如说,我们每一页最多可以显示两行和四列,当图标的总数目小于或等于4个时就只显示出一行,第二行就不要了。这样页面就不至于留出太多的空白。
先梳理一下我们要实现的效果:
大体的框架是一个可以左右滑动的页面;
每一页为两行四列的矩阵;
底部有和页面数目相等的导航点,且滑动到某个页面时,相应的导航点会改变颜色;
单选事件:选中图标后,图标变为完全不透明。
明白需求之后,我们穿越时空看看最后实现后的布局:
图片资源大家可以在我源码中找到。
2、实现思路我们已经分析了需求,现在就来分析一下怎么实现。
必须事先说明一下,我的实现方法比较暴力,也比较占资源,所以大家要是有更好的方法的话欢迎留言告诉我,我们互相学习。
首先,这是一个左右滑动的页面,所以我们可以考虑使用ViewPager我们每个页面有8个图标,也就是8个item。在图标的数目在4以下时就只显示第一行,所以布局上我们可以将四个item作为一行放置到横向的线性布局中,当第二行没有图标时就让它消失。
ViewPager中的每一页的布局都是一样,所以我们可以对其复用:将图标分组,每8个为一组(余下的不到八页也归为一组),每一组即为一页。每次滑动的页面时就加载布局,填充图标和文字。
下面的导航点就比较简单了,可以使用ViewPager的滑动监听事件进行监听,滑动到选中的页面时就改变导航点的颜色。
至于透明,我们只需要记录图标的选中状态,然后改变透明度即可。
3、编写布局接下来就让我们创建项目,编写页面布局吧。
在Android Studio中创建一个NavigationPager项目,MainActivity的布局如下:
activity_main.xmlMainActivity的主布局是一个纵向的线性布局,上面一个ViewPager,下面一个横向的线性布局,用于放置导航点。这里得说一下ViewPager的特性,在不指定特定高度的情况下,ViewPager的高度是默认填充整个父布局的,我们显然不希望这样,毕竟我们还要分一行和两行两种情况呢。所以这里我在这里使用了一个可以自适应高度的自定义ViewPager:
CustomViewPager/** * Created by Lindroid on 2017/3/20. * 自适应高度的ViewPager */ public class CustomViewPager extends ViewPager { public CustomViewPager(Context context) { super(context); } public CustomViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int height = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); int h = child.getMeasuredHeight(); if (h > height) height = h; } heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
页面的布局(也就是ViewPager的子布局)可以拆分成两个横向排列的布局:
layout_line1.xml和layout_line2.xml分别为第一行和第二行的布局,下面是layout_line1.xml的布局。layout_line2.xml除了控件的id名不同之外,其他的都一样,这里就不贴出来了。
好了,我们所需要的布局都已经编写完毕,下面就正式开始写代码了!
4、编写代码 4.1 创建Bean类创建一个NavigationItemBean类,用于存储图标的图片id、名称和选中状态等信息。
public class NavigationItemBean { private int iconID; private String iconName; private boolean isSelected; public NavigationItemBean(int iconID, String iconName) { this.iconID = iconID; this.iconName = iconName; } public boolean isSelected() { return isSelected; } public void setSelected(boolean selected) { isSelected = selected; } public int getIconID() { return iconID; } public void setIconID(int iconID) { this.iconID = iconID; } public String getIconName() { return iconName; } public void setIconName(String iconName) { this.iconName = iconName; } }4.2 设置图标资源
为了提高效率,我在初始化控件时使用了ButterKnife,大家需要它的jar包的话也可以从我的工程中复制粘贴。
public class MainActivity extends AppCompatActivity { @Bind(R.id.vp_navigation) CustomViewPager vpNavigation; @Bind(R.id.ll_dots) LinearLayout llDots; private NavigationAdapter adapter; private ListitemBeanList = new ArrayList<>(); private List dots = new ArrayList<>(); //图标id数组 private int[] icons = {R.mipmap.phone, R.mipmap.browser, R.mipmap.messages, R.mipmap.contacts, R.mipmap.camera, R.mipmap.gallery, R.mipmap.calendar, R.mipmap.calculator, R.mipmap.settings, R.mipmap.mail, R.mipmap.maps, R.mipmap.music, R.mipmap.movie}; //图标名称数组 private String[] names = {"电话", "浏览器", "信息", "联系人", "照相", "图库", "日历", "计算器", "设置", "邮箱", "地图", "音乐", "电影"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); initData(); } private void initData() { for (int i = 0; i < icons.length; i++) { itemBeanList.add(new NavigationItemBean(icons[i], names[i])); } adapter = new NavigationAdapter(this, itemBeanList, this); vpNavigation.setAdapter(adapter); } }
先将图标的id和名称存储到两个数组中,再遍历数组将资源添加到集合中。
4.3 创建适配器这一步应该是整个项目中最难的一步了,所以要打起精神来了。先看下面的代码:
public class NavigationAdapter extends PagerAdapter { @Bind(R.id.iv_icon1) ImageView ivIcon1; @Bind(R.id.tv_name1) TextView tvName1; @Bind(R.id.ll_item1) LinearLayout llItem1; @Bind(R.id.iv_icon2) ImageView ivIcon2; @Bind(R.id.tv_name2) TextView tvName2; @Bind(R.id.ll_item2) LinearLayout llItem2; @Bind(R.id.iv_icon3) ImageView ivIcon3; @Bind(R.id.tv_name3) TextView tvName3; @Bind(R.id.ll_item3) LinearLayout llItem3; @Bind(R.id.iv_icon4) ImageView ivIcon4; @Bind(R.id.tv_name4) TextView tvName4; @Bind(R.id.ll_item4) LinearLayout llItem4; @Bind(R.id.iv_icon5) ImageView ivIcon5; @Bind(R.id.tv_name5) TextView tvName5; @Bind(R.id.ll_item5) LinearLayout llItem5; @Bind(R.id.iv_icon6) ImageView ivIcon6; @Bind(R.id.tv_name6) TextView tvName6; @Bind(R.id.ll_item6) LinearLayout llItem6; @Bind(R.id.iv_icon7) ImageView ivIcon7; @Bind(R.id.tv_name7) TextView tvName7; @Bind(R.id.ll_item7) LinearLayout llItem7; @Bind(R.id.iv_icon8) ImageView ivIcon8; @Bind(R.id.tv_name8) TextView tvName8; @Bind(R.id.ll_item8) LinearLayout llItem8; @Bind(R.id.ll_line2) LinearLayout llLine2; private Context context; private ListitemBeanList; private List itemViews; int pages; //总页数 private View pageView; private View.OnClickListener onClickListener; public NavigationAdapter(Context context, List itemBeanList, View.OnClickListener onClickListener) { this.context = context; this.itemBeanList = itemBeanList; this.onClickListener = onClickListener; itemViews = new ArrayList<>(); initView(itemBeanList); } /** * 初始化ViewPager的页面布局 * @param list */ private void initView(List list) { itemViews.clear(); //计算ViewPager的页数 pages = list.size() / 9; pages = pages + 1; for (int i = 0; i < pages; i++) { pageView = View.inflate(context, R.layout.page_navigation, null); ButterKnife.bind(this, pageView); setPagerViewData(i); itemViews.add(pageView); } } /** * 根据页码来填充数据 * @param pageNum */ public void setPagerViewData(int pageNum) { if (itemBeanList.size() > pageNum * 8 + 0) { llItem1.setVisibility(View.VISIBLE); tvName1.setText(itemBeanList.get(pageNum * 8 + 0).getIconName()); ivIcon1.setBackgroundResource(itemBeanList.get(pageNum * 8 + 0).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 0).isSelected(), ivIcon1); } else { llItem1.setVisibility(View.INVISIBLE); } if (itemBeanList.size() > pageNum * 8 + 1) { llItem2.setVisibility(View.VISIBLE); tvName2.setText(itemBeanList.get(pageNum * 8 + 1).getIconName()); ivIcon2.setBackgroundResource(itemBeanList.get(pageNum * 8 + 1).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 1).isSelected(), ivIcon2); } else { llItem2.setVisibility(View.INVISIBLE); } if (itemBeanList.size() > pageNum * 8 + 2) { llItem3.setVisibility(View.VISIBLE); tvName3.setText(itemBeanList.get(pageNum * 8 + 2).getIconName()); ivIcon3.setBackgroundResource(itemBeanList.get(pageNum * 8 + 2).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 2).isSelected(), ivIcon3); } else { llItem3.setVisibility(View.INVISIBLE); } if (itemBeanList.size() > pageNum * 8 + 3) { llItem4.setVisibility(View.VISIBLE); tvName4.setText(itemBeanList.get(pageNum * 8 + 3).getIconName()); ivIcon4.setBackgroundResource(itemBeanList.get(pageNum * 8 + 3).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 3).isSelected(), ivIcon4); } else { llItem4.setVisibility(View.INVISIBLE); } if (itemBeanList.size() > pageNum * 8 + 4) { llItem5.setVisibility(View.VISIBLE); tvName5.setText(itemBeanList.get(pageNum * 8 + 4).getIconName()); ivIcon5.setBackgroundResource(itemBeanList.get(pageNum * 8 + 4).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 4).isSelected(), ivIcon5); } else { llItem5.setVisibility(View.INVISIBLE); llLine2.setVisibility(View.GONE); } if (itemBeanList.size() > pageNum * 8 + 5) { llItem6.setVisibility(View.VISIBLE); tvName6.setText(itemBeanList.get(pageNum * 8 + 5).getIconName()); ivIcon6.setBackgroundResource(itemBeanList.get(pageNum * 8 + 5).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 5).isSelected(), ivIcon6); } else { llItem6.setVisibility(View.INVISIBLE); } if (itemBeanList.size() > pageNum * 8 + 6) { llItem7.setVisibility(View.VISIBLE); tvName7.setText(itemBeanList.get(pageNum * 8 + 6).getIconName()); ivIcon7.setBackgroundResource(itemBeanList.get(pageNum * 8 + 6).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 6).isSelected(), ivIcon7); } else { llItem7.setVisibility(View.INVISIBLE); } if (itemBeanList.size() > pageNum * 8 + 7) { llItem8.setVisibility(View.VISIBLE); tvName8.setText(itemBeanList.get(pageNum * 8 + 7).getIconName()); ivIcon8.setBackgroundResource(itemBeanList.get(pageNum * 8 + 7).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 7).isSelected(), ivIcon8); } else { llItem8.setVisibility(View.INVISIBLE); } } public void setIconAlpha(boolean isSelected, ImageView imageView) { if (isSelected) { imageView.setAlpha(1.0f); } else { imageView.setAlpha(0.4f); } } @Override public int getCount() { return pages; } @Override public Object instantiateItem(ViewGroup container, int position) { container.addView(itemViews.get(position)); return itemViews.get(position); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } }
代码有点长,但其实不难理解。首先我们创建一个NavigationAdapter继承于ViewPager专用的PageAdapter,并重写其中的几个方法。这里就要注意一个小地方了,getCount方法返回的是ViewPager的item数目,也就是页数,而不是图标的数目,可千万不能搞错了!
下面我就讲解一下比较重要的方法。首先是初始化ViewPager页面布局的方法initView:
private void initView(Listlist) { itemViews.clear(); //计算ViewPager的页数 pages = list.size() / 9; pages = pages + 1; for (int i = 0; i < pages; i++) { pageView = View.inflate(context, R.layout.page_navigation, null); ButterKnife.bind(this, pageView); setPagerViewData(i); itemViews.add(pageView); } }
第4和第5行的代码是根据图标的数目来计算ViewPager的页数,而for循环则是有多少页我们就调用多少次View.inflate方法去创建布局。这也是ButterKnife.bind(this, pageView)要在for循环中执行的原因:我们使用的虽然都是同一个布局文件,但实际每次循环都会创建一个新的布局。
setPagerViewData方法则是给页面中的每个item填充数据。首先判断集合中的某一位是否有数据,有的话则设置图片和文字,反之则隐藏item。里面的代码基本都一个样子,我就摘录其中“最特别”的一段了:
if (itemBeanList.size() > pageNum * 8 + 4) { llItem5.setVisibility(View.VISIBLE); tvName5.setText(itemBeanList.get(pageNum * 8 + 4).getIconName()); ivIcon5.setBackgroundResource(itemBeanList.get(pageNum * 8 + 4).getIconID()); setIconAlpha(itemBeanList.get(pageNum * 8 + 4).isSelected(), ivIcon5); } else { llItem5.setVisibility(View.INVISIBLE); llLine2.setVisibility(View.GONE); //t图标数不大于4个时让第二行消失 }
如果你对于initView方法中for循环从0开始(页码的取值从0开始)而不是从1开始感到不解,那么看了setPagerViewData方法之后相信你就会懂了。页码为0时,第一页的第一个item取到的正好是集合的0位元素;页码为1时,第二页的第一个item则是集合中的第8位(1x8+1)元素,这样,我们就可以根据具体的页码来将集合中的元素填充到页面中。这里别忘了还有高度适应的问题。回头看看我摘出来的这段代码,当集合长度不大于4时,我们不仅要隐藏第五个item,而且要让第二行的布局消失,这时,ViewPager的高度就只有一行了。但你肯定会问,第一页时去掉第二行没什么,可以到了第二页时如果集合元素排不到第二行,第二行也会消失吗?答案是不会的。ViewPager默认所有页面的高度都是一致的,就算你在第二页就去掉了第二行,它的高度也不会随着改变。
4.4 设置导航点在MainActivity中,我们创建绘制导航点的方法。导航点是使用xml文件画的,灰色为未选中,绿色为选中。
private Listdots = new ArrayList<>(); /** * 绘制导航点 */ private void initDot() { for (int i = 0; i < (itemBeanList.size() / 9 + 1); i++) { Log.e("Tag", itemBeanList.size() + ""); ImageView imageView = new ImageView(this); //创建一个ImageView来放置圆点,并确定ImageView的宽高 LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( 20, 20); params.leftMargin = 10; params.rightMargin = 10; params.topMargin = 5; params.bottomMargin = 10; imageView.setLayoutParams(params); dots.add(imageView); //由于刚进去引导页时出现的是第一个引导页,故此时的导航点设置为红色 if (i == 0) { dots.get(i).setBackgroundResource(R.drawable.dot_green); } else { dots.get(i).setBackgroundResource(R.drawable.dot_gray); } llDots.addView(imageView); } }
导航点的父布局是LinearLayout,所以我们使用LinearLayout中的LayoutParams给导航点设置上下左右的边距。在onCreate中调用该方法就可以绘制导航点了。因为ViewPager默认显示的是第一页,所以我们默认第一个的导航点是选中的。
除此之外,我们还需要让导航点起到导航的作用。这时候就可以去调用ViewPager的页面滑动监听了:
/** * 滑动监听事件 */ vpNavigation.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { Log.e("Tag", "position=" + position); for (int i = 0; i < dots.size(); i++) { if (i == position) { dots.get(i).setBackgroundResource(R.drawable.dot_green); } else { dots.get(i).setBackgroundResource(R.drawable.dot_gray); } } } @Override public void onPageScrollStateChanged(int state) { } });
在选中某个页面时就将对应的导航点改为绿色就可以了。
4.5 图标的点击事件页面布局中的控件都是在适配器NavigationAdapter中初始化的,为了在MainActivity中能够实现它们的点击事件,我们在NavigationAdapter的构造方法中传入一个View.onClickListener的对象作为参数,只需在MainActivity继承View.OnClickListener接口,在创建NavigationAdapter时传入this即可。
当然,我们还需要在NavigationAdapter中让需要实现点击事件的控件调用setOnClickListener方法:
private void initListener() { llItem1.setOnClickListener(onClickListener); llItem2.setOnClickListener(onClickListener); llItem3.setOnClickListener(onClickListener); llItem4.setOnClickListener(onClickListener); llItem5.setOnClickListener(onClickListener); llItem6.setOnClickListener(onClickListener); llItem7.setOnClickListener(onClickListener); llItem8.setOnClickListener(onClickListener); }
大家想一想,initListener方法应该在哪里调用呢?这里有一个坑,由于ViewPager的页面是一页一页地绘制的,所以注册控件的点击事件也应该在for循环中,为每一页的控件都注册一遍:
for (int i = 0; i < pages; i++) { pageView = View.inflate(context, R.layout.page_navigation, null); ButterKnife.bind(this, pageView); setPagerViewData(i); itemViews.add(pageView); initListener(); }
好了,现在可以到MainActivity中实现监听事件了:
@Override public void onClick(View v) { int pageNum = vpNavigation.getCurrentItem(); Log.e("Tag", "viewPager.getCurrentItem()=" + vpNavigation.getCurrentItem()); switch (v.getId()) { case R.id.ll_item1: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 0).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 0).setSelected(true); adapter.refresh(itemBeanList); break; case R.id.ll_item2: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 1).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 1).setSelected(true); adapter.refresh(itemBeanList); break; case R.id.ll_item3: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 2).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 2).setSelected(true); adapter.refresh(itemBeanList); break; case R.id.ll_item4: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 3).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 3).setSelected(true); adapter.refresh(itemBeanList); break; case R.id.ll_item5: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 4).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 4).setSelected(true); adapter.refresh(itemBeanList); break; case R.id.ll_item6: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 5).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 5).setSelected(true); adapter.refresh(itemBeanList); break; case R.id.ll_item7: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 6).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 6).setSelected(true); adapter.refresh(itemBeanList); break; case R.id.ll_item8: Toast.makeText(this, itemBeanList.get(pageNum * 8 + 7).getIconName(), Toast.LENGTH_SHORT).show(); initSelected(); itemBeanList.get(pageNum * 8 + 7).setSelected(true); adapter.refresh(itemBeanList); break; default: break; } } /** * 初始化图标的选择状态,全部设置为未选中状态 */ private void initSelected() { for (int i = 0; i < itemBeanList.size(); i++) { itemBeanList.get(i).setSelected(false); } }
刷新Adapter时我是重新调用了initView方法,并且传入了修改后的数据源。下面是在Adapter中刷新的方法:
public void refresh(ListitemBeanList){ initView(itemBeanList); notifyDataSetChanged(); }
运行一下,点击,咦,怎么没有反应呢?PageAdapter这里有一个坑了:notifyDataSetChanged只有在ViewPager的页数发生变化才会刷新页面,单是某一个页面的数据发生变化时是没用的。但是不用着急,我们可以重写getItemPosition方法,让其返回POSITION_NONE,强迫viewpager重绘所有的页面。
/** * 使Adapter能够刷新布局 */ private int mChildCount = 0; @Override public void notifyDataSetChanged() { mChildCount = getCount(); super.notifyDataSetChanged(); } @Override public int getItemPosition(Object object) { if (mChildCount > 0) { mChildCount--; return POSITION_NONE; } return super.getItemPosition(object); }
运行一下,点击,发现可以完美实现我们要的效果了。
4.6 双击取消选中实现双击取消选中的效果并不难,我们只需在点击每一个图标之前都判断一下当前的选择状态,如果当前为选中的状态,则将状态改为未选中,反之,则改为选中。示例代码如下:
if (itemBeanList.get(pageNum * 8 + 0).isSelected()) { initSelected(); itemBeanList.get(pageNum * 8 + 0).setSelected(false); } else { initSelected(); itemBeanList.get(pageNum * 8 + 0).setSelected(true); }
我在代码中只设置了前面两项,大家如果想实现所有图标的双击选中事件的话就自己动一下手吧!
5、总结这个功能我前前后后花了不少时间,在参考别人代码的基础上总算是实现了,水平有限,如果博客中有错漏的地方,欢迎批评指正。我的写法还是太过简单粗暴,不够精巧,而且对资源的占用也比较大,如果你有更好的方法,欢迎交流学习。听说ViewPager+GridVie也可以实现同样的效果,接下来我再研究一下,看能不能再写一篇博客吧!
最后是源码下载地址:
GitHub
CSDN
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/67144.html
摘要:无论是跑马灯新闻标题还是饿了么的导航栏,它们的作用都是一样的,那就是复用有限的屏幕空间,展示更为丰富的内容。最后附上源码的地址参考文章之的简单使用仿淘宝首页的淘宝头条垂直滚动仿饿了么首页导航栏 在淘宝App的首页中间位置,有一块小小的地方在不知疲倦地循坏滚动着头条标题(见下图的红框区域),这样的设计无疑能够在有限的手机屏幕上展示更丰富的内容。而实现这一功能需要用到的控件就是我在上一篇文...
阅读 3030·2021-11-24 11:14
阅读 3332·2021-11-22 15:22
阅读 3174·2021-09-27 13:36
阅读 641·2021-08-31 14:29
阅读 1302·2019-08-30 15:55
阅读 1703·2019-08-29 17:29
阅读 1105·2019-08-29 16:24
阅读 2364·2019-08-26 13:48