热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

Android仿eleme点餐页面二级联动列表

本站一直在点外卖,于是心血来潮就像仿饿了么做个站,接下来通过本文给大家介绍android二级联动列表,仿eleme点餐页面的相关资料,需要的朋友可以参考下

本周末外卖点得多,就仿一仿“饿了么”好了。先上图吧,这样的订单页面是不是很眼熟:

这里写图片描述

右边的listview分好组以后,在左边的Tab页建立索引。可以直接导航,是不是很方便。关键在于右边滑动,左边也会跟着滑;而点击左边呢,也能定位右边的项。它们存在这样一种特殊的交互。像这种联动的效果,还有些常见的例子呢,比如知乎采用了常见的toolbar+viewPager的联动,只不过是上下布局:

这里写图片描述

再看看点评,它的城市选择页面也有这种联动的影子,只是稍微弱一点。侧边栏可以对listview进行索引,这最早是在微信好友列表里出现的把:

这里写图片描述

趁着周末,我也撸一个。就拓展性而言,应该可以适配以上所有情况吧。我称其为LinkedLayout,看下效果图:

这里写图片描述

我把右边按5个一组,可以看到,左边的索引 = 右边/5

特点

右边滑动,左边跟着动

左边滑动到边界,右边跟着动

点击左边tab项,右边滑动定位到相应的group

源码

github 传送门: https://github.com/fashare2015/LinkedScrollDemo

知识点

做之前先罗列一下知识点,或者说我们能从这个demo里收获到什么。

面向抽象/接口编程

自定义 view

代理模式

UML类图

复习 listview && recyclerview 的细节

感觉做完以后收获最大的还是第一点,面向接口编程。事实上,完成功能的时间只占了一半,后边的时间一直在抽象和重构;哎,一步到位太难了,还是老老实实写具体类,再抽取基类把。

构思

UI部分

LinkedLayout

要做的呢是两个相互关联的列表,在左边的作为tab页,右边的作为content页。先不考虑交互,我们来打个界面:搞一个叫做LinkedLayout的类,用来盛放tab和content:

这里写图片描述

public class LinkedLayout extends LinearLayout {
  private Context mContext;
  private BaseScrollableContainer mTabContainer;
  private BaseScrollableContainer mContentContainer;
  private SectionIndexer mSectionIndexer; // 代理
  ...
}

我们让它继承了LinearLayout,同时持有两个Container的东东,还有一个上帝对象mContext,以及一个分组用的SectionIndexer。

BaseScrollableContainer

先别管这些,主要看两个Container,从名字上看一个是tab页,一个是content页,嘿嘿。因为它们都能scroll嘛,干脆搞一个BaseScrollableContainer把。取名为Container呢,当然是致敬Fragment啦。我们来定义一下这个类:
初步一想,无非有一个 mContext, 一个 viewGroup, 还有一些 Listener 嘛:

这里写图片描述

public abstract class BaseScrollableContainer {
  protected Context mContext;
  public VG mViewGroup;
  protected RealOnScrollListener mRealOnScrollListener;
  private EventDispatcher mEventDispatcher;
  ...
}

和我们预想的差不多嘛,mContext上下文,mViewGroup基本就是指代我们的两个listview了吧。当然,我之后可是要做toolbar+viewpager的,肯定得依赖抽象,不能直接写listview啦。余下两个是Listener,等我们界面搭好,写交互的时候在看把。

看来UML图还是有好处的,继承和依赖关系一目了然。

自定义View && 动态布局

好了到了自定义view地环节了。我们已经有了一个LinkedLayout,这是我们的activity_main.xml布局代码:

<&#63;xml version="1.0" encoding="utf-8"&#63;>


  

擦,就没了嘛?剩下的得靠Java代码来搞啦。回到LinkedLayout咱们来布局UI~:

public class LinkedLayout extends LinearLayout {
  ...
  private static final int MEASURE_BY_WEIGHT = 0;
  private static final float WEIGHT_TAB = 1;
  private static final float WEIGHT_COnTENT= 3;

  public void setContainers(BaseScrollableContainer tabContainer, BaseScrollableContainer contentContainer) {
    mTabCOntainer= tabContainer;
    mCOntentContainer= contentContainer;
    mTabContainer.setEventDispatcher(this);
    mContentContainer.setEventDispatcher(this);

    // 设置 LayoutParams
    mTabContainer.mViewGroup.setLayoutParams(new LinearLayout.LayoutParams(
        MEASURE_BY_WEIGHT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
        WEIGHT_TAB
    ));

    mContentContainer.mViewGroup.setLayoutParams(new LinearLayout.LayoutParams(
        MEASURE_BY_WEIGHT,
        ViewGroup.LayoutParams.MATCH_PARENT,
        WEIGHT_CONTENT
    ));

    this.addView(mTabContainer.mViewGroup);
    this.addView(mContentContainer.mViewGroup);
    this.setOrientation(HORIZONTAL);
  }
}

