AdapterView、Adapter深度学习

来源:互联网 发布:英雄联盟 知乎 编辑:程序博客网 时间:2024/05/17 08:33

博主工作了几年,也用了几年的ListView等AdapterView控件,但关于Adapter的一些问题并没有深入下去,终于有时间学习总结下关于BaseAdapter的一些较深入的话题。本文涉及三个话题:Adapter的回收机制和效率提升,getItemViewType()/getViewTypeCount()方法以及notifyDatasetChanged()使用的注意点。

1.Adapter的回收机制和效率提升

  Android在绘制Adapter时,系统首先调用getCount()方法,根据它的返回值得到ListView的长度,然后根据这个长度,调用getView()方法逐行绘制。如果ListView的长度超过了屏幕的长度,android只会绘制显示出来的Item,同时,系统会回收走隐藏的Item。

  如下图所示,此时系统绘制的只有position:4到positon12这9个Item.若按箭头方法滑动,将回收position12,以及绘制position3.

  总的来说,显示出来然后因为拖动而被隐藏的Item才会触发回收。在方法getView(int position, View convertView, ViewGroup parent)中,第二个参数convertView的含义:是代表系统最近回收的View。若整屏能显示9个Item,第一次打开带ListView的控件时,因为并没有回收的View,调用getVIew时,参数convertView的值会为null,否则将不是null,而是最近回收的View的引用.那么合理利用convertView将是提升Adapter效率的关键,否则将会产生大量的new View开销。

复制代码
 1 @Override 2     public View getView(int position, View convertView, ViewGroup parent)  3     { 4         Holder1 holder1 = null; 5         if(null==convertView)     6         { 7             System.out.println("convertView == null" + " position:" + position); 8             holder1=new Holder1();     9             convertView=LayoutInflater.from(mContext).inflate(R.layout.textview, null);10             holder1.textView=(TextView)convertView.findViewById(R.id.textview);    11             convertView.setTag(holder1);12         }    13         else    14         {15             holder1=(Holder1)convertView.getTag();16             System.out.println("重用:" + holder1.textView.getText());17         }18         holder1.textView.setText("position: "+position);   19         return convertView;             20     }  21     22     class Holder1    23     {24         public TextView textView;    25     }
复制代码

说明一下上图中的例子,按箭头方法拖动,接下来将显示position=4的Item,系统调用getView方法时,第二个参数convertView的值将是position=12的View的引用(最近回收的一个Item的View).[读者可在convertView中用一个TextView记录下每个View的position值,就可发现这个规律]
精致的逻辑说明:系统绘制Item的View和回收Item的View时有个规则:该Item只要显示出一点点就触发绘制,但必须等该Item完全隐藏之后才触发回收。试验上例时若结果对不上请注意这条说明。

2.getItemViewType()/getViewTypeCount()方法

  若果Item的View都是同一个模板则用不到这俩方法,但很多情况下我们的AdapterView中可能会用到2个或以上的不同的模板,那这些不同的模板如何复用,那就是这俩方法的作用。

  看下官方对convertView的解释:The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view. Heterogeneous lists can specify their number of view types, so that this View is always of the right type (see getViewTypeCount() and getItemViewType(int)). 意思大概就是: 在有些AdapterView的使用中,比如微博中 有的item中包含图片 有的item包含视频 那么必然的 我们需要用到2种item的布局方式此时如果只是单纯判断“null==convertView”,会造成回收的view不符合你当前需要的布局 而类似转换失败出错退出。

  代码示例:

 View Code

  

这个例子中有两点需要说明:

  1.在getItemTypeView()方法中的返回值不是随便设置的,在SDK中有句话“Note: Integers must be in the range 0 to getViewTypeCount() - 1”。也就是说:返回值得返回必须是0 - (getViewTypeCount()-1)范围内。

  2.关于setTag()和getTag()的理解:初学者对这两个方法可能不能很好的理解,调用setTag("")方法时,可以理解为为View设置了一个标识,然后通过getTag()来获取标识,或者理解为View作为一个容器除了显示一些字符串,图片之外,还可以通过setTag("")方法往其中存放一些数据,然后通过通过getTag()来获取数据。

  说明:我自己在学习这个知识点的过程中,产生了一个奇怪问题:我不用继承父类的这两个方法,自定义方法也可以完成这个功能,想通了之后发现时钻了牛角尖,就不讨论这个问题,若读者也产生了这个问题,可留言交流。

