项目里要加一个点击可收缩展开的列表,要求带悬停标题,详细效果例如以下图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

也就是说。在某一个分组内部滚动时,要求分组标题悬停。当滚出该分组范围时,把标题顶出去。悬停下一个分组的标题。正好看到一个比較有趣的思路。做了一个实现,在这里分享一下。

代码结构例如以下。基本上是一个MVC的架构:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

既然是点击可收缩展开的列表,显然要用ExpandableListView,关于这个类的使用方法这里就不赘述了。网上一搜一大把,事实上跟ListView的使用方法几乎相同。只是它帮你分了组,所以原来Adapter里的getView()就变成了getGroupView()和getChildView()。getCount()就变成了getGroupCount()等等。另外既然要支持收缩展开,必定会提供collapseGroup()和expandGroup()等接口。

以下分析怎样加入悬停标题。事实上精华部分就一句话:悬停标题是画上去的。而不是加到view hierarchy里去,详细依据滚动的情况确定怎样画。

首先我们来写一个DockingExapandableListView类,继承自ExpandableListView,包括一个View类型的成员变量mDockingHeader。

一、重写onMeasure()和onLayout()方法

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mDockingHeader != null) {
measureChild(mDockingHeader, widthMeasureSpec, heightMeasureSpec);
mDockingHeaderWidth = mDockingHeader.getMeasuredWidth();
mDockingHeaderHeight = mDockingHeader.getMeasuredHeight();
}
} @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mDockingHeader != null) {
mDockingHeader.layout(0, 0, mDockingHeaderWidth, mDockingHeaderHeight);
}
}

这个比較简单。就是測量一下这个标题视图的宽度和高度。

二、重写dispatchDraw()方法

上面提到。悬停标题是画上去的。而不是加到view hierarchy里去的。因此,须要在完毕其它子view的绘制之后,再把悬停标题栏画上去:

    @Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mDockingHeaderVisible) {
// draw header view instead of adding into view hierarchy
drawChild(canvas, mDockingHeader, getDrawingTime());
}
}

三、依据滚动状态决定怎样绘制悬停标题

滚动到不同位置,悬停标题的显示是不同的,因此须要依据滚动状态定义一个状态机的切换。让DockingExpandableListView实现OnScrollListener接口,并重写onScroll()方法:

    @Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
long packedPosition = getExpandableListPosition(firstVisibleItem);
int groupPosition = getPackedPositionGroup(packedPosition);
int childPosition = getPackedPositionChild(packedPosition); // update header view based on first visible item
// IMPORTANT: refer to getPackedPositionChild():
// If this group does not contain a child, returns -1. Need to handle this case in controller.
updateDockingHeader(groupPosition, childPosition);
}

这里有几个比較有意思的方法,都是ExpandableListView自带的API:

getExpandableListPosition():这个API获得一个所谓的packed position,是一个64位的值。高32位表示group的ID,低32位表示在这个group内部的child ID。

getPackedPositionGroup():获取group ID,也就是高32位

getPackedPositionChild():获取child ID,也就是低32位

注意我们给getExpandableListPosition()传的參数是firstVisibleItem,因此我们就得到了最上方的第一个可见项所属的group以及组内位置。接下来就是最为关键的updateDockingHeader()方法。依据状态机来确定怎样绘制悬停标题。

在看这种方法之前,我们先看一下有哪几种状态,定义在IDockingController里:

public interface IDockingController {
int DOCKING_HEADER_HIDDEN = 1;
int DOCKING_HEADER_DOCKING = 2;
int DOCKING_HEADER_DOCKED = 3; int getDockingState(int firstVisibleGroup, int firstVisibleChild);
}

一共3种状态,这些状态都是什么含义呢?參见下图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

DOCKING_HEADER_HIDDEN:当分组没有展开,或者组里没有子项的时候,是不须要绘制悬停标题的

DOCKING_HEADER_DOCKING:当滚动到上一个分组的最后一个子项时,须要把旧的标题“推”出去。“停靠”新的标题,所以这个状态命名为“docking”

