Android Full App,第8部分:为主屏幕创建AppWidget

这是“ Android完整应用程序教程”系列的八个部分。 完整的应用程序旨在提供一种通过互联网搜索表演电影/演员的简便方法。 在本系列的第一部分( “主要活动UI” )中,我们创建了Eclipse项目并为应用程序的主要活动设置了基本界面。 在第二部分( “使用HTTP API” )中,我们使用Apache HTTP客户端库来使用外部HTTP API并将API的搜索功能集成到我们的应用程序中。 在第三部分( “解析XML响应” )中,我们看到了如何使用Android内置的XML解析功能来解析XML响应。 在第四部分( “从主要活动异步执行API请求” )中,我们将HTTP检索器和XML解析器服务绑定在一起,以便从应用程序的主要活动中执行API搜索请求。 为了避免阻塞主UI线程,该请求是在后台线程中异步执行的。 在第五部分( “使用意图启动新活动” )中,我们了解了如何启动新的活动以及如何将数据从一个活动转移到另一个活动 。 在第六部分( “用于数据表示的自定义列表视图” )中,我们创建了一个自定义列表视图,以提供更好的数据可视化表示。 在第七部分( “使用选项菜单和自定义对话框进行用户交互” )中,我们创建了选项菜单和自定义对话框,以促进更好的用户交互。 在这一部分中,我们将为用户主屏幕创建一个AppWidget,并通过它提供与应用程序相关的更新。

从1.5版开始,Android SDK包含AppWidget框架,该框架允许开发人员编写“小部件”,人们可以将其拖放到主屏幕上并与之交互。 窗口小部件的使用非常方便,因为它允许用户将自己喜欢的应用程序添加到主屏幕中,并与它们进行快速交互,而不必启动整个应用程序。 在继续之前,我建议您看一看发布在Android官方开发人员博客上的文章“介绍主屏幕小部件和AppWidget框架” 。 下图显示了一个小部件的示例。 它是为本文目的而构建的,它提供了“每日新闻”的更新。 可以在此处找到其源代码。

对于我们的应用程序,我们将创建一个小部件,该小部件定期提供有关TMDb数据库中创建的最新电影的更新。 正如我过去提到的那样,我们一直在使用非常酷的TMDb API进行电影/演员搜索以及其他各种相关功能。

创建应用程序小部件的第一步是为其提供声明以及一些描述它的XML元数据。 这是通过在项目的“ res / xml”文件夹中添加一个特殊的XML文件来完成的。 通过该文件,我们提供有关小部件的尺寸,其更新间隔等信息。相应的类名为AppWidgetProviderInfo ,其字段对应于我们将在下面看到的XML标记中的字段。 对于小部件的高度和宽度,Google建议使用特定的公式:

浸入的最小大小=(单元数* 74dip)– 2dip

由于我们希望小部件在宽度上占据2个单元格,在高度上占据1个单元格,因此dip的大小分别为146和72。 更新间隔以毫秒为单位定义,出于演示目的,我们仅使用10秒(10000毫秒)。 但是请注意,不建议间隔太短。 更具体地说,比每小时更频繁地更新会很快耗尽电池和带宽。

更新:出于避免电池电量耗尽的原因,Google在1.6版之后更改了其API,以使刷新速度不能少于30分钟。 如果您希望获得更频繁的更新,则必须使用警报机制才能将意图广播发送到小部件接收器。 您可以在“ 使用Alarm Manager的App Widget ”一文中找到这种方法的示例。

最后但并非最不重要的一点是,必须使用布局,以便我们能够处理小部件的呈现方式。 这将通过引用另一个名为“ widget_layout.xml”的XML文件来完成。

XML小部件声明文件名为“ movie_search_widget.xml”,它是以下内容:

   < appwidget-provider xmlns:android = " http://schemas.android.com/apk/res/android " 
     android:minWidth = "146dip" 
     android:minHeight = "72dip" 
     android:updatePeriodMillis = "10000" 
     android:initialLayout = "@layout/widget_layout"  /> 

现在让我们看看它的布局描述(“ /res/layout/widget_layout.xml”)是什么样的:

   < RelativeLayout xmlns:android = " http://schemas.android.com/apk/res/android " 
     android:id = "@+id/widget" 
     android:layout_width = "fill_parent" 
     android:layout_height = "wrap_content" 
     android:focusable = "true" 
     style = "@style/WidgetBackground" >     
     < TextView 
         android:id = "@+id/app_name" 
         android:layout_width = "wrap_content" 
         android:layout_height = "wrap_content" 
         android:layout_marginTop = "14dip" 
         android:layout_marginBottom = "1dip" 
         android:includeFontPadding = "false" 
         android:singleLine = "true" 
         android:ellipsize = "end" 
         style = "@style/Text.WordTitle" />     
     < TextView 
         android:id = "@+id/movie_name" 
         android:layout_width = "fill_parent" 
         android:layout_height = "wrap_content" 
         android:layout_below = "@id/app_name" 
         android:paddingRight = "5dip" 
         android:paddingBottom = "4dip" 
         android:includeFontPadding = "false" 
         android:lineSpacingMultiplier = "0.9" 
         android:maxLines = "4" 
         android:fadingEdge = "vertical" 
         style = "@style/Text.Movie" />       

