通信录列表+复杂Adapter分析

简介:

概述

最近写论文之余玩起了github,发现有个citypicker挺不错的,高仿了美团城市选择和定位的一些功能

地址链接 效果图如下:

这里写图片描述
这里写图片描述

自己手动写了一遍优化了一些内容,学到了一些姿势,下面对其中一些技术点做下总结。

  • 清晰的结构
  • SideLetterBar实现城市列表
  • 如何显示字母浮窗
  • 复杂的Adapter
  • ScrollView中嵌入ListView,GridView冲突的解决

清晰的结构

一般看到一个项目之前我会先看下他的结构规划,学习一下高手们架构上的意识,下面是目录结构

结构图

从这里清晰的看出MVC的模型,将工具类util、自定义view都进行了规整划分,这里由于只有一个Activity因此没有用单独的package,一般在项目大的时候还要单独划分,还有对应的网络模块

SideLetterBar.java

这个是仿通信录的的自定义view,主要涉及到了 拦截事件的分发处理、ondraw()方法、经典的回调机制

(关于回调可以参考android中的回调机制

非常非常适合新手学习事件处理时的练习材料!

  • 拦截事件的分发机制

当用于点击到这个区域时,事件分发时需要拦截该ActionDown事件,处理流程就是:点击那里就会出现一个字母的overlay,ActionMove时字母随之改变,ActionUp时字母的overlay就会消失,处理完return true表示该事件被本view消耗了,事件结束。

 @Override public boolean dispatchTouchEvent(MotionEvent event) {
 final int action = event.getAction();
 final float y = event.getY();
 final int oldChoose = choose;
 final OnLetterChangedListener listener = onLetterChangedListener;
 final int c = (int) (y / getHeight() * b.length);

 switch (action) {
 case MotionEvent.ACTION_DOWN:
 showBg = true;
 if (oldChoose != c && listener != null) {
 if (c >= 0 && c < b.length) {
 listener.onLetterChanged(b[c]);
 choose = c;
 invalidate();
 if (overlay != null){
 overlay.setVisibility(VISIBLE);
 overlay.setText(b[c]);
 }
 }
 }

 break;
 case MotionEvent.ACTION_MOVE:
 if (oldChoose != c && listener != null) {
 if (c >= 0 && c < b.length) {
 listener.onLetterChanged(b[c]);
 choose = c;
 invalidate();
 if (overlay != null){
 overlay.setVisibility(VISIBLE);
 overlay.setText(b[c]);
 }
 }
 }
 break;
 case MotionEvent.ACTION_UP:
 showBg = false;
 choose = -1;
 invalidate();
 if (overlay != null){
 overlay.setVisibility(GONE);
 }
 break;
 }
 return true;
 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

从上面可以清晰看出这里使用event.getAction()来获得点击事件,然后通过switch分别处理,这里要说一下通过点击位置获取准确字符的过程,关键代码及注释

final float y = event.getY();//获取当前像素长度
……
final int c = (int) (y / getHeight() * b.length);//通过当前像素长度/总像素长度*数组长度=当前点击在数组的index
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
  • onDraw()方法

该方法主要做俩件事,第一件将A-Z的字母绘绘制到控件中,第二件,点击时将控件背景颜色变化(这里选了透明色)

 @Override protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 if (showBg) {
 canvas.drawColor(Color.TRANSPARENT);
 }

 int height = getHeight();
 int width = getWidth();
 int singleHeight = height / b.length;
 for (int i = 0; i < b.length; i++) {
 paint.setTextSize(getResources().getDimension(R.dimen.side_letter_bar_letter_size));
 paint.setColor(getResources().getColor(R.color.gray));
 paint.setAntiAlias(true);
 if (i == choose) {
 paint.setColor(getResources().getColor(R.color.gray_deep));
 paint.setFakeBoldText(true); //加粗
 }
 float xPos = width / 2 - paint.measureText(b[i]) / 2;
 float yPos = singleHeight * i + singleHeight;
 canvas.drawText(b[i], xPos, yPos, paint);
 paint.reset();
 }

 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

这里核心代码是如何找到每个字母的绘制位置,关键代码如下:

 int singleHeight = height / b.length;//找出每个字母在当前手机上占的像素大小
 ……
 float xPos = width / 2 - paint.measureText(b[i]) / 2;//x的起始位置,计算了字符的宽度 float yPos = singleHeight * i + singleHeight;//for循环从0开始这里要为i+1
 canvas.drawText(b[i], xPos, yPos, paint);//画出字符 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但是我在csdn上随便一找,便看到2012年有人转载过这个控件文章,作者直接拿来用了,没做啥改动,这里的代码明显有些地方不够优雅,比如在ondraw中的for循环中每次都要设置画笔大小颜色啥的,变量定义也不明确比如ondraw中的c是什么鬼。。虽然无伤大雅但是看着总有些不舒服,这个控件在我的github中稍微做了一点改写

我的githubSideSelectBar.java

主要将paint画笔初始化位置做了改变、规范了变量,做了一些注释。

如何显示字母浮窗

主要使用overlay覆盖上去,在SideLetterBar 中留了一个函数

 /**
 * 设置悬浮的textview
 * @param overlay
 */ public void setOverlay(TextView overlay){
 this.overlay = overlay;
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然后在activty_city_list.xml布局文件中定义了一个长宽一致固定的textview,Android:id=”@+id/tv_letter_overlay”先把属性设置为gone,在CityPickerActivity中获取该空间通过setOverlay传到sideLetterBar中,在ActionDown处理中将该空间设置为visible然后通过settext设置对应的字母

 case MotionEvent.ACTION_DOWN:
 showBg = true;
 if (oldChoose != c && listener != null) {
 if (c >= 0 && c < b.length) {
 listener.onLetterChanged(b[c]);
 choose = c;
 invalidate();
 if (overlay != null){
 overlay.setVisibility(VISIBLE);
 overlay.setText(b[c]);
 }
 }
 }

 break;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

复杂的Adapter

在之前的博客中也介绍过这方面的内容

ListView中ConvertView和ViewHolder

一般是继承BaseAdapter然后遵循以下几步:
1.继承BaseAdapter
2.实现getCount(),getItem(),getItemId(),getView()
3.写一个ViewHolder内部类去存储复用的View
4.在getView()中实现数据的设置
这里要谈的是多个布局同时出现在一个listview中,如上图中在一个listview中出现了3个布局!如何做到的?
这时候就需要
重写 getViewTypeCount() – 返回你有多少个不同的布局
重写 getItemViewType(int) – 由position返回view type id
然后在getview中

 int viewType = getItemViewType(position);//获取对应的类型
  • 1
  • 1

然后通过switch case 不同的viewType在getView中convertView(即getView的第二个参数代码中为view)使用的不同的布局文件,简要代码如下:

 @Override public View getView(final int position, View view, ViewGroup parent) {
 CityViewHolder holder;
 int viewType = getItemViewType(position);
 switch (viewType){
 case 0: //定位
 view = inflater.inflate(R.layout.view_locate_city, parent, false);
 ViewGroup container = (ViewGroup) view.findViewById(R.id.layout_locate);
 ……
 });
 break;
 case 1: //热门
 view = inflater.inflate(R.layout.view_hot_city, parent, false);
 WrapHeightGridView gridView = (WrapHeightGridView) view.findViewById(R.id.gridview_hot_city);
 // GridView gridView = (GridView) view.findViewById(R.id.gridview_hot_city);
 ……
 });
 break;
 case 2: //所有 if (view == null){
 view = inflater.inflate(R.layout.item_city_listview, parent, false);
 ……
 });
 }
 break;
 }
 return view;
 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可以看出不同的case中通过inflate使用了不同的布局文件,关于inflate可以参看以前写的一篇博客

