标签:code 部件 tty twitter getc out data 发送 height
转载请注明出处:http://blog.csdn.net/ahence/article/details/62418926
RemoteViews乍一看名字似乎也是一种View,实则不然,它并不是View。来看RemoteViews的定义及官方说明:
/**
* A class that describes a view hierarchy that can be displayed in
* another process. The hierarchy is inflated from a layout resource
* file, and this class provides some basic operations for modifying
* the content of the inflated hierarchy.
*/
public class RemoteViews implements Parcelable, Filter {
……
}
我们可以得到以下几点结论:
现在我们对RemoteViews应该有了一个大概的认识,它可以跨进程来显示和更新视图。RemoteViews主要有两个应用场景:
本文最后会给出RemoteViews的应用实例,接下来我们先分析RemoteViews的实现原理。
RemoteViews提供了多个构造函数,如:
public RemoteViews(String packageName, int layoutId) {}
public RemoteViews(String packageName, int userId, int layoutId) {}
public RemoteViews(RemoteViews landscape, RemoteViews portrait) {}
public RemoteViews(Parcel parcel) {}
以一个最常用的构造方法为例:
/**
* Create a new RemoteViews object that will display the views contained
* in the specified layout file.
*
* @param packageName Name of the package that contains the layout resource
* @param layoutId The id of the layout resource
*/
public RemoteViews(String packageName, int layoutId) {
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
由注释可知,第一个参数为包名,第二个参数为布局资源文件的ID,接下来会调用:
/**
* Create a new RemoteViews object that will display the views contained
* in the specified layout file.
*
* @param application The application whose content is shown by the views.
* @param layoutId The id of the layout resource.
*
* @hide
*/
protected RemoteViews(ApplicationInfo application, int layoutId) {
mApplication = application;
mLayoutId = layoutId;
mBitmapCache = new BitmapCache();
// setup the memory usage statistics
mMemoryUsageCounter = new MemoryUsageCounter();
recalculateMemoryUsage();
}
同样也很简单,第一个参数为远程视图展示内容所属的Application信息,第二个参数为布局文件ID。该构造方法主要是初始化mApplication
与mLayoutId
,其他代码为图片缓存及内存计算的一些逻辑。
RemoteView有如下几个比较重要的属性字段:
/**
* Application that hosts the remote views.
*
* @hide
*/
private ApplicationInfo mApplication;
/**
* The resource ID of the layout file. (Added to the parcel)
*/
private final int mLayoutId;
/**
* An array of actions to perform on the view tree once it has been
* inflated
*/
private ArrayList<Action> mActions;
前两个比较好理解,而且上文提到是在构造函数中赋值的。mActions
是用来存储Action
的一个列表,而Action
可以理解为对远程视图操作的一个封装,下文会详细解释。
在RemoteViews源码中声明了如下注解:
/**
* This annotation indicates that a subclass of View is alllowed to be used
* with the {@link RemoteViews} mechanism.
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface RemoteView {
}
从注解类型来看为运行时注解,作用于类或接口,结合注释可知此注解用于View的子类,用来标识该View是否可以作为远程视图使用。由此我们也可以推断,并非所有View都可以作为远程视图,只有声明了RemoteView注解的View才可以。我们从源码定义来简单验证一下:
TextView的定义
@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {}
Button的定义
@RemoteView
public class Button extends TextView {}
ImageView的定义
@RemoteView
public class ImageView extends View {}
ProgressBar的定义
@RemoteView
public class ProgressBar extends View {}
LinearLayout的定义
@RemoteView
public class LinearLayout extends ViewGroup {}
EditText的定义
public class EditText extends TextView {}
不再一一列举,可见EditText
虽然是继承自TextView
的,但它没有使用@RemoteView
注解,因此并不能用作远程视图。
RemoteViews所支持的View类型如下:
LinearLayout、RelativeLayout、FrameLayout、GridLayout、AbsoluteLayout(已弃用)
TextView、Button、ImageView、ImageButton、Chronometer、ProgressBar、ListView、GridView、StackView、ViewFlipper、AdapterViewFlipper、ViewStub、AnalogClock(已弃用)
也就是说远程视图只能使用上述所列举的View,它们的子类及其他View都是不支持的,如果使用了不支持的View,则会报异常。
Parcelable比较容易理解,就是支持序列化以便于跨进程操作。
那么Filter的作用是什么呢?Filter接口的定义如下:
/**
* Hook to allow clients of the LayoutInflater to restrict the set of Views that are allowed
* to be inflated.
*
*/
public interface Filter {
/**
* Hook to allow clients of the LayoutInflater to restrict the set of Views
* that are allowed to be inflated.
*
* @param clazz The class object for the View that is about to be inflated
*
* @return True if this class is allowed to be inflated, or false otherwise
*/
@SuppressWarnings("unchecked")
boolean onLoadClass(Class clazz);
}
从注释中不难看出Filter是用来限制和过滤View用的,上文提到并非所有的View都能用作远程视图,如果为上述列举的View,则onLoadClass(Class clazz)
返回true,否则返回false。
在RemoteViews中实现了Filter接口的方法:
public boolean onLoadClass(Class clazz) {
return clazz.isAnnotationPresent(RemoteView.class);
}
可以看到就是根据@RemoteView
注解来判断是否可以使用该View作为远程视图。
很显然我们的应用自身是一个进程,那么另一个进程是什么呢?在RemoteViews的应用场景中,如自定义通知栏和桌面小部件,它们都运行在系统进程中,即SystemServer进程。如此一来,远程视图运行在SystemServer进程中,我们在自己的应用进程中跨进程来操作远程视图。前文说到RemoteViews实现了Parcelable接口,那么RemoteViews便可以从应用进程传输到系统进程了。
通常情况下,我们使用LayoutInflater加载布局只需要知道布局ID即可。还记得前文讲RemoteViews的构造函数时,有两个重要的字段:
在系统进程中加载远程视图正是利用了上述两个字段。在RemoteViews的源码中加载布局的逻辑如下:
/**
* Inflates the view hierarchy represented by this object and applies
* all of the actions.
*
* <p><strong>Caller beware: this may throw</strong>
*
* @param context Default context to use
* @param parent Parent that the resulting view hierarchy will be attached to. This method
* does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
* @return The inflated view hierarchy
*/
public View apply(Context context, ViewGroup parent) {
return apply(context, parent, null);
}
接下来会调用:
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result = inflateView(context, rvToApply, parent);
loadTransitionOverride(context, handler);
rvToApply.performApply(result, parent, handler);
return result;
}
先重点关注下面这一行代码:
View result = inflateView(context, rvToApply, parent);
其内部实现就是常见的布局加载方式了:
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
// RemoteViews may be built by an application installed in another
// user. So build a context that loads resources from that user but
// still returns the current users userId so settings like data / time formats
// are loaded without requiring cross user persmissions.
final Context contextForResources = getContextForResources(context);
Context inflationContext = new ContextWrapper(context) {
@Override
public Resources getResources() {
return contextForResources.getResources();
}
@Override
public Resources.Theme getTheme() {
return contextForResources.getTheme();
}
@Override
public String getPackageName() {
return contextForResources.getPackageName();
}
};
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don‘t add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
return inflater.inflate(rv.getLayoutId(), parent, false);
}
主要关注最后几行代码即可,把握主要流程,一些细节可以暂时忽略。
RemoteViews的apply方法中还有这样一行代码:
rvToApply.performApply(result, parent, handler);
接下来会调用:
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions != null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}
即遍历RemoteViews中存储的Action
,然后执行Action
的apply
方法。
上一小节提到了Action
对象,那么Action
又是什么呢?由于RemoteViews运行在远端进程中,因此无法通过findViewById
的方法来获取View。为了操作远程视图,于是就将对视图的操作封装成一个Action
对象,Action
是一个实现了Parcelable接口的抽象类,因此可以跨进程传输。
先来看下Action
的定义:
/**
* Base class for all actions that can be performed on an
* inflated view.
*
* SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
*/
private abstract static class Action implements Parcelable {
public abstract void apply(View root, ViewGroup rootParent,
OnClickHandler handler) throws ActionException;
public static final int MERGE_REPLACE = 0;
public static final int MERGE_APPEND = 1;
public static final int MERGE_IGNORE = 2;
public int describeContents() {
return 0;
}
/**
* Overridden by each class to report on it‘s own memory usage
*/
public void updateMemoryUsageEstimate(MemoryUsageCounter counter) {
// We currently only calculate Bitmap memory usage, so by default,
// don‘t do anything here
}
public void setBitmapCache(BitmapCache bitmapCache) {
// Do nothing
}
public int mergeBehavior() {
return MERGE_REPLACE;
}
public abstract String getActionName();
public String getUniqueKey() {
return (getActionName() + viewId);
}
/**
* This is called on the background thread. It should perform any non-ui computations
* and return the final action which will run on the UI thread.
* Override this if some of the tasks can be performed async.
*/
public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
return this;
}
int viewId;
}
从说明来看,Action就是对远程视图操作的一个封装,它提供了一个抽象方法:
public abstract void apply(View root, ViewGroup rootParent,
OnClickHandler handler) throws ActionException;
该抽象方法需要子类做具体实现。
Action有很多子类,几乎每个子类都用来辅助一种View操作,下面简单罗列两个:
/**
* Helper action to set compound drawables on a TextView. Supports relative
* (s/t/e/b) or cardinal (l/t/r/b) arrangement.
*/
private class TextViewDrawableAction extends Action {}
/**
* Helper action to set text size on a TextView in any supported units.
*/
private class TextViewSizeAction extends Action {}
这里不再一一列举,有兴趣的可以参考源码。
了解了Action
的概念之后,我们以为远程的TextView
设置文本为例,来具体看一下其工作流程。
平时我们给TextView
设置文本只需要调用其setText
方法即可,但RemoteViews无法这样使用,RemoteViews提供了一系列关于View操作的set方法,这里会用到如下方法:
/**
* Equivalent to calling TextView.setText
*
* @param viewId The id of the view whose text should change
* @param text The new text for the view
*/
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
其中第一个参数为TextView
控件的ID,布局文件是我们自己定义的,因此控件ID可知;第二个参数为要设置的目标文本,接下来继续跟踪源码:
/**
* Call a method taking one CharSequence on a view in the layout for this RemoteViews.
*
* @param viewId The id of the view on which to call the method.
* @param methodName The name of the method to call.
* @param value The value to pass to the method.
*/
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
可以看到第二个参数为方法名,此时应该可以想到后面会使用反射来为TextView赋值了。接着可以看到果然将对TextView的操作封装成了一个Action,这里的实现子类为ReflectionAction,并将其添加到mActions列表中,需要注意的是此时并未立即执行该View操作,只是添加到了操作列表,至于何时执行,稍后再说,先来看下ReflectionAction的apply实现:
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class<?> param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
正如我们猜测,内部就是利用反射机制为TextView来设置文本的。
上文说到只是暂时将Action添加到列表了,对于何时执行这些操作,要结合应用场景来说。对于自定义通知栏,需要NotificationManager调用notify()之后;而对于桌面小部件,则需要AppWidgetManager调用updateAppWidget()之后。至于为什么不实时调用,大概是基于频繁跨进程视图操作的性能开销及效率考量吧。
对于View操作的执行对应着每个Action的apply方法的调用,在RemoteViews中则对应着apply与reapply两种方式。它们的区别是:apply加载视图并执行View操作(前文已分析过),用于通知栏和桌面小部件的初始化操作;而reapply则适用于远程视图的更新操作。
除了更新远程视图的UI,往往还需要为远程视图添加点击事件,可想而知同样不能像以往那样设置setOnClickListener
,这里就需要用到PendingIntent。
顾名思义,PendingIntent可以理解为等待的或即将发生的意图,但也不要被其名字误导,它并非继承Intent,其定义如下:
public final class PendingIntent implements Parcelable {}
或者可以把PendingIntent理解为一种特殊的异步处理机制,且它往往是跨进程执行,因此实现了Parcelable接口。
PendingIntent提供了3种类型:
对应于如下方法:
public static PendingIntent getActivity(Context context, int requestCode,
Intent intent, @Flags int flags)
public static PendingIntent getActivity(Context context, int requestCode,
@NonNull Intent intent, @Flags int flags, @Nullable Bundle options)
public static PendingIntent getService(Context context, int requestCode,
@NonNull Intent intent, @Flags int flags)
public static PendingIntent getBroadcast(Context context, int requestCode,
Intent intent, @Flags int flags)
这里先简单了解PendingIntent即可,关于PendingIntent的使用下文会涉及。如果想深入理解PendingIntent,推荐一篇文章:说说PendingIntent的内部机制。
先来看展示自定义通知栏的核心代码:
public void showNotification() {
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotification = new Notification();
mNotification.icon = R.drawable.ic_star;
mNotification.tickerText = "Serendipity";
mNotification.when = System.currentTimeMillis();
mContentView = new RemoteViews(getPackageName(), R.layout.notification_layout);
// 可覆盖xml中定义的text
// mContentView.setTextViewText(R.id.text11, "other text");
// 设置常驻,不能滑动取消
// mNotification.flags = Notification.FLAG_ONGOING_EVENT;
// 默认跳转主界面
mIntent = new Intent(this, MainActivity.class);
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mPendingIntent = PendingIntent.getActivities(this, 0, new Intent[]{mIntent}, PendingIntent.FLAG_UPDATE_CURRENT);
// 每个view的点击响应
mContentView.setOnClickPendingIntent(R.id.ll_11, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_ONE), PendingIntent.FLAG_UPDATE_CURRENT));
mContentView.setOnClickPendingIntent(R.id.ll_22, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_TWO), PendingIntent.FLAG_UPDATE_CURRENT));
mContentView.setOnClickPendingIntent(R.id.ll_33, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_THREE), PendingIntent.FLAG_UPDATE_CURRENT));
mContentView.setOnClickPendingIntent(R.id.ll_44, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_FOUR), PendingIntent.FLAG_UPDATE_CURRENT));
mNotification.contentView = mContentView;
mNotification.contentIntent = mPendingIntent;
mNotificationManager.notify(notificationId, mNotification);
}
其中代码
mContentView = new RemoteViews(getPackageName(), R.layout.notification_layout);
从一个布局文件中加载视图,该布局文件的定义如下:
<?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"
android:gravity="center">
<LinearLayout
android:id="@+id/ll_11"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_menu_gallery"
android:textColor="#000" />
<TextView
android:id="@+id/text11"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Action1"
android:textColor="#000" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_22"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_menu_manage"
android:textColor="#000" />
<TextView
android:id="@+id/text22"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Action2"
android:textColor="#000" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_33"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_menu_send"
android:textColor="#000" />
<TextView
android:id="@+id/text33"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="action3"
android:textColor="#000" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_44"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_menu_share"
android:textColor="#000" />
<TextView
android:id="@+id/text44"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="action4"
android:textColor="#000" />
</LinearLayout>
</LinearLayout>
布局很简单,以上图下文的格式并排放了四个按钮。
接下来的代码为四个按钮设置了点击事件,此处使用的是广播类型的PendingIntent,并分别为Intent设置了不同的Action,后续通过自定义的BroadcastReceiver来接收广播,并根据接收到的Intent的Action进行响应。
对于整个通知栏则使用了Activity类型的PendingIntent,因此点击时会跳转主界面。
最终的效果图如下:
桌面小部件在新版本的Android系统上入口比较隐蔽了,在Android 7.1.1上(Nexus 6P)需要在主屏空白处长按,将会出现小部件的入口。
这里我们制作一个简单的小部件来作为示例:该小部件也是上图下文,文本来显示当前时间,并且每秒更新。最终效果如下:
首先定义小部件的视图布局,在res/layout文件夹中创建app_widget.xml,其内容如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/twitter_png" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Twitter"/>
</LinearLayout>
布局比较简单,上面为一个ImageView,下面是一个TextView,稍后我们会使用当前时间来远程更新TextView的内容。
接下来定义小部件的配置文件,在res/xml目录中创建app_widget_config.xml文件,其内容如下:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/app_widget"
android:minHeight="200dp"
android:minWidth="150dp"
android:updatePeriodMillis="50000" />
其中initialLayout指向小部件的视图布局,updatePeriodMillis规定了小部件的更新周期,系统为了节电,默认30分钟更新一次,即便设置的时间小于30分钟,也是30分钟更新一次。
然后我们再定义一个服务,用于更新小部件中的TextView,该服务的代码如下:
public class TestService extends Service {
private Timer mTimer;
private SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mTimer = new Timer();
mTimer.schedule(new TimerTask() {
@Override
public void run() {
updateWidget();
}
}, 0, 1000);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
}
public void updateWidget() {
String time = f.format(new Date());
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.app_widget);
remoteViews.setTextViewText(R.id.tv_content, time);
AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext());
ComponentName cn = new ComponentName(getApplicationContext(), WidgetProvider.class);
manager.updateAppWidget(cn, remoteViews);
}
}
在服务中运行一个定时器,每隔一秒调用AppWidgetManager的updateAppWidget来更新小部件。用来更新TextView的代码如下:
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.app_widget);
remoteViews.setTextViewText(R.id.tv_content, time);
其原理在前文中已有分析。
桌面小部件还要依赖于AppWidgetProvider,AppWidgetProvider本质上是一个广播接收器,从其定义可以验证:
public class AppWidgetProvider extends BroadcastReceiver {}
我们自定义了一个AppWidgetProvider,代码如下:
package com.aspook.appwidget;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public class WidgetProvider extends AppWidgetProvider {
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
context.stopService(new Intent(context, TestService.class));
}
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
}
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
context.startService(new Intent(context, TestService.class));
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
}
@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
}
public WidgetProvider() {
super();
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
}
上述代码onEnabled中会启动服务,onDisabled中停止服务,同时还有其他涉及小部件生命周期的回调。
最后需要在AndroidManifest.xml中注册广播接收器与服务:
<service android:name="com.aspook.appwidget.TestService" />
<receiver android:name="com.aspook.appwidget.WidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_config" />
</receiver>
上述步骤完毕之后,当你将上述代码运行到手机上,就可以从小部件库中找到刚才定义的小部件了,你可以把它拖到桌面上,会发现小部件的TextView每隔一秒都会更新当前时间,如最初的效果图所示。
标签:code 部件 tty twitter getc out data 发送 height
原文地址:http://blog.csdn.net/ahence/article/details/62418926