搞了个setContainers用来注入我们的Container,里边有一些像layout_height,layout_width,layout_weight,orientation之类的,很眼熟吧,和xml没差。顺便一提的是,我们用了weight属性来控制这个比例1:3,一直感觉这个属性比较神奇。。。

注入ViewGroup, 使用自定义的LinkedLayout

到这里为止,LinkedLayout已经布局好了,我们分别注入ViewGroup就可以用了。我这里分别用listview作tab,recyclerview作content。想像力有限,用来用去好像也就这么几个控件。。。这部分代码很简单,在MainActivity里,就不贴了。

子类化 BaseScrollableContainer

按照常理,下边应该实现基类了吧。前面的MainActivity中,我们是这样实例化的:

mTabCOntainer= new ListViewTabContainer(this, mListView); 
mCOntentContainer= new RecyclerViewContentContainer(this, mRecyclerView);

看名字一个是listview填充的tab,一个是recyclerview填充的content。就先实现这两个类吧,从图中可以看到,它们分别继承于BaseScrollableContainer,并被LinkedLayout所持有:

这里写图片描述 

交互部分

与用户的交互:OnScrollListener 与 代理模式

终于到了交互部分,既然是滑动,那少不了定义监听器啦。然而,麻烦在于listview和recyclerview各自的OnScrollListener还不一样,这个时候如果各自实现的话,既麻烦,又有冗余。像这样子:

// RecyclerView
public class RecyclerViewContentContainer extends BaseScrollableContainer {
  ...
  @Override
  protected void setOnScrollListener() {
    mViewGroup.addOnScrollListener(new ProxyOnScrollListener());
  }

  private class ProxyOnScrollListener extends RecyclerView.OnScrollListener {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
      if(newState == RecyclerView.SCROLL_STATE_IDLE) {      // 停止滑动
        1.停止时的逻辑...
      }else if(newState == RecyclerView.SCROLL_STATE_DRAGGING){  // 按下拖动
        2.刚刚拖动时的逻辑...
      }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) { // 滑动
      3.滑动时的逻辑...
    }
  }
}

// ListView
public class ListViewTabContainer extends BaseScrollableContainer {
  ...
  @Override
  protected void setOnScrollListener() {
    mViewGroup.setOnScrollListener(new ProxyOnScrollListener());
    ...
  }

  public class ProxyOnScrollListener implements AbsListView.OnScrollListener{
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
      if(scrollState == SCROLL_STATE_IDLE) {       // 停止滑动
        1.停止时的逻辑...
      }else if(scrollState == SCROLL_STATE_TOUCH_SCROLL) // 按下拖动
        2.刚刚拖动时的逻辑...
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
      3.滑动时的逻辑...        // 滑动
    }
  }
}

那该怎么办呢,虽然各自的OnScrollListener差异挺大,但是仔细观察可以发现其实很多逻辑都是类似的,可以共用的。这时恰恰可以用代理模式来做重构。我抽取了1、2、3处的逻辑,由于在抽象意义上是一致的,可以整理成接口:

public interface OnScrollListener {
  // tab 点击事件
  void onClick(int position);

  // 1.滑动开始
  void onScrollStart();

  // 2.滑动结束
  void onScrollStop();

  // 3.触发 onScrolled()
  void onScrolled();

  // 用户手动滑, 触发的 onScrolled()
  void onScrolledByUser();

  // 程序调用 scrollTo(), 触发的 onScrolled()
  void onScrolledByInvoked();
}

与此同时,RecyclerView和ListView各自的监听器便分别作为代理类,把1、2、3的逻辑都委托给某个接盘侠,不必自己去实现,倒也落的轻松自在。如图所示:这里写图片描述

然后,让我们来看看这个接盘侠:RealOnScrollListener。。。

不愧是一个老实类,它老实地接盘了OnScrollListener的所有接口,并被两个代理类Proxy…所持有(图中并未画出。。)。
具体实现就不贴了,大家可以下源码来看。这里大致分析一下,它有三个成员:

public class RealOnScrollListener implements OnScrollListener {
  public boolean isTouching = false; // 处于触摸状态
  private int mCurPosition = 0;    // 当前选中项
  private BaseViewGroupUtil mViewUtil; // ViewGroup 工具类
  ...
}

isTouching:

为啥要维护这个触摸状态呢?这是由于我们的效果是联动的。这就比较讨厌了,当onScrolled()被调用,我们分不清是用户的滑动,还是来自另一个列表滑动时的联动效果。那我们记录一下isTouching状态呢,就能区分开这两种情况了。
更改isTouching的逻辑在onScrollStart()和onScrollStop()里边。

mCurPosition:

这个很好解释,我们每次滑动需要记录当前位置,然后通知另一个列表进行联动。
这段逻辑在onScrolled()里边。

mViewUtil:
一个工具库,用于简化逻辑。大概有scrollTo(),setViewSelected(),UpdatePosOnScrolled()等方法,如图:

这里写图片描述 