3.notifyDatasetChanged()使用

  首先看SDK中的说明:Notifies the attached observers that the underlying data has been changed and any View reflecting the data set should refresh itself. 这句话也好理解。我们在使用该方法过程中,有时候会发现不生效。

1 //我们通常通过构造器将list赋给自定义的Adapter2     ArrayList<String> list = new ArrayList<String>();3     MyAdapter adapter = new MyAdapter(context,list);4     5     list = query(...);6     adapter.notifyDataSetChanged();

这时notifyDataSetChanged()是不会生效的,应该改为:

1 list.clear();2 list.addAll(query(...));3 adapter.notifyDataSetChanged();

这不是android的问题,而是Java特性相关的问题。Java语言的变量中存的是引用。使用"list=query(...);"时,效果是改变了list的引用,而MyAdapter中使用的还是原来的引用,所以notifyDataSetChanged()时不能生效。正确的做法是通过方法来操作对象本身,而不是改变其引用。



***************************************************************************************************************************



Android中不能在子线程中更新View视图的原因

这是一条规律,很多coder知道,但原因是什么呢?

如下:

When a process is created for your application, its main thread is dedicated to running a message queue that takes care of managing the top-level application objects (activities, broadcast receivers, etc) and any windows they create. You can create your own threads, and communicate back with the main application thread through a Handler. This is done by calling the same post or sendMessage methods as before, but from your new thread. The given Runnable or Message will then be scheduled in the Handler's message queue and processed when appropriate.

SDK API中Handler章节中的一段。

意思是:

当进程的主线程创建后,将启动一个MessageQueue,用来管理Top-level组件对象(activity,broadcastReceiver)和你创建的所有窗口[也就是包括更新View的操作],你可以创建子线程,并在子线程中用Handler来与main线程通信。通过在子线程中调用Handler的post或sendMessage方法来实现。你所传递的Runnable或Message将在主线程Handler的MessageQueue中参与调度,并且在适当的时候执行。

 

最近在深入研究Handler,不是那么简单,累。




***************************************************************************************************************************

android:inputType参数类型说明

android:inputType="none"--输入普通字符

android:inputType="text"--输入普通字符

android:inputType="textCapCharacters"--输入普通字符

android:inputType="textCapWords"--单词首字母大

android:inputType="textCapSentences"--仅第一个字母大小

android:inputType="textAutoCorrect"--前两个自动完成

android:inputType="textAutoComplete"--前两个自动完成

android:inputType="textMultiLine"--多行输入

android:inputType="textImeMultiLine"--输入法多行(不一定支持)

android:inputType="textNoSuggestions"--不提示

android:inputType="textUri"--URI格式

android:inputType="textEmailAddress"--电子邮件地址格式

android:inputType="textEmailSubject"--邮件主题格式

android:inputType="textShortMessage"--短消息格式

android:inputType="textLongMessage"--长消息格式

android:inputType="textPersonName"--人名格式

android:inputType="textPostalAddress"--邮政格式

android:inputType="textPassword"--密码格式

android:inputType="textVisiblePassword"--密码可见格式

android:inputType="textWebEditText"--作为网页表单的文本格式

android:inputType="textFilter"--文本筛选格式

android:inputType="textPhonetic"--拼音输入格式

android:inputType="number"--数字格式

android:inputType="numberSigned"--有符号数字格式

android:inputType="numberDecimal"--可以带小数点的浮点格式

android:inputType="phone"--拨号键盘

android:inputType="datetime"

android:inputType="date"--日期键盘

android:inputType="time"--时间键盘


***************************************************************************************************************************