布局非常简单。 我们正在使用RelativeLayout ,其中子对象的位置可以相对于彼此或相对于父对象以及几个TextView进行描述 。 请注意,实际上将使用的样式定义在另一个文件(“ res / values / styles.xml”)中,以便将所有与样式相关的属性收集在一个位置。 样式声明如下:

   < resources > 
     < style name = "WidgetBackground" > 
         < item name = "android:background" >@drawable/widget_bg 
          
     < style name = "Text" > 
          
     < style name = "Text.Title" > 
         < item name = "android:textSize" >16sp 
         < item name = "android:textStyle" >bold 
         < item name = "android:textColor" >@android:color/black 
          
     < style name = "Text.Movie" > 
         < item name = "android:textSize" >13sp 
         < item name = "android:textColor" >@android:color/black 
             

下一步是在AndroidManifest.xml文件中注册特殊的BroadcastReceiver 。 该接收器将处理时间到来时系统触发的所有应用程序小部件更新。 让我们看一下相应的清单文件片段:

 < application android:icon = "@drawable/icon" android:label = "@string/app_name" >  ... 
          
         < receiver android:name = ".widget.MovieSearchWidget" android:label = "@string/widget_name" > 
             < intent-filter > 
                 < action android:name = "android.appwidget.action.APPWIDGET_UPDATE" /> 
              
             < meta-data android:name = "android.appwidget.provider" android:resource = "@xml/movie_search_widget" /> 
                  
          
         < service android:name = ".widget.MovieSearchWidget$UpdateService" />  ...   

接收器类是“ com.javacodegeeks.android.apps.movi​​esearchapp.widget.MovieSearchWidget”,该类实际上将使用内部服务类(“ UpdateService”)来执行更新。 请注意,该接收者处理的动作属于APPWIDGET_UPDATE ,应在更新AppWidget时发送。 在元数据部分中,定义了小部件声明XML文件位置。

现在,让我们编写将接收AppWidget请求并提供应用程序更新的类。 为此,我们扩展了AppWidgetProvider类,而该类又扩展了BroadcastReceiver类。 实际上,AppWidgetProvider只是一个便利类,可以帮助实现AppWidget提供者,并且其全部功能可以通过常规的BroadcastReceiver来实现。

为了处理各种操作请求,可以重写五种基本方法:

  • onEnabled :在创建第一个应用程序小部件时调用。 如果有的话,全局初始化应该在这里进行。
  • onDisabled :在删除由该定义处理的最后一个App Widget时调用。 全局清理应在此处进行(如果有)。
  • onUpdate :在应用窗口小部件需要更新其视图时调用,可以在用户首次创建窗口小部件时进行。 这是最常用的方法。
  • onDeleted :在删除此应用小部件的一个或多个实例时调用。 特定实例的清理应在此处进行。
  • onReceive :处理BroadcastReceiver动作并将请求分派到上述方法。

这是我们的实现:

 package com.javacodegeeks.android.apps.moviesearchapp.widget;  import android.app.PendingIntent;  import android.app.Service;  import android.appwidget.AppWidgetManager;  import android.appwidget.AppWidgetProvider;  import android.content.ComponentName;  import android.content.Context;  import android.content.Intent;  import android.net.Uri;  import android.os.IBinder;  import android.widget.RemoteViews;  import com.javacodegeeks.android.apps.moviesearchapp.R;  import com.javacodegeeks.android.apps.moviesearchapp.model.Movie;  import com.javacodegeeks.android.apps.moviesearchapp.services.MovieSeeker;  public class MovieSearchWidget extends AppWidgetProvider {    
    private static final String IMDB_BASE_URL = " http://m.imdb.com/title/ " ;    
    @Override 
     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int [] appWidgetIds) { 
         // To prevent any ANR timeouts, we perform the update in a service 
         context.startService( new Intent(context, UpdateService. class )); 
     } 
     public static class UpdateService extends Service {        
        private MovieSeeker movieSeeker = new MovieSeeker();        
         @Override 
         public void onStart(Intent intent, int startId) { 
             // Build the widget update for today 
             RemoteViews updateViews = buildUpdate( this ); 
             // Push update for this widget to the home screen 
             ComponentName thisWidget = new ComponentName( this , MovieSearchWidget. class ); 
             AppWidgetManager manager = AppWidgetManager.getInstance( this ); 
             manager.updateAppWidget(thisWidget, updateViews); 
         }         
       public RemoteViews buildUpdate(Context context) {            
          Movie movie = movieSeeker.findLatest();          
          String imdbUrl = IMDB_BASE_URL + movie.imdbId; 
          // Build an update that holds the updated widget contents 
          RemoteViews updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout); 
          updateViews.setTextViewText(R.id.app_name, getString(R.string.app_name)); 
          updateViews.setTextViewText(R.id.movie_name, movie.name);          
          Intent intent = new Intent(); 
          intent.setAction( "android.intent.action.VIEW" ); 
          intent.addCategory( "android.intent.category.BROWSABLE" ); 
          intent.setData(Uri.parse(imdbUrl));          
          PendingIntent pendingIntent = 
             PendingIntent.getActivity(context, 0 , intent, PendingIntent.FLAG_UPDATE_CURRENT);            
          updateViews.setOnClickPendingIntent(R.id.movie_name, pendingIntent);          
          return updateViews; 
       } 
         @Override 
         public IBinder onBind(Intent intent) { 
             // We don't need to bind to this service 
             return null ; 
         }         
     }  } 