DOCKING_HEADER_DOCKED:新标题“停靠”完毕,在该分组内部滚动,称为“docked”状态

基于这个状态机,我们来看一下updateDockingHeader()方法的实现:

    private void updateDockingHeader(int groupPosition, int childPosition) {
if (getExpandableListAdapter() == null) {
return;
} if (getExpandableListAdapter() instanceof IDockingController) {
IDockingController dockingController = (IDockingController)getExpandableListAdapter();
mDockingHeaderState = dockingController.getDockingState(groupPosition, childPosition);
switch (mDockingHeaderState) {
case IDockingController.DOCKING_HEADER_HIDDEN:
mDockingHeaderVisible = false;
break;
case IDockingController.DOCKING_HEADER_DOCKED:
if (mListener != null) {
mListener.onUpdate(mDockingHeader, groupPosition, isGroupExpanded(groupPosition));
}
// Header view might be "GONE" status at the beginning, so we might not be able
// to get its width and height during initial measure procedure.
// Do manual measure and layout operations here.
mDockingHeader.measure(
MeasureSpec.makeMeasureSpec(mDockingHeaderWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(mDockingHeaderHeight, MeasureSpec.AT_MOST));
mDockingHeader.layout(0, 0, mDockingHeaderWidth, mDockingHeaderHeight);
mDockingHeaderVisible = true;
break;
case IDockingController.DOCKING_HEADER_DOCKING:
if (mListener != null) {
mListener.onUpdate(mDockingHeader, groupPosition, isGroupExpanded(groupPosition));
} View firstVisibleView = getChildAt(0);
int yOffset;
if (firstVisibleView.getBottom() < mDockingHeaderHeight) {
yOffset = firstVisibleView.getBottom() - mDockingHeaderHeight;
} else {
yOffset = 0;
} // The yOffset is always non-positive. When a new header view is "docking",
// previous header view need to be "scrolled over". Thus we need to draw the
// old header view based on last child's scroll amount.
mDockingHeader.measure(
MeasureSpec.makeMeasureSpec(mDockingHeaderWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(mDockingHeaderHeight, MeasureSpec.AT_MOST));
mDockingHeader.layout(0, yOffset, mDockingHeaderWidth, mDockingHeaderHeight + yOffset);
mDockingHeaderVisible = true;
break;
}
}
}

当中。是否显示悬停标题是通过一个叫做mDockingHeaderVisible的boolean变量控制的。这个在上面的dispatchDraw()方法里也见到了。

重点看“docking”状态的处理:通过计算第一个可见项的bottom和高度之间的差异,也就是这个yOffset,确定悬停标题在y轴方向的偏移量。这样在绘制悬停标题的时候。我们就仅仅能看到一部分,造成一种被“推出去”的感觉。

四、悬停标题状态机

在刚刚提到的那个IDockingController接口里有一个方法叫getDockingState(),在updateDockingHeader()方法里就是通过调用这种方法来确定当前悬停标题的状态的。DockingExpandableListViewAdapter实现了该接口和方法,完毕状态机状态转换:

    @Override
public int getDockingState(int firstVisibleGroup, int firstVisibleChild) {
// No need to draw header view if this group does not contain any child & also not expanded.
if (firstVisibleChild == -1 && !mListView.isGroupExpanded(firstVisibleGroup)) {
return DOCKING_HEADER_HIDDEN;
} // Reaching current group's last child, preparing for docking next group header.
if (firstVisibleChild == getChildrenCount(firstVisibleGroup) - 1) {
return IDockingController.DOCKING_HEADER_DOCKING;
} // Scrolling inside current group, header view is docked.
return IDockingController.DOCKING_HEADER_DOCKED;
}

逻辑很easy清晰:

假设当前group没有子项。而且也不是展开状态。就返回DOCKING_HEADER_HIDDEN状态,不绘制悬停标题。

假设到达了当前group的最后一个子项,进入DOCKING_HEADER_DOCKING状态;

其它情况,在当前group内部滚动,返回DOCKING_HEADER_DOCKED状态。

五、Touch事件处理

文章最前面提到过。这个标题视图是画上去。而不是加入到view hierarchy里的。因此它是无法响应touch事件的!

那就须要我们自己依据点击区域进行推断了。须要重写onInterceptTouchEvent()和onTouchEvent()方法:

    @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN && mDockingHeaderVisible) {
Rect rect = new Rect();
mDockingHeader.getDrawingRect(rect);
if (rect.contains((int)ev.getX(), (int)ev.getY())
&& mDockingHeaderState == IDockingController.DOCKING_HEADER_DOCKED) {
// Hit header view area, intercept the touch event
return true;
}
} return super.onInterceptTouchEvent(ev);
} // Note: As header view is drawn to the canvas instead of adding into view hierarchy,
// it's useless to set its touch or click event listener. Need to handle these input
// events carefully by ourselves.
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mDockingHeaderVisible) {
Rect rect = new Rect();
mDockingHeader.getDrawingRect(rect); switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (rect.contains((int)ev.getX(), (int)ev.getY())) {
// forbid event handling by list view's item
return true;
}
break;
case MotionEvent.ACTION_UP:
long flatPostion = getExpandableListPosition(getFirstVisiblePosition());
int groupPos = ExpandableListView.getPackedPositionGroup(flatPostion);
if (rect.contains((int)ev.getX(), (int)ev.getY()) &&
mDockingHeaderState == IDockingController.DOCKING_HEADER_DOCKED) {
// handle header view click event (do group expansion & collapse)
if (isGroupExpanded(groupPos)) {
collapseGroup(groupPos);
} else {
expandGroup(groupPos);
}
return true;
}
break;
}
} return super.onTouchEvent(ev);
}

