读《50 Android Hacks》笔记整理Hack 35~Hack 40

第九章 可复用的代码片段

Hack 35 同时发起多个Intent

Intent系统是Android提供的优秀特性之一。如果开发者想与另一个应用程序共享信息,就可以使用Intent;如果开发者想打开一个链接也可以使用Intent。在Android中,几乎所有操作都可以通过Intent完成。
如果我们想要实现一个向设置头像都那个功能,可以从相册或直接拍照选择,也可以使用Intent实现,但这个无法只使用一个Intent来实现。

35.1 拍照

使用照相机应用程序拍摄照片所需的Intent代码如下:

Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Intent chooserIntent = Intent.createChooser(takePhotoIntent,getString(R.string.activity_main_pick_picture));
startActivityForResult(chooserIntent,TAKE_PICTURE);

35.2 从相册中选择照片

从相册应用程序中选择照片所需Intent代码如下:

Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickIntent.setType("image/*");
Intent chooserIntent = Intent.createChooser(pickIntent,getString(R.string.activity_main_take_picture));
startActivityForResult(chooserIntent,PICK_PICTURE);

35.3 整合两种Intent

从Android API level 5开始,开发者可以创建一个选择式Intent,然后向该Intent中添加额外初始化Intent(Extra Initial Intent),着意味着不必局限于使用一种类型的Intent,开发者还可以同时使用多种Intent。
使用方法如下:

//创建选择图片的Intent
Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickIntent.setType("image/*");
//创建拍照Intent
Intent takePhotoIntent;
takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

Intent chooserIntent = Intent.createChooser(pickIntent,getString(R.string.activity_main_pick_both));
//将拍照Intent设置为额外初始化Intent
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,new Intent[]{takePhotoIntent});

startActivityForResult(chooserIntent,PICK_OR_TAKE_PICTURE);

上述代码会把处理两种Intent的所有应用程序都显示出来。这里要记住我们还需要在当前Activity中重写onActivityResult()方法,用于进行处理使用。

外链地址1
外链地址2
外链地址3


Hack 36 在用户反馈中收集信息

关注用户反馈(feedback)是确保应用程序成功有效的方式之一。用户反馈可以突出用户最喜欢应用程序的哪些部分,用户反馈还可能要求开发一些新特性以改进应用程序。
这里我们会展示如何在反馈邮件中附加用户设备信息。
需要创建两个类:一个用于收集用户信息,另一个用于提供发送反馈邮件的Intent。
EnvironmentInfoUtil.java代码如下:

public class EnvironmentInfoUtil{
//便于获取所有可用信息的方法
public static String getApplicationInfo(Context context){
    return String.format("%s\n%s\n%s\n%s\n%s\n%s\n",getCountry(context),getBrandInfo(),getModelInfo(),getDeviceInfo(),getVersionInfo(context),getLocale(context));
}
//使用TelephonyManager确定用户所在国家
public static String getCountry(Context context){
    TelephonyManager mTelephonyMgr = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
    return String.format("Country:%s",mTelephonyMgr.getNetworkCountryIso());
}
//从Build类中获取信息
public static String getModelInfo(){
    return String.format("Model:%s",Build.MODEL);
}
...
//使用Context获取用户本地化信息
public static String getLocale(Context context){
    return String.format("Locale:%s",context.getResources().getConfiguration().locale.getDisplayName());
}
}

下面我们通过邮件发送这些信息,LaunchEmailUtil类代码如下:

public class LaunchEmailUtil{
//从Activity中调用该方法
public static void launchEmailToIntent(Context context){
    Intent msg = new Intent(Intent.ACTION_SEND);
    StringBuilder body = new StringBuilder("\n\n------\n");
    body.append(EnvironmentInfoUtil.getApplicationInfo(context));
    //设置收信人
    msg.putExtra(Intent.EXTRA_EMAIL,context.getString(R.string.mail_support_feedback_to).split(","));
    msg.putExtra(Intent.EXTRA_EMAIL,context.getString(R.string.mail_support_feedback_subject));
    //使用EnvironmentInfoUtil提供的信息设置邮件正文
    msg.putExtra(Intent.EXTRA_TEXT,body.toString());
    msg.setType("message/rfc822");
    //设置选择器的标题
    context.startActivity(Intent.createChooser(msg,context.getString(R.string.pref_sendemail_title)));
}
}