如前所述,我们重写了onUpdate方法,在其中,我们只是启动了一个将实际提供更新的新服务 。 这样做是为了在另一个线程中执行耗时的操作(打开网络连接,下载数据等),从而避免了任何ANR超时 。 在我们的服务中,我们实现了onStart方法。 请注意,现在不建议使用此方法,应该使用onStartCommand 。 由于我使用的是Android 1.5 SDK,因此我将坚持使用onStart方法。

对于我们的应用程序,我使用了MovieSeeker类中的新方法,该方法已经过增强,以便提供最新的电影(我们将在以后看到)。 接下来,我们构造一个RemoteViews对象,该对象将描述将在另一个进程(即主屏幕)中显示的视图。 在其构造函数中 ,我们提供程序包名称(由相关Context获取 )和视图使用的布局的ID(在我们的示例中为“ widget_layout”)。 请注意,我们无法直接操作布局的任何子视图,例如,通过设置视图的文本或注册侦听器。 因此,我们将使用setTextViewText方法来提供TextView的文本,并使用setOnClickPendingIntent方法来为click事件提供处理程序。

每当用户单击小部件时,我们都希望启动浏览器并指向电影的IMDB页面。 为了实现这一目标,我们首先创建一个动作Intent of ACTION_VIEW ,并将页面的URL作为数据。 然后将该意图封装在PendingIntent内,并且将其用作单击处理程序的未决意图。

当RemoteViews对象准备就绪时,我们将创建一个ComponentName对象,该对象用作我们的BroadcastReceiver的标识符。 然后,我们引用AppWidgetManager,并使用它通过updateAppWidget方法将更新推送到主屏幕应用程序小部件。

在启动该应用程序之前,让我们看看如何检索最新的电影。 回想一下,MovieSeeker类用于电影搜索。 我们向该类添加了一个名为“ findLatest”的方法,该方法使用TMDb API的Movie.getLatest API调用。 由于该调用的响应与现有电影搜索响应的格式不匹配,因此我们必须创建一个附加的XML处理程序,称为“ SingleMovieHandler”。 您可以在本文结尾处找到可下载的项目中的全部更改。

现在启动相应的Eclipse项目配置。 仿真器可能会带您直接进入应用程序本身。 而是单击“后退”按钮以返回到主屏幕。 查看有关如何添加和删除应用程序小部件的文章 。 对于仿真器,您基本上必须单击空白区域而不释放鼠标按钮。 然后,将弹出以下对话框:

选择“小部件”,然后选择“ MovieSearchAppWidget”:

当小部件插入主屏幕时,将执行相关的更新方法,并获取最新的电影。 这是主屏幕的外观:

最后,如果您双击电影的名称,则会触发待处理的意图,并且Android浏览器将启动并指向电影IMDB页面(请注意,某些电影将没有关联的IMDB ID和页面。在这种情况下,浏览器将带您进入损坏的IMDB页面)。

而已。 您的应用程序的AppWidget。

相关文章 :
  • “ Android完整应用程序教程”系列
  • Android文字转语音应用
  • 带有Yahoo API的Android反向地理编码– PlaceFinder
  • Android基于位置的服务应用程序– GPS位置
  • 使用VirtualBox在PC上安装Android OS
  • 拥抱Android的强大功能:快速概述

翻译自: https://www.javacodegeeks.com/2010/12/android-full-app-part-8-appwidget-for.html

你可能感兴趣的:(Android Full App,第8部分:为主屏幕创建AppWidget)