这部分实现比較简单易懂,假设当前是DOCKING_HEADER_DOCKED状态。而且点击区域命中了标题视图的drawing rect,那么就须要拦截touch事件,而且在手指抬起时依据group当前的状态运行收起或者展开的动作。

六、更新标题视图内容

前面5步已经完毕了悬停标题状态机的控制,可是详细标题栏上应该怎么显示(比方变更标题文字、显示收缩展开图标等等),须要用户来处理。因此定义了一个IDockingHeaderUpdateListener接口。用户须要实现onUpdate()方法,依据当前的group ID以及收缩展开状态决定怎样更新悬停标题视图:

public interface IDockingHeaderUpdateListener {
void onUpdate(View headerView, int groupPosition, boolean expanded);
}

在demo该方法的实现就是简单的更新悬停标题栏的文字,详细參见MainActivity。

七、Adapter的数据源

这部分事实上就是给DockingExpandableListViewAdapter又封了一层adapter,由于有些方法实现过了。就把那些须要用户提供数据的方法单独拎出来封了一个IDockingAdapterDataSource接口。当然你也能够不用这个接口直接改Adapter。出于介绍的完整性考虑把接口贴在这里:

public interface IDockingAdapterDataSource {
int getGroupCount();
int getChildCount(int groupPosition);
Object getGroup(int groupPosition);
Object getChild(int groupPosition, int childPosition);
View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent);
View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent);
}

最后,也是最重要的部分。源代码下载地址:

演示样例代码下载 (CSDN)

https://github.com/qianxin2016/DockingExpandableListView

