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

Android仿eleme点餐页面二级联动列表,第1张

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

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

右边的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<VG extends VIEwGroup> {  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布局代码:

<?xml version="1.0" enCoding="utf-8"?><relativeLayout  xmlns:androID="http://schemas.androID.com/apk/res/androID"  xmlns:tools="http://schemas.androID.com/tools"  androID:layout_wIDth="match_parent"  androID:layout_height="match_parent">  <com.fashare.linkedscrolldemo.ui.linkedLayout    androID:ID="@+ID/linked_layout"    androID:layout_wIDth="match_parent"    androID:layout_height="match_parent"    androID:orIEntation="horizontal"/></relativeLayout>

擦,就没了嘛?剩下的得靠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还不一样,这个时候如果各自实现的话,既麻烦,又有冗余。像这样子:

// RecyclerVIEwpublic class RecyclerVIEwContentContainer extends BaseScrollableContainer<RecyclerVIEw> {  ...  @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.滑动时的逻辑...    }  }}// ListVIEwpublic class ListVIEwTabContainer extends BaseScrollableContainer<ListVIEw> {  ...  @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<VG> 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<VG extends VIEwGroup>    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的联动…

彩蛋

高清无码类图:(完整)

总结

以上是内存溢出为你收集整理的Android仿eleme点餐页面二级联动列表全部内容,希望文章能够帮你解决Android仿eleme点餐页面二级联动列表所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1147990.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存