我们可以在一个Activity中调用该类提供的launchEmailToIntent()方法。该方法逻辑是:在strings.xml文件中定义向谁发送电子邮件,并提供邮件正文。为了防备用户安装不止一个可以发送邮件的应用程序,我吗创建一个应用程序选择器并为该选择器指定自定义标题。

外链地址1
外链地址2


Hack 37 向media ContentProvider添加MP3文件

Android用户都知道,如果想在设备上播放一首音乐,只需把音乐文件拷贝到外部存储器上。文件拷贝完成打开音乐播放器就可以看到该音乐文件,这个是怎么实现的呢?
在Android中,提供了一个名为ContentProvider的组件。ContentProvider用于向外部应用程序提供数据。我们可以在Android中找到一个与媒体相关的ContentProvider。
当用户向外部存储器拷贝媒体文件时,有一个进程正在浏览所有文件夹以搜索媒体文件,这些媒体文件会被添加到与媒体相关的ContentProvider后,所有应用程序都可以访问该文件。

37.1 使用ContentValues添加MP3文件

与其他ContentProvider一样,可以通过ContentValues添加新元素,代码如下:

//首先将文件存储到外部存储器中
MediaUtils.saveRaw(this,R.raw.loop1,LOOP1_PATH);

//完成插入媒体文件所需的所有字段
ContentValues values = new ContentValues(5);
values.put(Meadia.ARTIST,"Android");
values.put(Meadia.ALBUM,"60AH");
values.put(Meadia.TITLE,"hack037");
values.put(Meadia.MIME_TYPE,"audio/mp3");
values.put(Meadia.DATA,LOOP1_PATH);

//通过uri将所有值插入到ContentProvider中
getContentResolver().insert(Media.EXTERNAL_CONTENT_URI,values);

存在的问题:需要手动设置不少值。

37.2 使用MediaScanner添加MP3文件

这个方法可以避免手动添加值,代码如下:

//首先将文件保存到外部存储器
MediaUtils.saveRaw(this,R.raw.loop2,LOOP2_PATH);

Uri uri = Uri.parse("file://"+LOOP2_PATH);
Intent i = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
//发送广播,请求扫描并添加指定的文件
sendBroadcast(i);

外链地址1
外链地址2
外链地址3
外链地址4


Hack 38 为ActionBar添加刷新动作

ActionBar相关API是在Android 3.0版本(Honeycomb)中引入的。引入ActionBar组件的目的是为了在应用程序界面中提供一个可以定位用户的“地方“,并提供与上下文相关的动作。
我们第一步需要使用一个带ActionBar的主题。然后在Activity中的代码如下:

...
//创建新菜单
@Override
public boolean onCreateOptionsMenu(Menu menu){
    MenuItem mRefreshMenu = menu.add(MENU_REFRESH,MENU_REFRESH,MENU_REFRESH,"Refresh");
    mRefreshMenu.setIcon(R.drawable.menu_reload);
    mRefreshMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
    return true;
}

第二步我们要进行处理刷新操作,这里为了模拟后台任务,我们创建了一个AsyncTask,代码如下:

private class LoadingAsyncTask extends AsyncTask<Void,Void,Void>{
@Override
protected void onPreExecute(){
    super.onPreExecute();
    //在Task即将开始时处理ui变化
    startLoading();
}
@Override
protected void doInBackground(Void... params){
    //休眠5秒
    SystemClock.sleep(5000);
}
@Override
protected void onPostExecute(Void result){
    super.onPostExecute(result);
    //在Task即将结束时处理ui变化
    stopLoading();
}
}

在单独一个方法中启动并执行AsyncTask:

public void handleRefresh(View v){
    new LoadingAsyncTask().execute();
}