Android自己定义ViewGroup(二)——带悬停标题的ExpandableListView的更多相关文章

  1. Android自己定义ViewGroup打造各种风格的SlidingMenu

    看鸿洋大大的QQ5.0側滑菜单的视频课程,对于側滑的时的动画效果的实现有了新的认识,似乎打通了任督二脉.眼下能够实现随意效果的側滑菜单了.感谢鸿洋大大!! 鸿洋大大用的是HorizontalScrol ...

  2. Android 自己定义ViewGroup手把手教你实现ArcMenu

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/37567907 逛eoe发现这种UI效果,感觉非常不错,后来知道github上有这 ...

  3. Android 自己定义ViewGroup 实战篇 -&gt; 实现FlowLayout

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38352503 .本文出自[张鸿洋的博客] 1.概述 上一篇已经基本给大家介绍了怎 ...

  4. Android 自己定义View (二) 进阶

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24300125 继续自己定义View之旅.前面已经介绍过一个自己定义View的基础 ...

  5. android 自己定义ViewGroup实现可记载并呈现选择的ListView

    转载请注明出处:王亟亟的大牛之路 之前也做过一些用TextView之类的记录ListView选项的东西.可是总认为好难看.发现个不错的实现就贴给大家. 项目文件夹 执行效果: 自己定义视图: @Tar ...

  6. Android ViewDragHelper全然解析 自己定义ViewGroup神器

    转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/46858663. 本文出自:[张鸿洋的博客] 一.概述 在自己定义ViewGro ...

  7. 50个Android开发技巧(03 自己定义ViewGroup)

    问题:怎样创建一个例如以下图所看到的的布局?                图1 (原文地址:http://blog.csdn.net/vector_yi/article/details/244155 ...

  8. Android自己定义组件系列【1】——自己定义View及ViewGroup

    View类是ViewGroup的父类,ViewGroup具有View的全部特性.ViewGroup主要用来充当View的容器.将当中的View作为自己孩子,并对其进行管理.当然孩子也能够是ViewGr ...

  9. Android 开发实践 ViewGroup 实现左右滑出窗口(二)

    接上一篇 <Android 开发实践 ViewGroup 实现左右滑出窗口(一)http://www.cnblogs.com/inkheart0124/p/3532862.html> 源码 ...

随机推荐

  1. npm 主要命令

    本文主要参考自:http://www.runoob.com/nodejs/nodejs-npm.html 1.使用 npm 命令安装模块 $ npm install express var expre ...

  2. 〖Android〗联想K860 logcat CM11.0出错信息及解决

    错误1: D/gpsd ( ): main() D/gpsd ( ): argv[] = '/system/bin/glgps' D/gpsd ( ): argv[] = '-c' D/gpsd ( ...

  3. 转:Ogre源码剖析1

    初学Ogre 貌似看到一些套路(ajohn) 1 Ogre的编译  获得最新的Ogre 1.71 和之前的Ogre比起来,除了sampler集成之外,最大的改变就是编译过程加入了Cmake,这个东西其 ...

  4. 创业成本?亲身经历告诉你做一个app要多少钱

    导语:作为一名苦逼的移动互联网创业者,被外行的朋友们问及最多的问题是“做一个网站需要多少钱?”或者“做一个APP需要多少钱?” 作为一名苦逼的移动互联网创业者,被外行的朋友们问及最多的问题是“做一个网 ...

  5. 实现外卖选餐时两级 tableView 联动效果

    最近实现了下饿了么中选餐时两级tableView联动效果,先上效果图,大家感受一下: 下面说下具体实现步骤: 首先分解一下,实现这个需求主要是两点,一是点击左边tableView,同时滚动右边tabl ...

  6. WKWebView 使用及注意事项

    iOS8之后,苹果推出了WebKit这个框架,用来替换原有的UIWebView,新的控件优点多多.由于一直在适配iOS7,就没有去替换,现在仍掉了iOS7,以为很简单的就替换过来了,然而在替换的过程中 ...

  7. 使用libmagic确定文件MIME类型【示例】【转】

    原文地址:http://blog.csdn.net/vevenlcf/article/details/46122661 使用libmagic确定文件MIME类型[示例] 引用:   <http: ...

  8. React(0.13) 定义一个动态的组件(注释,样式)

    <!DOCTYPE html> <html> <head> <title>React JS</title> <script src=& ...

  9. 在Linux上rpm安装运行Redis 3.0.4

    http://www.rpmfind.net搜索redis,找到redis3.0.4的rpm源选做 wget ftp://fr2.rpmfind.net/linux/remi/enterprise/6 ...

  10. log4j(五)——如何控制不同目的地的日志输出?

    一:测试环境与log4j(一)——为什么要使用log4j?一样,这里不再重述 二:老规矩,先来个栗子,然后再聊聊感受 import org.apache.log4j.*; import java.io ...