Android-Service系列之断点续传下载
2016-02-14 12:12
423 查看
课程地址:http://www.imooc.com/learn/363
源码:http://download.csdn.net/download/qq_22804827/9424950
本次,将会利用Service进行一个单线程的断点续传下载的实例练习。
在开始之前呢,先进行一下简单的案例分析:
会涉及到一下几点内容:
基本UI定义
数据库的操作:在下载的同时将下载进度保存到数据库里面
Service的启动
Activity给Service传递参数
Service中使用广播回传数据到Activity中
线程和Handler
网络操作
还有,在网络下载的时候有几个关键点需要注意一下:
1. 获得网络文件的长度(即大小)
2. 在本地创建一个文件,设置其长度(相当于一个容器,存储下载的文件)
3. 从数据库中获得上次下载的进度
4. 从上次下载的位置下载数据,同时保存进度到数据库中
5. 将下载进度回传给Activity
6. 下载完成后删除下载信息
接下来,就正式开始吧:
(因为接下来还会有多线程的断点续传,所以在这次代码中的一些类,是基于多线程的断点续传而考虑的)
1.首先定义两个实体类
FileInfo.java
2.ThreadInfo.java
然后是有关数据库操作的几个类
1.DBHelper.java
2.ThreadDAO.java
3.ThreadDAOImpl.java
接着是要用到的Service类以及用于下载的类
1.DownloadService.java
2.DownloadTask.java
最后就是有关布局的代码以及相关的配置
1.activity_main.xml
2.MainActivity.java
3.在AndroidMainfest.xml中添加权限
注册服务
新增知识点:RandomAccessFile类的使用
本来是,是想另外用一篇博客来写开始提到的“Android-Service系列之多线程断点续传”课程的(http://www.imooc.com/learn/376),但是,在看了视频课程之后,顿时有种哔了狗的感觉,因为觉得老师讲得让我感觉有点凌乱,虽然在里面学到了几个好的思想,但是也有一些疏漏,而且让我感觉逻辑性不强。
下面先来说说几点不错的思想:
1、将DatabaseHelper修改成单例模式,防止多个实例同时对数据库进行操作
2、同时也要保证同一时刻只有一个线程能够对数据库的内容进行修改(即对数据库的增删改操作进行同步)
3、尽量把对数据库的操作放在线程外面去做,减少对数据库的锁定
4、因为这这个实例中设计的线程比较多,使得系统对线程的创建、销毁所占用的时间,以及性能的消耗是占了相当的比例的,所以在这里用到了线程池来进行优化
好了,说了我觉得不错的地方,就要来找找茬了(都怪小编的强迫症):
1、在视频的2-3节说提到的在适配器的getView方法中将一部分语句放在if语句里面,达到减少其执行次数来进行优化的做法是错误的,因为当复用item的时候,显示的文件名等会错乱
2、因为在DownloadTask的download方法中每个线程需要下载的文件长度总是由文件的总长度/线程数量决定的(相当于固定了的),所以即使在更新UI的时候把下载的进度保存了起来了,但是如果正在下载的文件没有按下暂停而关闭了程序,那么在重新打开程序后就要重头下载文件
3、因为这次的教程只有实现了同时下载几个文件,所以下显示下载任务的item都在同一屏,因此不会涉及到item的复用,但是如果多下载几个文件,要用到第二屏,就会因为item的复用出现按钮响应以及进度条显示的错乱
多线程断点续传涉及的源码(有做修改):
http://download.csdn.net/download/qq_22804827/9431070
源码:http://download.csdn.net/download/qq_22804827/9424950
本次,将会利用Service进行一个单线程的断点续传下载的实例练习。
在开始之前呢,先进行一下简单的案例分析:
会涉及到一下几点内容:
基本UI定义
数据库的操作:在下载的同时将下载进度保存到数据库里面
Service的启动
Activity给Service传递参数
Service中使用广播回传数据到Activity中
线程和Handler
网络操作
还有,在网络下载的时候有几个关键点需要注意一下:
1. 获得网络文件的长度(即大小)
2. 在本地创建一个文件,设置其长度(相当于一个容器,存储下载的文件)
3. 从数据库中获得上次下载的进度
4. 从上次下载的位置下载数据,同时保存进度到数据库中
5. 将下载进度回传给Activity
6. 下载完成后删除下载信息
接下来,就正式开始吧:
(因为接下来还会有多线程的断点续传,所以在这次代码中的一些类,是基于多线程的断点续传而考虑的)
1.首先定义两个实体类
FileInfo.java
/** * 文件实体类 * 将该类序列化后(即实现Serializable接口) * 就可以在intent中进行传递 * */ public class FileInfo implements Serializable{ private int id; private String url; private String fileName; /** * 文件的大小 */ private int length; /** * 文件的下载进度 */ private int progress; public FileInfo() { } public FileInfo(int id, String url, String fileName) { this.id = id; this.url = url; this.fileName = fileName; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public int getLength() { return length; } public void setLength(int length) { this.length = length; } public int getProgress() { return progress; } public void setProgress(int progress) { this.progress = progress; } @Override public String toString() { return "FileInfo [id=" + id + ", url=" + url + ", fileName=" + fileName + ", length=" + length + ", progress=" + progress + "]"; } }
2.ThreadInfo.java
/** * 线程信息实体类 * */ public class ThreadInfo { private int id; /** * 跟下载的文件的url一致 */ private String url; /** * 上次保存的文件的下载进度 */ private int start; /** * 代表的线程中下载文件的总长度 */ private int end; /** * 下载的进度(即文件下载到了哪儿,以字节数为单位) */ private int finished; public ThreadInfo() { } public ThreadInfo(int id, String url, int start, int end, int finished) { super(); this.id = id; this.url = url; this.start = start; this.end = end; this.finished = finished; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public int getStart() { return start; } public void setStart(int start) { this.start = start; } public int getEnd() { return end; } public void setEnd(int end) { this.end = end; } public int getFinished() { return finished; } public void setFinished(int finished) { this.finished = finished; } @Override public String toString() { return "ThreadInfo [id=" + id + ", url=" + url + ", start=" + start + ", end=" + end + ", finished=" + finished + "]"; } }
然后是有关数据库操作的几个类
1.DBHelper.java
/** * 数据库帮助类 * 用来创建数据库 * */ public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME="download.db"; private static final int VERSION=1;//数据库的版本 /** * 建表语法 */ private static final String TABLE_CREATE="create table thread_info(_id integer primary key autoincrement," + "thread_id integer,url text,start integer,end integer,finished integer)"; /** * 删表语法 */ private static final String TABLE_DROP="drop table if exits thread_info"; public DBHelper(Context context) { super(context, DB_NAME, null, VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(TABLE_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(TABLE_DROP); db.execSQL(TABLE_CREATE); } }
2.ThreadDAO.java
/** * 数据访问接口i * */ public interface ThreadDAO { /** * 插入线程信息 * @param threadInfo */ public void insertThreadInfo(ThreadInfo threadInfo); /** * 删除线程信息 * @param url 文件的url * @param id 线程的id */ public void deleteThreadInfo(String url,int id); /** * 更新线程下载进度 */ public void updateThreadInfo(String url,int threadId,int finished); /** * 查询下载文件的线程信息 */ public List<ThreadInfo> getThreads(String url); /** * 判断指定线程信息是否已经存在数据库中 */ public boolean isExists(String url,int threadId); }
3.ThreadDAOImpl.java
/** * 线程数据访问接口实现 * */ public class ThreadDAOImpl implements ThreadDAO { private DBHelper mHelper; public ThreadDAOImpl(Context context) { mHelper=new DBHelper(context); } @Override public void insertThreadInfo(ThreadInfo threadInfo) { SQLiteDatabase db=mHelper.getWritableDatabase(); db.execSQL( "insert into thread_info(thread_id,url,start,end,finished) values(?,?,?,?,?)", new Object[] { threadInfo.getId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished() }); db.close(); } @Override public void deleteThreadInfo(String url, int id) { SQLiteDatabase db=mHelper.getWritableDatabase(); db.execSQL("delete from thread_info where url = ? and thread_id = ?", new Object[]{url,id}); db.close(); } @Override public void updateThreadInfo(String url, int threadId,int finished) { SQLiteDatabase db=mHelper.getWritableDatabase(); db.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?", new Object[]{finished,url,threadId}); db.close(); } @Override public List<ThreadInfo> getThreads(String url) { List<ThreadInfo> list=null; SQLiteDatabase db=mHelper.getWritableDatabase(); Cursor cursor=db.rawQuery("select * from thread_info where url = ?",new String[]{url}); if (cursor != null) { list=new ArrayList<ThreadInfo>(); while (cursor.moveToNext()) { ThreadInfo temp = new ThreadInfo(); temp.setId(cursor.getInt(cursor.getColumnIndex("thread_id"))); temp.setUrl(cursor.getString(cursor.getColumnIndex("url"))); temp.setStart(cursor.getInt(cursor.getColumnIndex("start"))); temp.setEnd(cursor.getInt(cursor.getColumnIndex("end"))); temp.setFinished(cursor.getInt(cursor .getColumnIndex("finished"))); list.add(temp); } cursor.close(); } db.close(); return list; } @Override public boolean isExists(String url, int threadId) { SQLiteDatabase db=mHelper.getWritableDatabase(); Cursor cursor=db.rawQuery("select * from thread_info where url = ? and thread_id = ?",new String[]{url,""+threadId}); boolean exists=false; if(cursor!=null) { exists=cursor.moveToNext(); } db.close(); return exists; } }
接着是要用到的Service类以及用于下载的类
1.DownloadService.java
//每个服务都只会存在一个实例 public class DownloadService extends Service { public static final String ACTION_DOWNLOAD="DOWNLOAD"; public static final String ACTION_STOP="STOP"; public static final String ACTION_UPDATE="UPDATE"; /** * 存放下载文件的文件夹路径 */ public static final String DOWNLOAD_PATH = Environment .getExternalStorageDirectory().getAbsolutePath() + "/DownloadsTest/"; private static final int MSG_INIT=0;//代表创建本地文件完成 private DownloadTask mTask; /** * 在每次服务启动的时候调用 * 如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand()方法里 */ @Override public int onStartCommand(Intent intent, int flags, int startId) { //获得Activity传来的参数 if(ACTION_DOWNLOAD.equals(intent.getAction())) { if(mTask==null||mTask.isPause||mTask.isFinished) { //这里的判断语句要将mTask==null放在最前面,因为mTask还没有new出来 FileInfo fileInfo = (FileInfo) intent .getSerializableExtra("fileInfo"); Log.d("测试", "ACTION_DOWNLOAD:" + fileInfo.toString()); // 启动初始化线程 new InitThread(fileInfo).start(); } } else if(ACTION_STOP.equals(intent.getAction())){ if (mTask!=null&&!mTask.isPause&&!mTask.isFinished) { //这里的判断语句要将mTask!=null放在最前面,因为mTask还没有new出来 FileInfo fileInfo = (FileInfo) intent .getSerializableExtra("fileInfo"); Log.d("测试", "ACTION_STOP:" + fileInfo.toString()); if (mTask != null) { mTask.isPause = true; } } } return super.onStartCommand(intent, flags, startId); } @Override public IBinder onBind(Intent intent) { return null; } private Handler mHandler=new Handler() { public void handleMessage(android.os.Message msg) { switch(msg.what) { case MSG_INIT: FileInfo fileInfo=(FileInfo) msg.obj; Log.d("测试", "mHandler"+fileInfo.toString()); //启动下载任务 mTask=new DownloadTask(DownloadService.this, fileInfo); mTask.download(); break; } } }; /** * 从网上读取文件的长度然后再本地建立文件 */ private class InitThread extends Thread { private FileInfo mFileInfo; public InitThread(FileInfo fileInfo) { mFileInfo=fileInfo; } public void run() { Log.d("测试", "InitThread"); HttpURLConnection connection=null; RandomAccessFile raf=null; try { //连接网络文件 URL url=new URL(mFileInfo.getUrl()); connection=(HttpURLConnection) url.openConnection(); connection.setConnectTimeout(3000);//设置连接超时 connection.setReadTimeout(3000);//设置读取超时 connection.setRequestMethod("GET"); int length=-1; if(connection.getResponseCode()==HttpStatus.SC_OK) {//判断是否成功连接 //获得文件长度 length=connection.getContentLength(); } if(length<=0) { return; } File dir=new File(DOWNLOAD_PATH); if(!dir.exists()) {//判断存放下载文件的文件的文件夹是否存在 dir.mkdir(); } //在本地创建文件 File file=new File(dir,mFileInfo.getFileName()); raf=new RandomAccessFile(file,"rwd");//随机存取文件,用于断点续传,r-读取/w-写入/d-删除权限 //设置本地文件长度 raf.setLength(length); mFileInfo.setLength(length); mHandler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget(); } catch (Exception e) { } finally { try { raf.close(); connection.disconnect(); } catch (Exception e) { } } } } }
Activity属于一个前台的组件,有可能会被用户关闭,或者被切到后台(这时就有可能会被安卓系统回收),如果在Activity中创建一个线程去下载,一旦Activity被回收,就无法对在其中创建的线程进行管理,会导致不必要的麻烦 而Service的优先级比较高,一般不会被系统回收
2.DownloadTask.java
/** * 下载任务类 * */ public class DownloadTask { private Context mContext; private FileInfo mFileInfo; private ThreadDAO mDAO; private int mFinished;//用于更新UI的下载进度 public boolean isPause;//判断是否正在下载 public boolean isFinished;//判断是否下载完成 public DownloadTask(Context context, FileInfo fileInfo) { super(); this.mContext = context; this.mFileInfo = fileInfo; mDAO=new ThreadDAOImpl(context); isPause=false; isFinished=false; } public void download() { Log.d("测试","download"); //读取上次的线程信息 List<ThreadInfo> list=mDAO.getThreads(mFileInfo.getUrl()); ThreadInfo threadInfo=null; if(list.size()==0||list==null) {//有可能是第一次下载,数据库中还没有信息 threadInfo=new ThreadInfo(0,mFileInfo.getUrl(),0,mFileInfo.getLength(),0); } else { //因为这里是单线程下载,所以这里直接get(0)就行了,下次会涉及多线程 threadInfo=list.get(0); } new DownloadThread(threadInfo).start(); } /** * 下载线程 * */ class DownloadThread extends Thread { private ThreadInfo mThreadInfo; public DownloadThread(ThreadInfo ThreadInfo) { mThreadInfo=ThreadInfo; } public void run() { Log.d("测试","DownloadThread"); //向数据库中插入线程信息 if(!mDAO.isExists(mThreadInfo.getUrl(), mThreadInfo.getId())) { mDAO.insertThreadInfo(mThreadInfo); } HttpURLConnection connection=null; RandomAccessFile raf=null; InputStream input=null; try { URL url=new URL(mThreadInfo.getUrl()); connection=(HttpURLConnection) url.openConnection(); connection.setConnectTimeout(3000); connection.setRequestMethod("GET"); //设置下载位置 int start=mThreadInfo.getFinished();//上一次保存的下载进度即为这次要开始下载的地方 connection.setRequestProperty("Range", "bytes="+start+"-"+mThreadInfo.getEnd()); //设置请求属性,将field参数设置为Range(范围),newValues为指定的字节数区间 //设置文件写入位置 File file=new File(DownloadService.DOWNLOAD_PATH,mFileInfo.getFileName()); raf=new RandomAccessFile(file, "rwd"); raf.seek(start); //在读写的时候跳过设置的字节数,从下一个字节数开始读写 //如seek(100)则跳过100个字节从第101个字节开始读写 Intent intent=new Intent(DownloadService.ACTION_UPDATE); mFinished=start; //开始下载 if(connection.getResponseCode()==HttpStatus.SC_PARTIAL_CONTENT) { //因为前面的RequestProperty设置的Range,服务器会认为进行部分的下载,所以这里判断是否成功连接要用SC_PARTIAL_CONTENT //读取数据 input=connection.getInputStream(); byte[] buffer=new byte[1024*4]; int len=-1;//标记每次读取的长度 long time=System.currentTimeMillis(); while((len=input.read(buffer))!=-1) { //写入文件 raf.write(buffer,0,len); //把下载进度发送给Activity mFinished += len; if(System.currentTimeMillis()-time>500) {//因为该循环运行较快,所以这里减缓一下UI更新的频率 time=System.currentTimeMillis(); intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength()); mContext.sendBroadcast(intent); Log.d("测试", ""+mFinished * 100 / mFileInfo.getLength()); mDAO.updateThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId(), mFinished); //如果隔断时间就更新一下数据库的内容 //可以防止没有按下暂停就关闭程序重进后要重新开始下载的问题 //而又不至于更新数据库太频繁,影响效率 } //在下载暂停时,保存下载进度至数据库 if(isPause) { mDAO.updateThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId(), mFinished); intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength()); mContext.sendBroadcast(intent); raf.close(); input.close(); connection.disconnect(); return; } } intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength()); mContext.sendBroadcast(intent); //因为有可能在刚好下载完成的时候没有进入到if(isPause)中,所以进度条会停在上次更新的时候,显示的时候还有一小段没有下载,但是实际已经下载完成了 //删除线程信息 mDAO.deleteThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId()); isFinished=true; } } catch (Exception e) { } finally { try { raf.close(); input.close(); connection.disconnect(); } catch (Exception e) { } } } } }
最后就是有关布局的代码以及相关的配置
1.activity_main.xml
<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" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.downloaddemo.MainActivity" > <TextView android:id="@+id/id_fileName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="文件名" /> <ProgressBar android:id="@+id/id_progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignLeft="@+id/id_fileName" android:layout_below="@+id/id_fileName" android:layout_marginTop="15dp" /> <Button android:id="@+id/id_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/id_download" android:layout_alignBottom="@+id/id_download" android:layout_alignRight="@+id/id_progressBar" android:text="暂停" /> <Button android:id="@+id/id_download" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/id_progressBar" android:layout_marginTop="22dp" android:layout_toLeftOf="@+id/id_stop" android:text="下载" /> </RelativeLayout>
2.MainActivity.java
public class MainActivity extends Activity { private ProgressBar mProgressBar; private Button mStop; private Button mDownload; private TextView mFileName; private FileInfo fileInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mFileName=(TextView) findViewById(R.id.id_fileName); mProgressBar=(ProgressBar) findViewById(R.id.id_progressBar); mProgressBar.setMax(100); mStop=(Button) findViewById(R.id.id_stop); mDownload=(Button) findViewById(R.id.id_download); //注册广播接收器 IntentFilter filter=new IntentFilter(); filter.addAction(DownloadService.ACTION_UPDATE); registerReceiver(mReceiver, filter); //创建文件信息对象 String url="http://www.imooc.com/mobile/imooc.apk"; fileInfo=new FileInfo(0,url,"imooc.apk"); mDownload.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_DOWNLOAD); intent.putExtra("fileInfo", fileInfo); startService(intent); } }); mStop.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_STOP); intent.putExtra("fileInfo", fileInfo); startService(intent); } }); } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(mReceiver); } /** * 更新UI广播的广播接收器 */ BroadcastReceiver mReceiver=new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if(intent.getAction().equals(DownloadService.ACTION_UPDATE)) { Log.d("测试", "mReceiver"); mFileName.setText(fileInfo.getFileName()); int finished=intent.getIntExtra("finished", 0); mProgressBar.setProgress(finished); } } }; }
3.在AndroidMainfest.xml中添加权限
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
注册服务
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > ... <service android:name="com.example.downloaddemo.services.DownloadService"></service> ... </application>
新增知识点:RandomAccessFile类的使用
本来是,是想另外用一篇博客来写开始提到的“Android-Service系列之多线程断点续传”课程的(http://www.imooc.com/learn/376),但是,在看了视频课程之后,顿时有种哔了狗的感觉,因为觉得老师讲得让我感觉有点凌乱,虽然在里面学到了几个好的思想,但是也有一些疏漏,而且让我感觉逻辑性不强。
下面先来说说几点不错的思想:
1、将DatabaseHelper修改成单例模式,防止多个实例同时对数据库进行操作
2、同时也要保证同一时刻只有一个线程能够对数据库的内容进行修改(即对数据库的增删改操作进行同步)
3、尽量把对数据库的操作放在线程外面去做,减少对数据库的锁定
4、因为这这个实例中设计的线程比较多,使得系统对线程的创建、销毁所占用的时间,以及性能的消耗是占了相当的比例的,所以在这里用到了线程池来进行优化
好了,说了我觉得不错的地方,就要来找找茬了(都怪小编的强迫症):
1、在视频的2-3节说提到的在适配器的getView方法中将一部分语句放在if语句里面,达到减少其执行次数来进行优化的做法是错误的,因为当复用item的时候,显示的文件名等会错乱
2、因为在DownloadTask的download方法中每个线程需要下载的文件长度总是由文件的总长度/线程数量决定的(相当于固定了的),所以即使在更新UI的时候把下载的进度保存了起来了,但是如果正在下载的文件没有按下暂停而关闭了程序,那么在重新打开程序后就要重头下载文件
3、因为这次的教程只有实现了同时下载几个文件,所以下显示下载任务的item都在同一屏,因此不会涉及到item的复用,但是如果多下载几个文件,要用到第二屏,就会因为item的复用出现按钮响应以及进度条显示的错乱
多线程断点续传涉及的源码(有做修改):
http://download.csdn.net/download/qq_22804827/9431070
相关文章推荐
- 与ios相比,android为什么越用越卡
- LinearLayout的weight(权值)详解
- android:Activity启动模式之singleTask(二)
- Android中实现监听ScrollView滑动事件
- android开发中经常看到@Override是什么意思呢?
- android 资源
- Android getTopActivity的方法
- 访问者模式--Android源码设计模式笔记
- Android——Animator笔记:属性动画
- 阅读《Android 从入门到精通》(25)——标签切换
- Android权限
- Android高级控件(二)——SurfaceView实现GIF动画架包,播放GIF动画,自己实现功能的初体现
- Android高级控件(二)——SurfaceView实现GIF动画架包,播放GIF动画,自己实现功能的初体现
- Android多国语言文件夹命名
- Android百度地图自定义公交路线导航
- Android开发学习之路--UI之简单聊天界面
- Android事件传递机制【Touch事件】
- android Fragment(Android官方文档中文版)
- GitHub Android 最火开源项目Top20
- Android云测试