两个Container之间的交互

之前都是对用户的交互,终于到联动部分了。不急着实现,先回答我一个问题:假设我一个Activity里持有两个Fragment,问它们之间如何通信?

A同学大声道:用广播
B同学:EventBus !!!
C同学:看我 RxBus 。。。
别闹好吗。。。给我老老实实用Listener。显然,我们这里面临的是同样的场景。LinkedLayout=Activity,COntainer=Fragment。
动手前先定义Listener吧,要取个中二点的名字:

/*
 * 事件分发者
 */
public interface EventDispatcher {
  /**
   * 分发事件: fromView 中的 pos 被选中
   * @param pos
   * @param fromView
   */
  void dispatchItemSelectedEvent(int pos, View fromView);
}
/*
 * 事件接受者
 */
public interface EventReceiver {
  /**
   * 收到事件: 立即选中 newPos
   * @param newPos
   */
  void selectItem(int newPos);
}

然后LinkedLayout作为父级元素,肯定是分发者的角色,应当实现EventDispatcher;而BaseScrollableContainer作为子元素,接受该事件,应当实现EventReceiver。看下类图:

这里写图片描述

看下相应的实现(EventReceiver):

public abstract class BaseScrollableContainer
    implements EventReceiver {
  protected RealOnScrollListener mRealOnScrollListener;
  private EventDispatcher mEventDispatcher; // 持有分发者
  ...
  public void setEventDispatcher(EventDispatcher eventDispatcher) {
    mEventDispatcher = eventDispatcher;
  }
  // 掉用 mEventDispatcher,也就是 LinkedLayout
  protected void dispatchItemSelectedEvent(int curPosition){
    if(mEventDispatcher != null)
      mEventDispatcher.dispatchItemSelectedEvent(curPosition, mViewGroup);
  }
  @Override
  public void selectItem(int newPos) {
    mRealOnScrollListener.selectItem(newPos);
  }
  // OnScrollListener: 代理模式
  public class RealOnScrollListener implements OnScrollListener {
    ...
    public void selectItem(int position){
      mCurPosition = position;
      Log.d("setitem", position + "");
      // 来自另一边的联动事件
      mViewUtil.smoothScrollTo(position);
//      if(mViewUtil.isVisiblePos(position))  // curSection 可见时, 不滚动
        mViewUtil.setViewSelected(position);
    }
    @Override
    public void onClick(int position) {
      isTouching = true;
      mViewUtil.setViewSelected(mCurPosition = position);
      dispatchItemSelectedEvent(position); // 点击tab,分发事件
      isTouching = false;
    }
    ...
    @Override
    public void onScrolled() {
      mCurPosition = mViewUtil.updatePosOnScrolled(mCurPosition);
      if(isTouching)     // 来自用户, 通知 对方 联动
        onScrolledByUser();
      else          // 来自对方, 被动滑动不响应
        onScrolledByInvoked();
    }
    @Override
    public void onScrolledByUser() {
      dispatchItemSelectedEvent(mCurPosition);  // 来自用户, 通知 对方 联动
    }
  }
}

再看(EventDispatcher):

public class LinkedLayout extends LinearLayout implements EventDispatcher {
  private BaseScrollableContainer mTabContainer;
  private BaseScrollableContainer mContentContainer;
  private SectionIndexer mSectionIndexer; // 分组接口
  ...
  @Override
  public void dispatchItemSelectedEvent(int pos, View fromView) {
    if (fromView == mContentContainer.mViewGroup) { // 来自 content, 转发给 tab
      int cOnvertPos= mSectionIndexer.getSectionForPosition(pos);
      mTabContainer.selectItem(convertPos);
    } else {          // 来自 tab, 转发给 content
      int cOnvertPos= mSectionIndexer.getPositionForSection(pos);
      mContentContainer.selectItem(convertPos);
    }
  }
}

总结

到此为止,有没有一种酣畅淋漓的感觉?不管怎么说,面向对象是信仰,定义好接口以后,实现起来怎么写怎么舒服。
// TODO: 之前说了,这个联动是通用的。之后有时间会继续实现一个toolbar+viewPager的联动…

彩蛋

高清无码类图:(完整)

这里写图片描述


推荐阅读
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 【Windows】实现微信双开或多开的方法及步骤详解
    本文介绍了在Windows系统下实现微信双开或多开的方法,通过安装微信电脑版、复制微信程序启动路径、修改文本文件为bat文件等步骤,实现同时登录两个或多个微信的效果。相比于使用虚拟机的方法,本方法更简单易行,适用于任何电脑,并且不会消耗过多系统资源。详细步骤和原理解释请参考本文内容。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • 安卓select模态框样式改变_微软Office风格的多端(Web、安卓、iOS)组件库——Fabric UI...
    介绍FabricUI是微软开源的一套Office风格的多端组件库,共有三套针对性的组件,分别适用于web、android以及iOS,Fab ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
author-avatar
我才是陈墨_773
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有