***************************************************************************************************************************
***************************************************************************************************************************
***************************************************************************************************************************

 首先说一点相关的知识:

  一:作用

  ContentProvider是不同应用程序共享数据的接口,跟共享数据的别的方法相比,ContentProvider更好地提供了数据共享接口的统一性。CP(ContentProvider的简称)通过一张或者多张表的形式向外部应用程序提供数据(就像关系型数据库中看见的表那样)。

  二:Content URIS

  URI(Uniform Resource Identifier)统一资源标示符,能表示provider中的数据。由三部分组成,分别是scheme,authority,path.它的scheme,Android系统规定为"content://",authority唯一表示了要访问的CP名字,而path表示了要访问的CP中的数据。在使用ContentResolver对象访问CP时,需要一个Uri参数。构造Uri时可能会用到三个类:

  Uri.Builder类可用来通过String构建Uri

  ContentUris.withAppendedId()可在Uri后面加一个id

  UriMatcher:在CP中这个类可帮你从接受到Uri中选择出要执行的ACTION

  三:MIME type

  MIME,多用途互联网邮件扩展,是一种互联网标准。它的作用是:服务器会通过MIME值来告诉客户端所传送的多媒体数据的类型。CP也是一种C/S架构,所以需要使用到这个值。它的格式为:type/subtype,比如text/html,代表所传输的文本为html的格式。在Android中MIME可提供两类值:统一的MIME数据类型和定制的MIME类型字符串。在CP中前者在传输文件时会用到,而后者更常用,发送结构化的数据,譬如SQLiteDatabase时会使用定制的MIME类型字符串。在android中后者有自己的规定:

  如果返回单行数据,type被设置为"vnd.android.cursor.item"

  如果返回多行数据,type被设置为"vnd.android.cursor.dir"

  而subtype部分由CP类自己设置。

  接下来进入正题,说代码环节,这篇代码是通过结构化数据来阐述CP的。

  一:客户端

  在客户端,是通过一个ContentResolver对象作为client和CP交互的。ContentResolver对象可调用CP中的同名方法,可提供“增删改查”的功能。

  Step1:在客户端的manifest文件申请要访问的CP的权限(该权限在CP端已向系统注册,下文会讲)

1 <uses-permission android:name="com.example.cpserver.permission" />

  Step1.在“增删改查”之前需要判断将要请求的Uri的有效性,代码如下:

复制代码
 1 //判断所要使用的Provider是否有效 2     private boolean checkValidProvider(Uri uri) 3     {  4         ContentProviderClient client = getContentResolver().acquireContentProviderClient(uri); 5         if(client == null) 6         { 7             System.out.println("provider is invalid!"); 8             return false; 9         }10         else11         {12             client.release();13             return true;14         }15     }
复制代码

  Step3:"增删改查"功能,这些方法都是SQL中DDL语言的一个封装,具体每个方法的返回值,参数意义不展开讲了,否则很长,可留言询问。

复制代码
 1 private int insert() 2     { 3         if(!checkValidProvider(Contract.CONTENT_URI)) 4             return -1; 5         ContentValues values = new ContentValues(); 6         values.put(Contract.COLUMN_NAME_1, "小卫的春天"); 7         values.put(Contract.COLUMN_NAME_2, "翟卫华"); 8         values.put(Contract.COLUMN_NAME_3, "100"); 9         Uri uri = getContentResolver().insert(Contract.CONTENT_URI, values);10         String lastPath = uri.getLastPathSegment();11         if(TextUtils.isEmpty(lastPath)) 12         {13             System.out.println("insert failure!");14         } 15         else 16         {17             System.out.println("insert success! the id is " + lastPath);18         }19         return Integer.parseInt(lastPath);20     }21     22     //删除所有行23     private int delete()24     {25         if(!checkValidProvider(Contract.CONTENT_URI))26             return -1;27         int count = getContentResolver().delete(Contract.CONTENT_URI, null, null);28         return count;29     }30     31     //将所有行的数据进行一个修改32     private int update()33     {34         if(!checkValidProvider(Contract.CONTENT_URI))35             return -1;36         ContentValues values = new ContentValues();37         values.put(Contract.COLUMN_NAME_1,"小宝的春天");38         values.put(Contract.COLUMN_NAME_2, "翟小宝");39         values.put(Contract.COLUMN_NAME_3, "200");40         int count = getContentResolver().update(Contract.CONTENT_URI, values, null, null);41         if(count == 0)42         {43             System.out.println("update failed!");44         }45         return count;46     }47     48     //row:要查找的行号, "-1"代表查找多行49     private void query(int row)50     {51         if(!checkValidProvider(Contract.CONTENT_URI))52             return;53         Cursor cursor = null;54         if(row != -1)55         {56             57         }58         else59         {60             cursor = getContentResolver().query(Contract.CONTENT_URI,61                     new String[]{Contract.COLUMN_NAME_1,Contract.COLUMN_NAME_2,Contract.COLUMN_NAME_3},null, null, null);62         }63         if(cursor == null)64             System.out.println("query failure!");65         else66         {67             String strDisplay = getDataFromCursor(cursor);    68             tvDisplay.setText(strDisplay);69             cursor.close();70         }71     }
