当前位置: 移动技术网 > IT编程>移动开发>Android > Android仿eleme点餐页面二级联动列表

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

2019年07月24日  | 移动技术网IT编程  | 我要评论

云南移动12530,采购投标书,老师您辛苦了英文

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

这里写图片描述

右边的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还不一样,这个时候如果各自实现的话,既麻烦,又有冗余。像这样子:

// recyclerview
public 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.滑动时的逻辑...
    }
  }
}

// listview
public 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的联动…

彩蛋

高清无码类图:(完整)

这里写图片描述

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网