前言
Launcher在Android的AppWidget整个体系中扮演AppWidgetHost的角色,本文分析Launcher对于AppWidget的处理 部分源码分析。
一、AppWidget加载流程
1. Android系统启动,SystemServer创建AppWidgetService,并调用systemReady()方法,在systemReady()方法中做以下三项准备工作:
- 通过PackageManager从Android系统中查找所有已经被安装的AppWidget(包含"android.appwidget.action.APPWIDGET_UPDATE“ 的Action和meta-data标签),解析AppWidget的配置信息,封闭成对象,保存到List集合。
- 从/data/system/users/0/appwidgets.xml文件读取已经被添加到Launcher的AppWidget信息,封闭成对象,保存到List集合中。
- 注册四个广播接收器:第一. Android系统启动完成,第二. Android配置信息改变,第三. 添加删除应用,第四. sdcard的安装与缷载。
2. Android系统启动Launcher应用程序,会做以下准备工作:
- 从Launcher应用的数据库查找已经被添加到Launcher的AppWidget信息。
- 根据查找到的appWidgetId值(整型值)创建LauncherAppWidgetHostView布局对象。
- 根据查找到的appWidgetId值(整型值)从AppWidgetService中获取RemoteViews对象(因为是第一次启动所以RemoteViews对象为空)。
- 将获取到的RemoteViews对象的布局解析并设置到第(2)步中创建的LauncherAppWidgetHostView布局对象中。
- 将LauncherAppWidgetHostView布局对象添加到Launcher的WorkSpace中(因为RemoteViews对象为空,所以只在Launcher的 WorkSpace中占了一个位置)。
3. Android系统启动完成,发出BOOT_COMPLETED广播,AppWidgetService接收到广播后,会做以下事情:
- 获取已经添加到Launcher的AppWidget列表,依次向这个Widget发出APPWIDGET_ENABLED和 APPWIDGET_UPDATE更新广播,根据配置的更新间隔定时发出更新广播。
- 每个AppWidget接收到广播后都会调用onEnabled()方法和onUpdate()方法,在onEnabled()方法中进行一些初始化操作,在onUpdate()方法中创建RemoteViews布局对象并通过AppWidgetManager的updateAppWidget(int appWidgetId, RemoteViews remoteViews)方法通知AppWidgetService对象用RemoteViews对象更新appWidgetId所对应的AppWidget。
- AppWidgetService接收到了appWidgetId和RemoteViews后,通过appWidgetId查找已经被添加到Launcher的LauncherAppWidgetHostView布局对象,并RemoteViews中的布局更新到LauncherAppWidgetHostView布局对象中。AppWidget显示在Launcher中。
二、AppWidget 预览界面的加载流程
1. 操作流程
如图,长按空白区域出现弹框 ,点击 widgets选选项 进入下图 widget预览页面
2. 显示流程源码分析
- Workspace 的构造方法中 设置了 setOnTouchListener 交给 WorkspaceTouchListener类处理
public Workspace(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLauncher = Launcher.getLauncher(context);
....
//设置 触摸事件
setOnTouchListener(new WorkspaceTouchListener(mLauncher, this));
}
public class WorkspaceTouchListener implements OnTouchListener, Runnable {
...
}
- WorkspaceTouchListener 中的 onTouch 中的 mWorkspace.postDelayed(this, getLongPressTimeout()); 自己实现的 Runnable接口再观其 run() 方法。
public boolean onTouch(View view, MotionEvent ev) {
int action = ev.getActionMasked();
if (action == ACTION_DOWN) {
// Check if we can handle long press.
boolean handleLongPress = canHandleLongPress();
...
cancelLongPress();
if (handleLongPress) {
mLongPressState = STATE_REQUESTED;
mTouchDownPoint.set(ev.getX(), ev.getY());
// 长按点击事件
mWorkspace.postDelayed(this, getLongPressTimeout());
}
mWorkspace.onTouchEvent(ev);
// Return true to keep receiving touch events
return true;
}
......
}
public void run() {
if (mLongPressState == STATE_REQUESTED) {
if (canHandleLongPress()) {
...
OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
} else {
cancelLongPress();
}
}
}
- OptionsPopupView 中添加选项,根据触摸坐标设置显示的区域 RectF, 最后显示弹框
public static void showDefaultOptions(Launcher launcher, float x, float y) {
float halfSize = launcher.getResources().getDimension(R.dimen.options_menu_thumb_size) / 2;
if (x < 0 || y < 0) {
x = launcher.getDragLayer().getWidth() / 2;
y = launcher.getDragLayer().getHeight() / 2;
}
RectF target = new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
ArrayList options = new ArrayList<>();
options.add(new OptionItem(R.string.wallpaper_button_text, R.drawable.ic_wallpaper,
ControlType.WALLPAPER_BUTTON, OptionsPopupView::startWallpaperPicker));
options.add(new OptionItem(R.string.widget_button_text, R.drawable.ic_widget,
ControlType.WIDGETS_BUTTON, OptionsPopupView::onWidgetsClicked));
options.add(new OptionItem(R.string.settings_button_text, R.drawable.ic_setting,
ControlType.SETTINGS_BUTTON, OptionsPopupView::startSettings));
show(launcher, target, options);
}
public static void show(Launcher launcher, RectF targetRect, List items) {
OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater()
.inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false);
popup.mTargetRect = targetRect;
for (OptionItem item : items) {
DeepShortcutView view = popup.inflateAndAdd(R.layout.system_shortcut, popup);
view.getIconView().setBackgroundResource(item.mIconRes);
view.getBubbleText().setText(item.mLabelRes);
view.setDividerVisibility(View.INVISIBLE);
view.setOnClickListener(popup);
view.setOnLongClickListener(popup);
popup.mItemMap.put(view, item);
}
popup.reorderAndShow(popup.getChildCount());
}
- 其中注意 OptionsPopupView::onWidgetsClicked 这个是 点击事件 显示 WidgetsFullSheet(widget的预览界面)
public static boolean onWidgetsClicked(View view) {
Launcher launcher = Launcher.getLauncher(view.getContext());
if (launcher.getPackageManager().isSafeMode()) {
Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
return false;
} else {
WidgetsFullSheet.show(launcher, true /* animated */);
return true;
}
}
public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
.inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
sheet.mIsOpen = true;
launcher.getDragLayer().addView(sheet);
sheet.open(animate);
return sheet;
}
二、AppWidget 绑定流程
- Launcher 的 onCreate 方法中 mModel.startLoader(currentScreen)
protected void onCreate(Bundle savedInstanceState) {
...
if (!mModel.startLoader(currentScreen)) {
if (!internalStateHandled) {
// If we are not binding synchronously, show a fade in animation when
// the first page bind completes.
mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
}
} else {
// Pages bound synchronously.
mWorkspace.setCurrentPage(currentScreen);
setWorkspaceLoading(true);
}
...
}
- LauncherModel类中 startLoader 方法中 主要关注 loaderResults.bindWidgets() ,其通过
callbacks.bindAllWidgets(widgets) ,回调到 Launcher的bindAllWidgets 方法处理。
public boolean startLoader(int synchronousBindPage) {
// Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
synchronized (mLock) {
// Don't bother to start the thread if we know it's not going to do anything
if (mCallbacks != null && mCallbacks.get() != null) {
final Callbacks oldCallbacks = mCallbacks.get();
// Clear any pending bind-runnables from the synchronized load process.
mUiExecutor.execute(oldCallbacks::clearPendingBinds);
// If there is already one running, tell it to stop.
stopLoader();
LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
mBgAllAppsList, synchronousBindPage, mCallbacks);
if (mModelLoaded && !mIsLoaderTaskRunning) {
// Divide the set of loaded items into those that we are binding synchronously,
// and everything else that is to be bound normally (asynchronously).
loaderResults.bindWorkspace();
// For now, continue posting the binding of AllApps as there are other
// issues that arise from that.
loaderResults.bindAllApps();
loaderResults.bindDeepShortcuts();
loaderResults.bindWidgets();
return true;
} else {
// 其中方法,最后也是走到了 loaderResults.bindWidgets();
startLoaderForResults(loaderResults);
}
}
}
return false;
}
获取 widget数据 回调到 launcher中
public void bindWidgets() {
final ArrayList widgets =
mBgDataModel.widgetsModel.getWidgetsList(mApp.getContext());
Runnable r = new Runnable() {
public void run() {
Callbacks callbacks = mCallbacks.get();
if (callbacks != null) {
callbacks.bindAllWidgets(widgets);
}
}
};
mUiExecutor.execute(r);
}
- 向 PopupDataProvider类中设置 widget 数据,调用 AbstractFloatingView.onWidgetsBound();
Launcher.java
@Override
public void bindAllWidgets(final ArrayList allWidgets) {
mPopupDataProvider.setAllWidgets(allWidgets);
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(this);
if (topView != null) {
topView.onWidgetsBound();
}
}
前面也讲到过 widget的预览视图由 WidgetsFullSheet 显示 而WidgetsFullSheet是 AbstractFloatingView的子类,所以最终调用了WidgetsFullSheet.onWidgetsBound()。
WidgetsFullSheet.java
@Override
protected void onWidgetsBound() {
// 从PopupDataProvider类中取出之前设置进去的数据,填充adapter
mAdapter.setWidgets(mLauncher.getPopupDataProvider().getAllWidgets());
}
- 总结上面,widget数据最终 交由 WidgetsFullSheet 类处理,下面我们看下
/**
* Popup for showing the full list of available widgets
*/
public class WidgetsFullSheet extends BaseWidgetSheet
implements Insettable, ProviderChangedListener {
private static final long DEFAULT_OPEN_DURATION = 267;
private static final long FADE_IN_DURATION = 150;
private static final float VERTICAL_START_POSITION = 0.3f;
private final Rect mInsets = new Rect();
private final WidgetsListAdapter mAdapter;
private WidgetsRecyclerView mRecyclerView;
public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LauncherAppState apps = LauncherAppState.getInstance(context);
mAdapter = new WidgetsListAdapter(context,
LayoutInflater.from(context), apps.getWidgetCache(), apps.getIconCache(),
this, this);
}
public WidgetsFullSheet(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = findViewById(R.id.container);
mRecyclerView = findViewById(R.id.widgets_list_view);
mRecyclerView.setAdapter(mAdapter);
mAdapter.setApplyBitmapDeferred(true, mRecyclerView);
TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent;
springLayout.addSpringView(R.id.widgets_list_view);
mRecyclerView.setEdgeEffectFactory(springLayout.createEdgeEffectFactory());
onWidgetsBound();
}
@Override
protected Pair getAccessibilityTarget() {
return Pair.create(mRecyclerView, getContext().getString(
mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mLauncher.getAppWidgetHost().addProviderChangeListener(this);
notifyWidgetProvidersChanged();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mLauncher.getAppWidgetHost().removeProviderChangeListener(this);
}
@Override
public void setInsets(Rect insets) {
mInsets.set(insets);
mRecyclerView.setPadding(
mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingTop(),
mRecyclerView.getPaddingRight(), insets.bottom);
if (insets.bottom > 0) {
setupNavBarColor();
} else {
clearNavBarColor();
}
((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom);
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthUsed;
if (mInsets.bottom > 0) {
widthUsed = 0;
} else {
Rect padding = mLauncher.getDeviceProfile().workspacePadding;
widthUsed = Math.max(padding.left + padding.right,
2 * (mInsets.left + mInsets.right));
}
int heightUsed = mInsets.top + mLauncher.getDeviceProfile().edgeMarginPx;
measureChildWithMargins(mContent, widthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = r - l;
int height = b - t;
// Content is laid out as center bottom aligned
int contentWidth = mContent.getMeasuredWidth();
int contentLeft = (width - contentWidth) / 2;
mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
contentLeft + contentWidth, height);
setTranslationShift(mTranslationShift);
}
@Override
public void notifyWidgetProvidersChanged() {
mLauncher.refreshAndBindWidgetsForPackageUser(null);
}
@Override
protected void onWidgetsBound() {
mAdapter.setWidgets(mLauncher.getPopupDataProvider().getAllWidgets());
}
private void open(boolean animate) {
if (animate) {
if (mLauncher.getDragLayer().getInsets().bottom > 0) {
mContent.setAlpha(0);
setTranslationShift(VERTICAL_START_POSITION);
}
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
mOpenCloseAnimator
.setDuration(DEFAULT_OPEN_DURATION)
.setInterpolator(AnimationUtils.loadInterpolator(
getContext(), android.R.interpolator.linear_out_slow_in));
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRecyclerView.setLayoutFrozen(false);
mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
mOpenCloseAnimator.removeListener(this);
}
});
post(() -> {
mRecyclerView.setLayoutFrozen(true);
mOpenCloseAnimator.start();
mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
});
} else {
setTranslationShift(TRANSLATION_SHIFT_OPENED);
mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
post(this::announceAccessibilityChanges);
}
}
@Override
protected void handleClose(boolean animate) {
handleClose(animate, DEFAULT_OPEN_DURATION);
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
// Disable swipe down when recycler view is scrolling
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = false;
RecyclerViewFastScroller scroller = mRecyclerView.getScrollbar();
if (scroller.getThumbOffsetY() >= 0 &&
mLauncher.getDragLayer().isEventOverView(scroller, ev)) {
mNoIntercept = true;
} else if (mLauncher.getDragLayer().isEventOverView(mContent, ev)) {
mNoIntercept = !mRecyclerView.shouldContainerScroll(ev, mLauncher.getDragLayer());
}
}
return super.onControllerInterceptTouchEvent(ev);
}
public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
.inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
sheet.mIsOpen = true;
launcher.getDragLayer().addView(sheet);
sheet.open(animate);
return sheet;
}
@Override
protected int getElementsRowCount() {
return mAdapter.getItemCount();
}
}
由以上代码可以看出 WidgetsFullSheet 是一个自定义的view ,构造方法中 mAdapter = new WidgetsListAdapter(); 在show(Launcher launcher, boolean animate) 方法中 inflate(R.layout.widgets_full_sheet,,) 布局文件;在 onFinishInflate() 方法中 mRecyclerView = findViewById(R.id.widgets_list_view); mRecyclerView.setAdapter(mAdapter); 最后由之前所说的
onWidgetsBound() 方法中 adapter 设置数据。
R.layout.widgets_full_sheet 文件
- WidgetsListAdapter 就是一个recycleView的adapter ,onCreateViewHolder()中
inflate(R.layout.widgets_list_row_view, parent, false) 布局文件如下
widgets_scroll_container.xml 文件
然后 看 onBindViewHolder 方法
@Override
public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) {
WidgetListRowEntry entry = mEntries.get(pos);
List infoList = entry.widgets;
ViewGroup row = holder.cellContainer;
if (DEBUG) {
Log.d(TAG, String.format(
"onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]",
pos, infoList.size(), row.getChildCount()));
}
// Add more views.
// if there are too many, hide them.
int expectedChildCount = infoList.size() + Math.max(0, infoList.size() - 1);
int childCount = row.getChildCount();
if (expectedChildCount > childCount) {
for (int i = childCount ; i < expectedChildCount; i++) {
if ((i & 1) == 1) {
// Add a divider for odd index
mLayoutInflater.inflate(R.layout.widget_list_divider, row);
} else {
// Add cell for even index
WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
R.layout.widget_cell, row, false);
// set up touch.
widget.setOnClickListener(mIconClickListener);
widget.setOnLongClickListener(mIconLongClickListener);
row.addView(widget);
}
}
} else if (expectedChildCount < childCount) {
for (int i = expectedChildCount ; i < childCount; i++) {
row.getChildAt(i).setVisibility(View.GONE);
}
}
// Bind the views in the application info section.
holder.title.applyFromPackageItemInfo(entry.pkgItem);
// Bind the view in the widget horizontal tray region.
for (int i=0; i < infoList.size(); i++) {
WidgetCell widget = (WidgetCell) row.getChildAt(2*i);
widget.applyFromCellItem(infoList.get(i), mWidgetPreviewLoader);
widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
widget.ensurePreview();
widget.setVisibility(View.VISIBLE);
if (i > 0) {
row.getChildAt(2*i - 1).setVisibility(View.VISIBLE);
}
}
}
以单个应用进行分组 作为一级菜单,一个应用可能多个 wideget 作为二级菜单,用WidgetCell作为容器放置于HorizontalScrollView中 横向滚动;for 循环中 获取每一个WidgetCell调用applyFromCellItem() 和 ensurePreview() 。
-
WidgetCell 作为单个widget显示的容器,applyFromCellItem()设置数据,ensurePreview()中加载预览图
image.png
public void applyFromCellItem(WidgetItem item, WidgetPreviewLoader loader) {
mItem = item;
mWidgetName.setText(mItem.label);
mWidgetDims.setText(getContext().getString(R.string.widget_dims_format,
mItem.spanX, mItem.spanY));
mWidgetDims.setContentDescription(getContext().getString(
R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
mWidgetPreviewLoader = loader;
if (item.activityInfo != null) {
setTag(new PendingAddShortcutInfo(item.activityInfo));
} else {
setTag(new PendingAddWidgetInfo(item.widgetInfo));
}
}
public void ensurePreview() {
if (mActiveRequest != null) {
return;
}
mActiveRequest = mWidgetPreviewLoader.getPreview(
mItem, mPresetPreviewSize, mPresetPreviewSize, this);
}
- 其中 预览图 交由WidgetPreviewLoader类中处理 ,其中PreviewLoadTask 是一个 AsyncTask ,从缓存池中获取 空白的bitmap ,从数据库中获取预览图数据 赋值给 空白的bitmap ,最后调用 mCaller.applyPreview(preview);
public CancellationSignal getPreview(WidgetItem item, int previewWidth,
int previewHeight, WidgetCell caller) {
String size = previewWidth + "x" + previewHeight;
WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller);
task.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
CancellationSignal signal = new CancellationSignal();
signal.setOnCancelListener(task);
return signal;
}
public class PreviewLoadTask extends AsyncTask
implements CancellationSignal.OnCancelListener {
@Thunk final WidgetCacheKey mKey;
private final WidgetItem mInfo;
private final int mPreviewHeight;
private final int mPreviewWidth;
private final WidgetCell mCaller;
private final BaseActivity mActivity;
@Thunk long[] mVersions;
@Thunk Bitmap mBitmapToRecycle;
PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth,
int previewHeight, WidgetCell caller) {
mKey = key;
mInfo = info;
mPreviewHeight = previewHeight;
mPreviewWidth = previewWidth;
mCaller = caller;
mActivity = BaseActivity.fromContext(mCaller.getContext());
if (DEBUG) {
Log.d(TAG, String.format("%s, %s, %d, %d",
mKey, mInfo, mPreviewHeight, mPreviewWidth));
}
}
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap unusedBitmap = null;
// If already cancelled before this gets to run in the background, then return early
if (isCancelled()) {
return null;
}
synchronized (mUnusedBitmaps) {
// Check if we can re-use a bitmap
for (Bitmap candidate : mUnusedBitmaps) {
if (candidate != null && candidate.isMutable() &&
candidate.getWidth() == mPreviewWidth &&
candidate.getHeight() == mPreviewHeight) {
unusedBitmap = candidate;
mUnusedBitmaps.remove(unusedBitmap);
break;
}
}
}
// creating a bitmap is expensive. Do not do this inside synchronized block.
if (unusedBitmap == null) {
unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888);
}
// If cancelled now, don't bother reading the preview from the DB
if (isCancelled()) {
return unusedBitmap;
}
Bitmap preview = readFromDb(mKey, unusedBitmap, this);
// Only consider generating the preview if we have not cancelled the task already
if (!isCancelled() && preview == null) {
// Fetch the version info before we generate the preview, so that, in-case the
// app was updated while we are generating the preview, we use the old version info,
// which would gets re-written next time.
boolean persistable = mInfo.activityInfo == null
|| mInfo.activityInfo.isPersistable();
mVersions = persistable ? getPackageVersion(mKey.componentName.getPackageName())
: null;
// it's not in the db... we need to generate it
preview = generatePreview(mActivity, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight);
}
return preview;
}
@Override
protected void onPostExecute(final Bitmap preview) {
mCaller.applyPreview(preview);
// Write the generated preview to the DB in the worker thread
if (mVersions != null) {
mWorkerHandler.post(new Runnable() {
@Override
public void run() {
if (!isCancelled()) {
// If we are still using this preview, then write it to the DB and then
// let the normal clear mechanism recycle the bitmap
writeToDb(mKey, mVersions, preview);
mBitmapToRecycle = preview;
} else {
// If we've already cancelled, then skip writing the bitmap to the DB
// and manually add the bitmap back to the recycled set
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(preview);
}
}
}
});
} else {
// If we don't need to write to disk, then ensure the preview gets recycled by
// the normal clear mechanism
mBitmapToRecycle = preview;
}
}
@Override
protected void onCancelled(final Bitmap preview) {
// If we've cancelled while the task is running, then can return the bitmap to the
// recycled set immediately. Otherwise, it will be recycled after the preview is written
// to disk.
if (preview != null) {
mWorkerHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(preview);
}
}
});
}
}
@Override
public void onCancel() {
cancel(true);
// This only handles the case where the PreviewLoadTask is cancelled after the task has
// successfully completed (including having written to disk when necessary). In the
// other cases where it is cancelled while the task is running, it will be cleaned up
// in the tasks's onCancelled() call, and if cancelled while the task is writing to
// disk, it will be cancelled in the task's onPostExecute() call.
if (mBitmapToRecycle != null) {
mWorkerHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mUnusedBitmaps) {
mUnusedBitmaps.add(mBitmapToRecycle);
}
mBitmapToRecycle = null;
}
});
}
}
}
public void applyPreview(Bitmap bitmap) {
if (mApplyBitmapDeferred) {
mDeferredBitmap = bitmap;
return;
}
if (bitmap != null) {
mWidgetImage.setBitmap(bitmap,
DrawableFactory.get(getContext()).getBadgeForUser(mItem.user, getContext()));
if (mAnimatePreview) {
mWidgetImage.setAlpha(0f);
ViewPropertyAnimator anim = mWidgetImage.animate();
anim.alpha(1.0f).setDuration(FADE_IN_DURATION_MS);
} else {
mWidgetImage.setAlpha(1f);
}
}
}
自此,整个 widget的 预览列表数据加载完成。