复制代码

  二:CP端

  在CP端要通过继承ContentProvider来实现。CP是Android的四大组件之一,比较重要,而且系统本身就能提供很多的ContentProvider供开发者使用,比如可通过CP请求道所有的图片,音频,视频等数据。在Android系统中CP默认是可被别的任何应用程序请求到的,如果你不设置权限加以限制的话。所以第一步应该设置一个permission字段,并把它添加到provider标签里面去。

复制代码
 1 <permission  2     android:name="com.example.cpserver.permission" 3     android:label="Example Data" 4     android:protectionLevel="signature" /> 5  6 <provider 7             android:name="provider.TestProvider" 8             android:authorities="com.example.cpserver.provider" 9             android:permission="com.example.cpserver.permission"10             android:exported="true"11             android:enabled="true" />
复制代码

  Step1:实现一个SQLiteOpenHelper作为Provider的数据存储库

复制代码
 1 package db; 2  3 import com.example.cpserver.Contract; 4  5 import android.content.Context; 6 import android.database.sqlite.SQLiteDatabase; 7 import android.database.sqlite.SQLiteOpenHelper; 8 /* 9  * 实现一个SQLiteOpenHelper作为Provider的数据存储库10  */11 public final class MainDatabaseHelper extends SQLiteOpenHelper {12 13     //创建一张表的SQL语句14     private static final String SQL_CREATE_MAIN = "CREATE TABLE " +15     Contract.TABLE_NAME + "(" +                           16     " _ID INTEGER PRIMARY KEY, " +17     Contract.COLUMN_NAME_1 + " TEXT," +18     Contract.COLUMN_NAME_2 + " TEXT," +19     Contract.COLUMN_NAME_3 + " INTEGER )";20     21     public MainDatabaseHelper(Context context) 22     {23         super(context, Contract.DB_NAME, null, 1);24     }25 26     /*27      *当Provider设法打开数据存储库,并且数据库不存在时该方法被调用28      */29     @Override30     public void onCreate(SQLiteDatabase db) {31         // Creates the main table32         db.execSQL(SQL_CREATE_MAIN);33     }34 35     @Override36     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {37         // TODO Auto-generated method stub38         39     }40 }
复制代码

  Step2:实现ContentProvider的子类,并且扩展抽象方法,代码注释很详细。

复制代码
  1 package provider;  2   3 import com.example.cpserver.Contract;  4 import db.MainDatabaseHelper;  5 import android.content.ContentProvider;  6 import android.content.ContentUris;  7 import android.content.ContentValues;  8 import android.content.UriMatcher;  9 import android.database.Cursor; 10 import android.database.SQLException; 11 import android.database.sqlite.SQLiteDatabase; 12 import android.net.Uri; 13 import android.text.TextUtils; 14  15 /* 16  * 1.除了onCreate方法,别的方法都可能会被多线程调用,所以这些方法要设计成线程安全的 17  * 2.在onCreate中避免耗时操作 18  * 3.虽然以下方法都要被继承,但不必重写每个方法,除了getType 19  */ 20 public class TestProvider extends ContentProvider  21 { 22     //创建一个UriMatcher对象,该对象帮你从接受的URI中选择出要执行的动作 23     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 24     private MainDatabaseHelper mOpenHelper; 25     private SQLiteDatabase db = null; 26      27     //调用addURI方法添加provider可以识别的所有URI类型 28     static 29     { 30         sUriMatcher.addURI(Contract.authority, Contract.TABLE_NAME, 1); 31         sUriMatcher.addURI(Contract.authority, Contract.TABLE_NAME+"/#", 2); 32     } 33     /* 34      * 该方法作用:初始化该Provider 35      * 注意:直到一个ContentResolver对象访问时,该方法才被调用 36      * 这个方法里不应做耗时的操作,因为这样可能延缓Provider对别的应用程序的相应 37      */ 38     @Override 39     public boolean onCreate() { 40         mOpenHelper = new MainDatabaseHelper(getContext()); 41         return true; 42     } 43  44     @Override 45     public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder)  46     { 47         int type = sUriMatcher.match(uri); 48         switch (type) 49         { 50         //多行请求的URI 51         case 1: 52             if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC"; 53             break; 54         case 2: 55             //单行请求的URI 56             selection = selection + "_ID = " + uri.getLastPathSegment(); 57             break; 58         default: 59            //如果URI不匹配,做一些错误提示,返回null或者抛出异常 60             throw new IllegalArgumentException("Unknown URI " + uri); 61         } 62         if(db == null) 63             db = mOpenHelper.getWritableDatabase(); 64         Cursor cursor = db.query(Contract.TABLE_NAME, projection, selection, selectionArgs, null, null,sortOrder); 65         return cursor; 66     } 67  68     //返回content URI相应的MIME 类型 69     @Override 70     public String getType(Uri uri)  71     { 72         int type = sUriMatcher.match(uri); 73         switch (type)  74         { 75         case 1: 76             return Contract.CONTENT_TYPE; 77         case 2: 78             return Contract.CONTENT_ITEM_TYPE; 79         default: 80             throw new IllegalArgumentException("Unknown URI " + uri); 81         } 82     } 83  84     @Override 85     public Uri insert(Uri uri, ContentValues initialValues)  86     { 87         int type = sUriMatcher.match(uri); 88         if(type != 1) 89             throw new IllegalArgumentException("Unknown URI " + uri); 90          91         //创建一个可写数据库,将调用MainDatabaseHelper的onCreate方法,如果数据库还不存在的话 92         if(db == null) 93             db = mOpenHelper.getWritableDatabase(); 94          95         //确保所有的域都被设置 96         ContentValues values; 97         if (initialValues != null)  98             values = new ContentValues(initialValues); 99         else 100             values = new ContentValues();101         if (values.containsKey(Contract.COLUMN_NAME_1) == false) {102             values.put(Contract.COLUMN_NAME_1, "");103         }104         if (values.containsKey(Contract.COLUMN_NAME_2) == false) {105             values.put(Contract.COLUMN_NAME_2, "");106         }107         if (values.containsKey(Contract.COLUMN_NAME_3) == false) {108             values.put(Contract.COLUMN_NAME_3, "");109         }110         111         long rowId = db.insert(Contract.TABLE_NAME,null, values);112         if(rowId > 0)113         {114             Uri noteUri = ContentUris.withAppendedId(Contract.CONTENT_URI, rowId);115             getContext().getContentResolver().notifyChange(noteUri, null);116             return noteUri;117         }118         throw new SQLException("Failed to insert row into " + uri);119     }120 121     @Override122     public int delete(Uri uri, String selection, String[] selectionArgs) 123     {124         if(db == null)125             db = mOpenHelper.getWritableDatabase();126         int type = sUriMatcher.match(uri);127         int count;128         switch (type)129         {130             case 1:131                 count = db.delete(Contract.TABLE_NAME, selection, selectionArgs);132                 break;133             case 2:134                 String noteId = uri.getLastPathSegment();135                 count = db.delete(Contract.TABLE_NAME, "_ID" + "=" + noteId +136                         (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""), selectionArgs);137                 break;138             default:139                 throw new IllegalArgumentException("Unknown URI " + uri);140         }141         getContext().getContentResolver().notifyChange(uri, null);142         return count;143     }144 145     @Override146     public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) 147     {148         if(db == null)149             db = mOpenHelper.getWritableDatabase();150         int type = sUriMatcher.match(uri);151         int count;152         switch (type)153         {154             case 1:155                 count = db.update(Contract.TABLE_NAME, values, selection, selectionArgs);156                 break;157             case 2:158                 String noteId = uri.getLastPathSegment();159                 count = db.update(Contract.TABLE_NAME, values,"_ID" + "=" + noteId +160                         (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""), selectionArgs);161                 break;162             default:163                 throw new IllegalArgumentException("Unknown URI " + uri);164         }165         getContext().getContentResolver().notifyChange(uri, null);166         return count;167     }168 169     //如果Provider提供file数据,要用这个方法返回MIME类型170     @Override171     public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {172         // TODO Auto-generated method stub173         return super.getStreamTypes(uri, mimeTypeFilter);174     }175 176 }
复制代码

 

  我认为CP之所以不那么容易理解,是因为它涉及到的东西较多,还涉及到计算机网络中的的URI,MIME等概念,确实不是那么容易,作为码农的我们只能去啃了,无别的办法。总结一下,CP机制中主要涉及到这么几点知识:permission权限,uri,MIME,使用UriMatcher来匹配uri的类型,还要掌握一些基本的SQL语言等。关于使用CP来实现File分享,又是很长的一个篇幅,等得空再研究吧。我的项目用百度云做个下载链接吧,有需求的朋友可拿去用。欢迎留言交流。


***************************************************************************************************************************
***************************************************************************************************************************
0 0
原创粉丝点击