什么是AppWidget?AppWidget就是我们平常在桌面上见到的那种一个个的小窗口,利用这个小窗口可以给用户提供一些方便快捷的操作。本篇打算从以下几个点来介绍AppWidget:
1.如何创建一个简单的AppWidget
2.如何使得AppWidget与客户端程序交互
创建简单的AppWidget
在介绍之前给大家看一下程序运行的最后结果和项目结构图,以便大家有个整体的印象。
运行结果图:
项目结构图:
第一步:
首先在res文件夹下新建一个名字为xml的文件夹,然后在xml目录下创建一个名为appwidget01的xml文件(如上图所示)。这个appwidget01中的内容如下:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth = "294dp" android:minHeight = "72dp" android:updatePeriodMillis = "86400000" android:initialLayout = "@layout/appwidgetlayout" > </appwidget-provider>
这个xml是用来描述你所要创建的appWidget的一些描述信息的,比如高度、宽度、刷新间隔、布局文件等等。仅仅这个描述文件还不够,我们看到的appWidget可都是有界面元素的呀,比如说文本,图片,按钮等等,这些东西的定义都需要放到layout文件夹下面。这个文件就是上面代码中写到的那个appwidgetlayout。
第二步:
在layout文件夹下面新建一个appwidgetlayout.xml文件,在这个文件中描述了appWidget的控件和布局等等信息,就和我们平常创建的一个activity的布局文件没什么两样,因为只是简单的演示,所以仅用一个文本和一个按钮。xml的内容如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/txtapp" android:text="test" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ffffff"></TextView> <Button android:id="@+id/btnSend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send"></Button> </LinearLayout>
第三步:
既然appWidget中存在按钮等等控件,那么就肯定少不了处理这些控件事件的处理代码啦。这些代码被放在一个继承于AppWidgetProvider的类中,在本例子中我新建了一个AppWidget的类,该类继承于AppWidgetProvider,以后所有的AppWidget上面的控件事件都会在这个类中处理。看一下类的内容:
public class AppWidget extends AppWidgetProvider { private final String broadCastString = "com.qlf.appWidgetUpdate"; /** * 删除一个AppWidget时调用 * */ @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); } /** * 最后一个appWidget被删除时调用 * */ @Override public void onDisabled(Context context) { super.onDisabled(context); } /** * AppWidget的实例第一次被创建时调用 * */ @Override public void onEnabled(Context context) { super.onEnabled(context); } /** * 接受广播事件 * */ @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); } /** * 到达指定的更新时间或者当用户向桌面添加AppWidget时被调用 * */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { } }
各个方法的作用大家一看上面的注释就明白了。我们暂时不需要实现里面的方法。
第四步:
在AndroidManifest.xml中定义一些创建AppWidget必要的东西,先看代码:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.qlf.widget" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name="AppWidget"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE"></action> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget01" /> </receiver> </application> <uses-sdk android:minSdkVersion="8" /> </manifest>
可以看到我们在配置文件里面定义了一个receiver,他的名字是上面创建处理控件代码的那个类,下面那个intent-filter中的action是系统自带的用于更新所有appwidget的广播动作。然后meta-data标签是一个描述我们创建appwidget的元数据,那个android:name="android.appwidget.provider"是固定的,android:resource="@xml/appwidget01"指定创建的appWidget的描述信息的位置。这样程序就知道到哪里去初始化这些appWidget啦。
经过上面四个步骤,我想您已经能够成功在桌面上添加小工具了,效果就是我们最前面发出的样子。
AppWidget与程序交互
前面我们只是简单的介绍了如何创建一个appWidget,但是目前这个appWidget还没有任何的交互功能。下面我们介绍一下appWidget如何与程序进行交互。首先要介绍一个对象,这个对象在appwidget和程序的交互中很重要,他就是RemoteViews。因为appwidget运行的进程和我们创建的应用不在一个进程中,所以我们也就不能像平常引用控件那样来获得控件的实例。这个时候RemoteViews出场了,从字面上看他的意思是远程的视图,也就是说通过这个东西我们能够获得不在同一进程中的对象,这也就为我们编写appwidget的处理事件提供了帮助。我们使用一下代码来创建一个RemoteViews :
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),R.layout.appwidgetlayout); remoteViews.setOnClickPendingIntent(R.id.btnSend, pendingIntent); //为小工具上的按钮绑定事件
可以看到上面又出现了一个陌生的对象pendingIntent,这个又是用来干嘛的呢?我们知道在一般的程序中绑定按钮的点击事件是直接在实现了OnClickListener接口的类中中完成的。不过因为appwidget并不在我们应用的进程中,所以当然他也访问不到我们在应用中设置的onclick代码啦。而PendingIntent就是被用来解决这个问题的。PendingIntent可以看成是一个特殊的Intent,如果我们把Intent看成一封信,那么PendingIntent就是一封被信封包裹起来的信。这封信在remoteViews.setOnClickPendingIntent()中被“邮寄”到了appwidget, 当appwidget中的按钮单击时他知道将这封信打开,并执行里面的内容。这样就避免了直接从appwidget中执行本地代码。我们来看看PendingIntent是如何定义的:
//创建一个Intent对象 Intent intent = new Intent(); intent.setAction(broadCastString); //这一步相当于写信,说明这个信的作用到底是什么,在这里表示将发送一个广播 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
有了上面的介绍,我们在创建appwidget的交互应用时就简单不少了。我们剩下要做的工作就是在appwidget在创建的时候调用上面说到的方法为appwidget中的控件绑定事件,也就是在AppWidget类下的onUpdate方法中完成这个过程。
/** * 到达指定的更新时间或者当用户向桌面添加AppWidget时被调用 * */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { //创建一个Intent对象 Intent intent = new Intent(); intent.setAction(broadCastString); //设置pendingIntent的作用 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0); RemoteViews remoteViews = new RemoteViews(context.getPackageName(),R.layout.appwidgetlayout); //绑定事件 remoteViews.setOnClickPendingIntent(R.id.btnSend, pendingIntent); //更新Appwidget appWidgetManager.updateAppWidget(appWidgetIds, remoteViews); }
通过上面的代码我们就为button按钮绑定了一个事件,这个事件的作用是发送一个广播便于其他应用接收、更新信息。这是appwidget发送广播,那么appwidget如何接受来自其他程序发送的广播呢?这就是public void onReceive(Context context, Intent intent)的功能啦。这个方法会接收来自其他应用发出的广播,我们只要在这个程序中过滤我们需要的广播就能响应其他应用的操作来更新appwidget的信息了。要注意的是,因为appwidget运行的进程和我们创建的应用不在一个进程中的限制,所以更新的appwidget的时候也要通过远程对象来操作,具体代码如下:
/** * 接受广播事件 * */ @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(broadCastString)) { //只能通过远程对象来设置appwidget中的控件状态 RemoteViews remoteViews = new RemoteViews(context.getPackageName(),R.layout.appwidgetlayout); //通过远程对象将按钮的文字设置为”hihi” remoteViews.setTextViewText(R.id.btnSend, "hihi"); //获得appwidget管理实例,用于管理appwidget以便进行更新操作 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); //相当于获得所有本程序创建的appwidget ComponentName componentName = new ComponentName(context,AppWidget.class); //更新appwidget appWidgetManager.updateAppWidget(componentName, remoteViews); } super.onReceive(context, intent); }
总结下就是appwidget上的操作都必须借助远程对象来操作。最后看一下运行的图片吧:
按之前:
按之后:
代码下载:
请查看附件.
最近由于项目需要,研究了下百度地图定位,他们提供的实例基本都是用监听器实现自动定位的。我想实现一种效果:当用户进入UI时,不定位,用户需要定位的时候,自己手动点击按钮,再去定位当前位置。 经过2天研究和咨询,找到了,在此备忘一下。
注意:定位使用真机才能够真正定位;模拟器的话,在DDMS中的Emulator Control中,选择Manual,下面单选按钮选择Decimal,然后填写经纬度,send后,再点击定位我的位置按钮,就能定位了(这应该算是固定定位,哈哈。。。)、
1、第一步当然是获取一个针对自己项目的key值。http://dev.baidu.com/wiki/static/imap/key/
2、使用百度API是有前提的,摘自百度:首先将API包括的两个文件baidumapapi.jar和libBMapApiEngine.so拷贝到工程根目录及libs\armeabi目录下,并在工程属性->Java Build Path->Libraries中选择“Add JARs”,选定baidumapapi.jar,确定后返回,这样您就可以在您的程序中使用API了。(这两个文件见附件)。
3、按照自己的需求写一个layout,我的如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/myLocation_id" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="15dp" android:gravity="center_horizontal" android:textColor="@drawable/black" android:background="@drawable/gary" /> <com.baidu.mapapi.MapView android:id="@+id/bmapsView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:clickable="true" android:layout_weight="1" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/location_button_id" android:text="@string/location_button_text" /> </LinearLayout>
需要特别注意的是:<com.baidu.mapapi.MapView /> 这玩意。
4、写一个MapApplication实现application,提供全局的BMapManager,以及其初始化。
public BMapManager mapManager = null; static MapApplication app; public String mStrKey = "你申请的key值"; @Override public void onCreate() { mapManager = new BMapManager(this); mapManager.init(mStrKey, new MyGeneralListener()); } @Override //建议在您app的退出之前调用mapadpi的destroy()函数,避免重复初始化带来的时间消耗 public void onTerminate() { // TODO Auto-generated method stub if(mapManager != null) { mapManager.destroy(); mapManager = null; } super.onTerminate(); } static class MyGeneralListener implements MKGeneralListener{ @Override public void onGetNetworkState(int arg0) { Toast.makeText(MapApplication.app.getApplicationContext(), "您的网络出错啦!", Toast.LENGTH_LONG).show(); } @Override public void onGetPermissionState(int iError) { if (iError == MKEvent.ERROR_PERMISSION_DENIED) { // 授权Key错误: Toast.makeText(MapApplication.app.getApplicationContext(),"您的授权Key不正确!", Toast.LENGTH_LONG).show(); } } } 5、接下来就是按照百度api写定位代码了,使用handler机制去添加定位图层,需要说明的都在注释上了。 private BMapManager mBMapMan = null; private MapView mMapView = null; private MapController bMapController; private MKLocationManager mkLocationManager; private MKSearch mkSearch; private TextView address_view; //定位到的位置信息 private ProgressDialog dialog; private List<HotelInfo> hotelList; private int distance = 1000; //查询的范围(单位:m) Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { double lat = msg.getData().getDouble("lat"); double lon = msg.getData().getDouble("lon"); if(lat!=0&&lon!=0){ GeoPoint point = new GeoPoint( (int) (lat * 1E6), (int) (lon * 1E6)); bMapController.animateTo(point); //设置地图中心点 bMapController.setZoom(15); mkSearch.reverseGeocode(point); //解析地址(异步方法) MyLocationOverlay myLoc = new MyLocationOverlayFromMap(ShowMapAct.this,mMapView); myLoc.enableMyLocation(); // 启用定位 myLoc.enableCompass(); // 启用指南针 mMapView.getOverlays().add(myLoc); }else{ Toast.makeText(ShowMapAct.this, "没有加载到您的位置", Toast.LENGTH_LONG).show(); } if(hotelList!=null){ Drawable marker = getResources().getDrawable(R.drawable.iconmarka); //设置marker marker.setBounds(0, 0, marker.getIntrinsicWidth(), marker.getIntrinsicHeight()); //为maker定义位置和边界 mMapView.getOverlays().add(new OverItemList(marker,hotelList,ShowMapAct.this,bMapController)); }else if(hotelList==null&&lat!=0&&lon!=0){ Toast.makeText(ShowMapAct.this, "网络异常,没有获取到酒店信息。", Toast.LENGTH_LONG).show(); } if(dialog!=null) dialog.dismiss(); } }; @Override protected void onCreate(Bundle savedInstanceState) { distance = getIntent().getExtras().getInt("distance"); //获取查询范围 super.onCreate(savedInstanceState); setContentView(R.layout.location); mMapView = (MapView)findViewById(R.id.bmapsView); //初始化一个mapView 存放Map init(); //初始化地图管理器 super.initMapActivity(mBMapMan); address_view = (TextView)findViewById(R.id.myLocation_id); SpannableStringBuilder style = new SpannableStringBuilder(String.format(getResources().getString(R.string.location_text),"位置不详")); style.setSpan(new ForegroundColorSpan(Color.RED), 5, style.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); address_view.setText(style); Button location_button = (Button)findViewById(R.id.location_button_id); location_button.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { dialog = ProgressDialog.show(ShowMapAct.this, "", "数据加载中,请稍后....."); new Thread(new MyThread()).start(); } }); mkSearch = new MKSearch(); //初始化一个MKSearch,根据location解析详细地址 mkSearch.init(mBMapMan, this); mMapView.setBuiltInZoomControls(true); //启用内置的缩放控件 bMapController = mMapView.getController(); GeoPoint defaultPoint = new GeoPoint((int) (39.920934 * 1E6),(int) (116.412817 * 1E6)); //用给定的经纬度构造一个GeoPoint,单位是微度 (度 * 1E6) bMapController.setCenter(defaultPoint); //设置地图中心点 bMapController.setZoom(12); //设置地图zoom级别 mkLocationManager = mBMapMan.getLocationManager(); } /** * 初始化地图管理器BMapManager */ public void init(){ MapApplication app = (MapApplication)getApplication(); if (app.mapManager == null) { app.mapManager = new BMapManager(getApplication()); app.mapManager.init(app.mStrKey, new MapApplication.MyGeneralListener()); } mBMapMan = app.mapManager; } @Override protected void onDestroy() { MapApplication app = (MapApplication)getApplication(); if (mBMapMan != null) { mBMapMan.destroy(); app.mapManager.destroy(); app.mapManager = null; mBMapMan = null; } super.onDestroy(); } @Override protected void onPause() { if (mBMapMan != null) { // 终止百度地图API mBMapMan.stop(); } super.onPause(); } @Override protected void onResume() { if (mBMapMan != null) { // 开启百度地图API mBMapMan.start(); } super.onResume(); } @Override protected boolean isRouteDisplayed() { return false; } @Override public void onGetAddrResult(MKAddrInfo result, int iError) { if(result==null) return; SpannableStringBuilder style = new SpannableStringBuilder(String.format(getResources().getString(R.string.location_text),result.strAddr)); style.setSpan(new ForegroundColorSpan(Color.RED), 5, style.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); address_view.setText(style); if(dialog!=null) dialog.dismiss(); } @Override public void onGetDrivingRouteResult(MKDrivingRouteResult arg0, int arg1) {} @Override public void onGetPoiResult(MKPoiResult arg0, int arg1, int arg2) {} @Override public void onGetTransitRouteResult(MKTransitRouteResult arg0, int arg1) {} @Override public void onGetWalkingRouteResult(MKWalkingRouteResult arg0, int arg1) {} /** * 重新定位,加载数据 * @author Administrator * */ class MyThread implements Runnable{ @Override public void run() { /** * 最重要的就是这个玩意 * 由于LocationListener获取第一个位置修正的时间会很长,为了避免用户等待, * 在LocationListener获取第一个更精确的位置之前,应当使用getLocationInfo() 获取一个缓存的位置 */ Location location = mkLocationManager.getLocationInfo(); double lat = 0d,lon = 0d; if(location!=null){ //定位到位置 String coordinate = location.getLatitude()+","+location.getLongitude(); HotelRemoteData hotelData = new HotelRemoteData(); /** * 远程获取酒店列表数据 */ hotelList = hotelData.getHotelToMap(coordinate,distance); lat = location.getLatitude(); lon = location.getLongitude(); } Message msg = new Message(); Bundle data = new Bundle(); data.putDouble("lat", lat); data.putDouble("lon", lon); msg.setData(data); handler.sendMessage(msg); } }
6、还有一种就是百度示例相当推荐的,也是加载定位位置速度比较快的,那就是通过定位监听器来定位信息。没啥难的,照着百度的示例写,都能搞定。
LocationListener listener = new LocationListener() { @Override /** 位置变化,百度地图即会调用该方法去获取位置信息。 * (我测试发现就算手机不动,它也会偶尔重新去加载位置;只要你通过重力感应,他就一定会重新加载) */ public void onLocationChanged(Location location) { GeoPoint gp = new GeoPoint((int) (location.getLatitude() * 1E6), (int) (location.getLongitude() * 1E6)); //通过地图上的经纬度转换为地图上的坐标点 bMapController.animateTo(gp); //动画般的移动到定位的位置 } };
今天折腾了好一阵子,终于明白怎么通过反射来构造内部类对象了。这里头名堂不少,只能一一道来。
看完觉得方法才是最重要的
首先在 javalang 包下写一个包含内部类的类:
package javalang;
public class Outer {
public static class Inner1{}
}
注意这个类是 public static,后面我们慢慢把这些修饰符去掉。
要想通过反射来创建 Inner1 对象,首先要获得 Inner1 的 Class 对象。我们在 Outer 中写上 main 方法:
public class Outer {
public static class Inner1{}
public static void main(String[] args) {
System.out.println(Inner1.class);
}
}
输出结果:class javalang.Outer$Inner1
然后我们试一下这个类名对不对:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1"));
}
运行一下,没错。然后就是用它来创建对象。创建对象要靠构造方法。这个类有没有构造方法呢?我们可以这么写:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1").getConstructors().length);
}
运行一下,输出 1。看来有。然后看看这个构造方法是什么样子的:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1").getConstructors()[0]);
}
输出结果:public javalang.Outer$Inner1()。这就是缺省构造方法嘛。所以我们可以这样写:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1")
.getConstructors()[0].newInstance();
}
输出结果:javalang.Outer$Inner1@ca0b6。这说明执行成功了。
接下来我们把 Inner 的 public 关键字去掉,然后再运行。结果报错了:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
这说明没有找到构造方法。真的没有吗?我们把 main 方法改回来:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1")
.getConstructors().length);
}
输出结果:0。真的没有构造方法吗?其实不是,只是构造方法不是公开的。这时我们必须用 getDeclaredConstructors() 来获得:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1")
.getDeclaredConstructors().length);
}
输出结果:1。这就把构造方法找到了。然后我们继续调用这个构造方法:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1")
.getDeclaredConstructors()[0].newInstance());
}
输出结果:javalang.Outer$Inner1@ca0b6。现在我们可以用反射来构造非公开内部类的对象了。
接下来,我们再把 static 关键字去掉。这时候报错了:
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of arguments
这说明什么呢?我们调用的时候没有传参数,而错误内容就是说参数数量不正确。那么这个构造方法有什么参数呢?我们改一下代码看看:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1")
.getDeclaredConstructors()[0]);
}
输出结果:javalang.Outer$Inner1(javalang.Outer)
原来构造方法里面需要一个 Outer 类型的参数。这好办:
public static void main(String[] args) throws Exception {
System.out.println(Class.forName("javalang.Outer$Inner1")
.getDeclaredConstructors()[0].newInstance(new Outer()));
}
输出结果:javalang.Outer$Inner1@ca0b6
OK,原来如此。看来非静态的内部类没有缺省的构造方法,构造时需要传一个外部类的实例作为参数。