当我们操作ActionBar时,onOptionsItemSlected()方法被触发,进而会调用handleRefresh方法。
在这里我们还需要使用一个可旋转视图(这里我们使用ProgressBar)代替进度菜单项,使用ActionView。
开发文档介绍:
ActionView是在ActionBar中显示的一种控件,用于替代一个动作项按钮。例如:如果在选项菜单中有一个搜索项,开发者可以添加一个ActionView,然后使用SearchView控件替代该搜索项。
在startLoading()和stopLoading()方法处理刷新菜单项的ActionView代码如下:

private void startLoading(){
    mRefreshMenu.setActionView();
    mButton.setEnabled(false);
}
private void stopLoading(){
    mRefreshMenu.setActionView(null);
    mButton.setEnabled(true);
}

外链地址1
外链地址2


Hack 39 从Market中获取依赖功能

这里我们会使用名为Layar的应用程序。Layar是一款手机浏览器软件,该软件基于现实感增强技术(augmented reality technology),允许用户查找各种信息。开发者可以创建一种称为图层的东西(layer),该图层用于在Layar浏览器中显示关注点。需要完成的功能:
1。判断是否安装Layar的方法
2。编写代码以打开Market下载Layar
3。打开一个特定图层的Intent调用

1的代码如下:

public static boolean isLayarAvailable(Context ctx){
    PackageManager pm = ctx.getPackageManager();

    try{
        //PackageManager的getApplicationInfo()方法
        pm.getApplicationInfo("com.layar",0);
        return true;    
    }catch(PackageManager.NameNotFoundException e){
        //表示无法获取应用程序信息
        return false;
    }
}

判断是否安装了Layar。
2的代码如下:

public static AlertDialog showDownloadDialog(final Context ctx){
    AlertDialog.Builder downloadDialog = new AlertDialog.Builder(ctx);
    //创建一个AlertDialog,让用户决定是否需要从Market上下载Layar
    downloadDialog.setTitle("Layar is not available");
    downloadDialog.setMessage("Do you want to download it from the market?");
    downloadDialog.setPositiveButton("Yes",new DialogInterface.OnClickListener(){
        @Override
        public void onClick(DialogInterface dialogInterface,int i){
            //要启动Market,可以使用uri并指定Intent的action为ACTION_VIEW
            Uri uri = Uri.parse("market://details?id=com.layar");
            Intent intent = new Intent(Intent.ACTION_VIEW,uri);
            try{
                ctx.startActivity(intent);
            }catch(ActivityNotFoundException e){
                //一些Android设备上可能没有Market应用程序,这里使用try-catch块确保应用不会崩溃
            }
        }
    });
    downloadDialog.setNegativeButton("No",new DialogInterface.OnClickListener(){
        @Override
        public void onClick(DialogInterface dialogInterface,int i){}
    });
    //创建AlertDialog后,进行显示
    return downloadDialog.show();
}

使用PackageManager提供的getApplicationInfo()方法判断某个应用程序是否可用。该方法需要用到应用程序的包名。如果应用程序存在,会返回一个ApplicationInfo对象,并且以AndroidManifest.xml的标签中的内容填充该对象。如果在试图获取应用程序信息过程中得到一个NameNotFoundException异常,开发者就可以确定该应用程序不可用。
3的代码如下:

public void onLayarClick(View v){
    if(!ActivityHelper.isLayarAvailable(this)){
        //显示下载对话框的逻辑
        ActivityHelper.showDownloadDialog(this);
    }else{
        //如果Layar可用,通过其 URI显示teather图层
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_VIEW);
        Uri uri = Uri.parse("layar://teather/?action=refresh");
        intent.setData(uri);
        startActivity(intent);
    }
}

添加代码逻辑用于判断是否应该下载Layar或通过Intent启动一个图层关注点。

很多应用程序都可以提供这种类型的Intent API。使用的重要优势有两个:第一个是编码更少。第二个是如果用户已经在使用某个同类应用程序,那么这意味着用户不必在学习完成某个操作的另一种方法。

外链地址1
外链地址2
外链地址3


Hack 40 以后进先出方式加载图片

开发者经常面对的一个挑战是显示网络上的图片,这类挑战的理想解决方案需要包括以下几个方面:
1.保持响应灵敏的UI
2.在应用程序UI线程之外处理网络和磁盘I/O操作
3.对于ListView,需要支持视图回收
4.快速显示图片的缓存机制