LayoutInflater和inflate的用法,有图有真相

ScrollView中嵌入ListView,GridView冲突的解决

可以看到作者在处理处理GridView时候特意对控件的高做了重写,否则gridview显示不全的,代码如下

public class WrapHeightGridView extends GridView { public WrapHeightGridView(Context context) {
 this(context, null);
 }

 public WrapHeightGridView(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
 }

 public WrapHeightGridView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 }

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
 super.onMeasure(widthMeasureSpec, heightSpec);
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这里关键代码

 int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);//表示测量格式的高最大是无穷大,因此按照实际情况完全显示出来
  • 1
  • 1

结束!
第一次尝试markdown感觉还不赖~

参考文章

http://blog.csdn.net/guozh/article/details/7568668 (2012年的仿通信录代码)
http://blog.csdn.net/jdsjlzx/article/details/8273661 (adapter)
http://www.aitinan.com/3885.html (adapter)

目录
相关文章
|
XML SQL 前端开发
Adapter基础讲解
这一节我们要讲的UI控件都是跟Adapter(适配器)打交道的,了解并学会使用Adapter很重要, Adapter是用来帮助填充数据的中间桥梁,简单来说就是:将各种数据以合适的形式显示到view上,提供 给用户看!
402 0
|
计算机视觉
RecyclerView#Adapter支持无数据布局、错误布局和列表尾部的”没有更多了“布局
实际开发中,UI小姐姐都会提供通用的`无数据页面`、`错误提示页面`。 针对常见的`支持下拉刷新和上拉加载更多的列表页面`,将他们的通用逻辑抽取出来,这样我们在开发过程中就只需要关注具体的业务逻辑了,无需每次通过cv来完善`无数据页面`、`错误提示页面`的逻辑了。
|
API 开发工具 git
使用RecycleView优雅的实现数据列表更新
使用RecycleView优雅的实现数据列表更新
531 0
使用RecycleView优雅的实现数据列表更新
|
API 数据处理 Apache
|
XML Android开发 数据格式
4-VI--☆ListView的封装支持多种条目
零、前言 [1.]封装了一晚,总算把多条目的ListView封装了一下 listview.gif 一、使用 1.初始化数据 ArrayList messages = new ArrayList(); messages.
835 0
|
XML Android开发 数据格式
Android开发技巧——使用RecyclerView实现分组列表
有一个多月没写原创博客了,今天来介绍一下使用RecyclerView来实现分组列表。之所以使用RecyclerView,主要原因还是因为项目开发中使用ExpandableListView无法实现设计师所需要的分割线。
3176 0
|
缓存
ListView 的 Adapter 适配器模板
Adapter优化.png ListView中的Adapter优化的通用模板。 如果发现对ListView优化工作做完后,还会卡顿,可以检查notifyDataSetChanged()方法是否运用合理,频繁的调用此方法也会引发卡顿。
872 0