1.简介
- 长按桌面空白处,弹框选择widgets,里边找到home screen tips,拖动到桌面
- 代码有点旧,pendingIntent少了flag :PendingIntent.FLAG_MUTABLE,导致布局加载失败
2.Protips
2.1.清单文件
<receiver android:name=".ProtipWidget"
android:label="@string/widget_name"
android:exported="true">
<!--小部件查询的条件-->
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="com.android.protips.NEXT_TIP"/>
<action android:name="com.android.protips.HEE_HEE"/>
</intent-filter>
<intent-filter>
<action android:name="android.provider.Telephony.SECRET_CODE"/>
<data android:scheme="android_secret_code"
android:host="8477"/>
</intent-filter>
<!--必须的,这个是预览效果-->
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_build"/> //见补充1
</receiver>
>1.widget_build.xml
初始化布局见2.2
<appwidget-provider
android:minWidth="294dip"
android:minHeight="72dip"
android:updatePeriodMillis="0"
android:previewImage="@drawable/preview"
android:initialLayout="@layout/widget" />
2.2.布局
>1.widget.xml
效果图见开头
<RelativeLayout
android:id="@+id/widget"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dip"
>
<include layout="@layout/droid" />
<include layout="@layout/bubble" />
</RelativeLayout>
>2.droid.xml
<ImageView
android:id="@+id/bugdroid"
android:src="@drawable/droidman_down_closed"
android:scaleType="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
/>
>3.bubble.xml
<RelativeLayou
android:id="@+id/tip_bubble"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/bugdroid"
android:layout_centerVertical="true"
android:gravity="center_vertical|left"
android:layout_marginRight="2dip"
android:visibility="invisible"
android:background="@drawable/droid_widget"
android:focusable="true"
>
<TextView
android:layout_width="0dip"
android:layout_height="0dip"
android:layout_alignParentTop="true"
android:layout_marginTop="-100dip"
android:text="@string/widget_name"
/>
<TextView
android:layout_width="0dip"
android:layout_height="0dip"
android:layout_alignParentTop="true"
android:layout_marginTop="-90dip"
android:text="@string/tts_pause"
/>
<TextView
android:id="@+id/tip_footer"
style="@style/TipText.Footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginRight="1dip"
/>
<ImageView
android:id="@+id/tip_callout"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_above="@id/tip_footer"
android:visibility="gone"
android:padding="4dip"
/>
<TextView
android:id="@+id/tip_header"
style="@style/TipText.Header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@id/tip_callout"
android:layout_alignWithParentIfMissing="true"
android:layout_marginTop="0dip"
android:layout_marginLeft="3dip"
/>
<TextView
android:id="@+id/tip_message"
style="@style/TipText.Message"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tip_header"
android:layout_alignLeft="@id/tip_header"
android:layout_alignRight="@id/tip_header"
android:layout_marginTop="1dip"
/>
</RelativeLayout>
3.AppWidgetProvider
先复习小部件的核心类
- 就是个广播的封装,处理常用的action,完事交给对应的方法处理,
public class AppWidgetProvider extends BroadcastReceiver {
3.1.onReceive
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
//新加的小部件,需要提供小部件id
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
//删除小部件,需要提供id
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
//更新,需要提供id,以及参数bundle
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
}
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
//可用
this.onEnabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
//不可用
this.onDisabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
//恢复,必须有旧的ids,就是要恢复的,也可以有新的ids,用来更新
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
4.ProtipWidget
public class ProtipWidget extends AppWidgetProvider {
4.1.onReceive
就是把监听交给异步线程处理了
public void onReceive(final Context context, final Intent intent) {
final PendingResult result = goAsync();
Runnable worker = new Runnable() {
@Override
public void run() {
//见4.2
onReceiveAsync(context, intent);
result.finish();
}
};
mAsyncHandler.post(worker);
}
4.2.onReceiveAsync
- mMessage 可以理解为tips数组的索引
void onReceiveAsync(Context context, Intent intent) {
setup(context);//补充1
Resources res = mContext.getResources();
mTips = res.getTextArray(mTipSet == 1 ? R.array.tips2 : R.array.tips);
//显示下一个提示
if (intent.getAction().equals(ACTION_NEXT_TIP)) {
//补充2,点击next以后,索引加一
mMessage = getNextMessageIndex();
//保存数据
SharedPreferences.Editor pref = context.getSharedPreferences(PREFS_NAME, 0).edit();
pref.putInt(PREFS_TIP_NUMBER, mMessage);
pref.apply();
//刷新,补充3
refresh();
} else if (intent.getAction().equals(ACTION_POKE)) {
blink(intent.getIntExtra(EXTRA_TIMES, 1));
} else if (intent.getAction().equals(AppWidgetManager.ACTION_APPWIDGET_ENABLED)) {
goodmorning();
} else if (intent.getAction().equals("android.provider.Telephony.SECRET_CODE")) {
//这个广播是在拨号页面输入了调试代码
Log.d("Protips", "ACHIEVEMENT UNLOCKED");
mTipSet = 1 - mTipSet;
mMessage = 0;
//更新tip数据
SharedPreferences.Editor pref = context.getSharedPreferences(PREFS_NAME, 0).edit();
pref.putInt(PREFS_TIP_NUMBER, mMessage);
pref.putInt(PREFS_TIP_SET, mTipSet);
pref.apply();
mContext.startActivity(
new Intent(Intent.ACTION_MAIN)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP)
.addCategory(Intent.CATEGORY_HOME));
final Intent bcast = new Intent(context, ProtipWidget.class);
bcast.setAction(ACTION_POKE);
bcast.putExtra(EXTRA_TIMES, 3);
mContext.sendBroadcast(bcast);
} else {
mIconRes = R.drawable.droidman_open;
refresh();
}
}
>1.setup
获取初始化的变量值
private void setup(Context context) {
mContext = context;
mWidgetManager = AppWidgetManager.getInstance(context);
mWidgetIds = mWidgetManager.getAppWidgetIds(new ComponentName(context, ProtipWidget.class));
SharedPreferences pref = context.getSharedPreferences(PREFS_NAME, 0);
//当前提示在集合里的索引
mMessage = pref.getInt(PREFS_TIP_NUMBER, 0);
//提示用到集合类型,默认是0
mTipSet = pref.getInt(PREFS_TIP_SET, 0);
//根据提示类型加载不同的集合数据
mTips = context.getResources().getTextArray(mTipSet == 1 ? R.array.tips2 : R.array.tips);
if (mTips != null) {
if (mMessage >= mTips.length) mMessage = 0;
} else {
mMessage = -1;
}
}
>2.getNextMessageIndex
mMessage 索引加一,不能超过tips数组的长度
private int getNextMessageIndex() {
return (mMessage + 1) % mTips.length;
}
>3.refresh
private void refresh() {
//见4.3 构建view
RemoteViews rv = buildUpdate(mContext);
for (int i : mWidgetIds) {
mWidgetManager.updateAppWidget(i, rv);
}
}
>4.goodmorning
图片切换
public void goodmorning() {
mMessage = -1;
try {
setIcon(R.drawable.droidman_down_closed);
Thread.sleep(500);
setIcon(R.drawable.droidman_down_open);
Thread.sleep(200);
setIcon(R.drawable.droidman_down_closed);
Thread.sleep(100);
setIcon(R.drawable.droidman_down_open);
Thread.sleep(600);
} catch (InterruptedException ex) {
}
mMessage = 0;//置为0
mIconRes = R.drawable.droidman_open;
refresh();
}
private void setIcon(int resId) {
mIconRes = resId;
refresh();
}
>5.blink
闪烁,就是切换图片几次
private void blink(int blinks) {
// don't blink if no bubble showing or if goodmorning() is happening
if (mMessage < 0) return;
setIcon(R.drawable.droidman_closed);
try {
Thread.sleep(100);
while (0<--blinks) {
setIcon(R.drawable.droidman_open);
Thread.sleep(200);
setIcon(R.drawable.droidman_closed);
Thread.sleep(100);
}
} catch (InterruptedException ex) { }
setIcon(R.drawable.droidman_open);
}
4.3.buildUpdate
public RemoteViews buildUpdate(Context context) {
RemoteViews updateViews = new RemoteViews(
context.getPackageName(), R.layout.widget);
//气泡的点击事件,发送next_tip广播
Intent bcast = new Intent(context, ProtipWidget.class);
bcast.setAction(ACTION_NEXT_TIP);
PendingIntent pending = PendingIntent.getBroadcast(
context, 0, bcast, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
updateViews.setOnClickPendingIntent(R.id.tip_bubble, pending);
//android图标的点击事件,发送闪烁广播
bcast = new Intent(context, ProtipWidget.class);
bcast.setAction(ACTION_POKE);
bcast.putExtra(EXTRA_TIMES, 1);//闪烁次数,见4.2.5,先进行的自减,所以while循环不会走
pending = PendingIntent.getBroadcast(
context, 0, bcast, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
updateViews.setOnClickPendingIntent(R.id.bugdroid, pending);
// Tip bubble text
if (mMessage >= 0) {
//文本内容见补充2
//根据索引读取要显示的提示文字,先按照换行符分割字符串
String[] parts = sNewlineRegex.split(mTips[mMessage], 2);
String title = parts[0];
String text = parts.length > 1 ? parts[1] : "";
//再看文本部分是否有图片
Matcher m = sDrawableRegex.matcher(text);
if (m.find()) {
//有图片
String imageName = m.group(1);
int resId = context.getResources().getIdentifier(
imageName, null, context.getPackageName());
//加载图片并显示
updateViews.setImageViewResource(R.id.tip_callout, resId);
updateViews.setViewVisibility(R.id.tip_callout, View.VISIBLE);
text = m.replaceFirst("");
} else {
//隐藏图片
updateViews.setImageViewResource(R.id.tip_callout, 0);
updateViews.setViewVisibility(R.id.tip_callout, View.GONE);
}
updateViews.setTextViewText(R.id.tip_message,
text);
updateViews.setTextViewText(R.id.tip_header,
title);
//footer显示的就是tip的索引, 1 of 5
updateViews.setTextViewText(R.id.tip_footer,
context.getResources().getString(
R.string.pager_footer,
(1+mMessage), mTips.length));
//容器可见
updateViews.setViewVisibility(R.id.tip_bubble, View.VISIBLE);
} else {
//容器不可见
updateViews.setViewVisibility(R.id.tip_bubble, View.INVISIBLE);
}
//右侧机器人图标
updateViews.setImageViewResource(R.id.bugdroid, mIconRes);
return updateViews;
}
>1.Pattern
private static final Pattern sNewlineRegex = Pattern.compile(" *\n *");
private static final Pattern sDrawableRegex = Pattern.compile(" *@(drawable/[a-z0-9_]+) *");
>2.tips集合数据
<!-- Set of tips to show the user.
First line is the tip title, which is shown in bold on its own line.
All subsequent text is placed into the tip body.
The occurrence of @drawable/foo causes the bitmap drawable named "foo"
to be displayed to the right of the tip text.
Example for tip #1:
_____________________________________________ _/
| | /._.\
| See all your apps. [all_apps.png] > U| |U
| Touch the Launcher icon. | |___|
|_____________________________________________| U U
-->
<string-array name="tips">
<!-- Tip: Where the launcher icon is and what it does. With icon. -->
<item>See all your apps.\n
Touch the Launcher icon. @drawable/all_apps</item>
<!-- Tip: Longpress in Launcher (all apps) to drag apps to the home screen. -->
<item>Drag apps to your Home screen.\n
Touch & hold an app in the Launcher until it vibrates.</item>
<!-- Tip: Longpress to move icons/widgets around. -->
<item>Rearrange your Home screen.\n
Touch & hold an item and when it vibrates, drag it where you want.</item>
<!-- Tip: Longpress icons/widgets and drag to trash to remove them. -->
<item>Remove items.\n
Touch & hold an item and when it vibrates, drag it to the Trash icon. @drawable/trash</item>
<!-- Tip: Swipe to switch workspaces; drag items to move them to other
workspaces. -->
<item>Multiple Home screens.\n
Swipe left or right to switch. Drag items to other screens.</item>
<!-- Tip: Reminder about how to delete widgets, reinforcing that this
technique can be used to remove the tips widget iteself. -->
<item>Done with this widget?\n
Touch & hold it and when it vibrates, drag it to the Trash icon.</item>
</string-array>
5.总结
5.1.广播顺序
在widgets列表里,选中长按以后,就会依次收到2个广播
android.appwidget.action.APPWIDGET_ENABLED
android.appwidget.action.APPWIDGET_UPDATE
长按小组件,拖动到顶部删除
android.appwidget.action.APPWIDGET_DELETED
android.appwidget.action.APPWIDGET_DISABLED
5.2.自定义流程
- 自定义一个小组件,首先写个广播,继承小节3(或者不继承,你把小节3里要监听的广播自己处理也可以)
- 广播里处理相应的action,小节3已封装,我们只需要实现对应的方法即可。
- 清单文件里注册广播,添加必要的intentFilter(APPWIDGET_UPDATE),以及meta-data,参考2.1
5.3.特殊的小部件
- 参考settings的小部件,搜索清单文件,并没有注册对应的广播,可实际却显示一个logo图标的小部件
- 它是注册了一个CREATE_SHORTCUT的IntentFilter
String ACTION_CREATE_SHORTCUT = "android.intent.action.CREATE_SHORTCUT"
相关的清单文件
<activity android:name=".Settings$CreateShortcutActivity"
android:exported="true"
android:label="@string/settings_shortcut">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.shortcut.CreateShortcut" />
</activity>