解决这个难题的许多方案是使用内存缓存来保存之前加载的图片,并且通过一个线程池为需要加载的图片排队。但经常被忽略的特性是图片被请求加载的顺序。
例如ListView中每行都包含一个图片的情况,如果用户向下滚动(fling)列表,多数图片加载方案会根据图片所在的父视图到屏幕的顺序的顺序来加载每张图片。这样的结果就是当用户停止滚动操作,当前显示在屏幕上的行虽然是当前点最重要的行,但却被滞后加载。而开发者想要的结果是最后请求显示的行能够通过“插队”被优先处理。

40.1 起点:Android示例程序

在Android官方文档的培训章节(Training)包含一篇名为“高效显示Bitmap”的文档(在外链地址中)有多个核心概念,如将图片缩减采样(downsampling)到合适大小、使用LruCache类实现内存缓存以及在UI线程外执行工作的基本机制。

40.2 引入executor

使用AsyncTask的解决方案并不适用于处理大量图片,也不会让开发者控制任务的优先级。所以做为替代方案,我们使用java.util.concurrent包中的执行器服务(executor service)和一个优先级队列来指定请求图片的顺序。后进先出(LIFO)的实现方案包含两个类:LIFOTask和LIFOThreadPoolProcessor。
利用counter实现compareTo()方法用于排序,代码如下:

//本例的任务都是在相同线程中创建的
public class LIFOTask extends FutureTask<Object> implements Comparable<LIFOTask>{
    private static long counter = 0;
    private final long priority;
    public LIFOTask(Runnable runnable){
        super(runnable,new Object());
        priority = counter++;
    }
    public long getPriority(){
        return priority;
    }
    @Override
    public int compareTo(LIFOTask other){
        return priority > other.getPriority() ? -1 : 1;
    }
}

我们这里选择继承FutureTask类,该类会被传入Executor类,因为它暴露了一个消失方法。
创建了LIFOTask类,需要在ThreadPoolExecutor类中使用其compareTo()方法,代码如下:

public class LIFOThreadPoolProcessor{
    private BlockingQueue<Runnable> opsToRun = new PriorityBlockingQueue<Runnable>(64,new Comparator<Runnable>(){
        @Override
        public int compare(Runnable r0,Runnable r1){
            if(r0 instanceof LIFOTask && r1 instanceof LIFOTask){
                LIFOTask 10 = (LIFOTask)r0;
                LIFOTask 11 = (LIFOTask)r1;
                return 10.compareTo(11);
            }
            return 0;
        }
    });
    private ThreadPoolExecutor executor;
    public LIFOThreadPoolProcessor(int threadCount){
        executor = new ThreadPoolExecutor(threadCount,threadCount,0,TimeUnit.SECONDS,opsToRun);
    }
    public Future<?> submitTask(LIFOTask task){
        return executor.submit(task);
    }
    public void clear(){
        executor.purge();
    }
}

该类需要注意的是传入ThreadPoolExecutor构造方法参数。我们让客户端应用程序选择线程池的大小,并选用PriorityBlockingQueue作为客户端应用程序所提交任务的容器。然后,我们使用LIFOTask对象的compareTo()方法得到期望的优先级顺序。要注意在本例中keepAlive时间参数不适用于给定的核心线程池大小和最大线程池大小。

40.3 UI线程——离开返回的无缝衔接

我们在切换到UI线程需要注意几个方面:
1.如果用户滚动ListView,ImageView的实例有可能被回收
2.宿主Activity有可能在任务完成前已经被销毁
因此,用于处理图片的Runnable的每一步都需要检查是否应该停止处理图片。如果宿主Activity使用ImageWorker的setExitTasksEarly()方法设置一个标记(flag),就可以检查到停止状态,该方法需要在onPause()方法中调用。此外,如果FutureTask的cancel()方法被调用,也可以检查到停止状态。

40.4 注意事项

对于产品级应用,Android官方文档的training小节建议使用更好的磁盘缓存解决方案。

外链地址1
外链地址2
外链地址3
外链地址4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值