diff options
Diffstat (limited to 'core/java/android/appwidget/AppWidgetHostView.java')
-rw-r--r-- | core/java/android/appwidget/AppWidgetHostView.java | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java new file mode 100644 index 0000000..be0f96e --- /dev/null +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.appwidget; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.util.Config; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.widget.FrameLayout; +import android.widget.RemoteViews; +import android.widget.TextView; + +/** + * Provides the glue to show AppWidget views. This class offers automatic animation + * between updates, and will try recycling old views for each incoming + * {@link RemoteViews}. + */ +public class AppWidgetHostView extends FrameLayout { + static final String TAG = "AppWidgetHostView"; + static final boolean LOGD = false; + static final boolean CROSSFADE = false; + + static final int VIEW_MODE_NOINIT = 0; + static final int VIEW_MODE_CONTENT = 1; + static final int VIEW_MODE_ERROR = 2; + static final int VIEW_MODE_DEFAULT = 3; + + static final int FADE_DURATION = 1000; + + // When we're inflating the initialLayout for a AppWidget, we only allow + // views that are allowed in RemoteViews. + static final LayoutInflater.Filter sInflaterFilter = new LayoutInflater.Filter() { + public boolean onLoadClass(Class clazz) { + return clazz.isAnnotationPresent(RemoteViews.RemoteView.class); + } + }; + + Context mContext; + + int mAppWidgetId; + AppWidgetProviderInfo mInfo; + View mView; + int mViewMode = VIEW_MODE_NOINIT; + int mLayoutId = -1; + long mFadeStartTime = -1; + Bitmap mOld; + Paint mOldPaint = new Paint(); + + /** + * Create a host view. Uses default fade animations. + */ + public AppWidgetHostView(Context context) { + this(context, android.R.anim.fade_in, android.R.anim.fade_out); + } + + /** + * Create a host view. Uses specified animations when pushing + * {@link #updateAppWidget(RemoteViews)}. + * + * @param animationIn Resource ID of in animation to use + * @param animationOut Resource ID of out animation to use + */ + public AppWidgetHostView(Context context, int animationIn, int animationOut) { + super(context); + mContext = context; + } + + /** + * Set the AppWidget that will be displayed by this view. + */ + public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { + mAppWidgetId = appWidgetId; + mInfo = info; + } + + public int getAppWidgetId() { + return mAppWidgetId; + } + + public AppWidgetProviderInfo getAppWidgetInfo() { + return mInfo; + } + + /** + * Process a set of {@link RemoteViews} coming in as an update from the + * AppWidget provider. Will animate into these new views as needed. + */ + public void updateAppWidget(RemoteViews remoteViews) { + if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); + + boolean recycled = false; + View content = null; + Exception exception = null; + + // Capture the old view into a bitmap so we can do the crossfade. + if (CROSSFADE) { + if (mFadeStartTime < 0) { + if (mView != null) { + final int width = mView.getWidth(); + final int height = mView.getHeight(); + try { + mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + } catch (OutOfMemoryError e) { + // we just won't do the fade + mOld = null; + } + if (mOld != null) { + //mView.drawIntoBitmap(mOld); + } + } + } + } + + if (remoteViews == null) { + if (mViewMode == VIEW_MODE_DEFAULT) { + // We've already done this -- nothing to do. + return; + } + content = getDefaultView(); + mLayoutId = -1; + mViewMode = VIEW_MODE_DEFAULT; + } else { + int layoutId = remoteViews.getLayoutId(); + + // If our stale view has been prepared to match active, and the new + // layout matches, try recycling it + if (content == null && layoutId == mLayoutId) { + try { + remoteViews.reapply(mContext, mView); + content = mView; + recycled = true; + if (LOGD) Log.d(TAG, "was able to recycled existing layout"); + } catch (RuntimeException e) { + exception = e; + } + } + + // Try normal RemoteView inflation + if (content == null) { + try { + content = remoteViews.apply(mContext, this); + if (LOGD) Log.d(TAG, "had to inflate new layout"); + } catch (RuntimeException e) { + exception = e; + } + } + + mLayoutId = layoutId; + mViewMode = VIEW_MODE_CONTENT; + } + + if (content == null) { + if (mViewMode == VIEW_MODE_ERROR) { + // We've already done this -- nothing to do. + return ; + } + Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception); + content = getErrorView(); + mViewMode = VIEW_MODE_ERROR; + } + + if (!recycled) { + prepareView(content); + addView(content); + } + + if (mView != content) { + removeView(mView); + mView = content; + } + + if (CROSSFADE) { + if (mFadeStartTime < 0) { + // if there is already an animation in progress, don't do anything -- + // the new view will pop in on top of the old one during the cross fade, + // and that looks okay. + mFadeStartTime = SystemClock.uptimeMillis(); + invalidate(); + } + } + } + + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (CROSSFADE) { + int alpha; + int l = child.getLeft(); + int t = child.getTop(); + if (mFadeStartTime > 0) { + alpha = (int)(((drawingTime-mFadeStartTime)*255)/FADE_DURATION); + if (alpha > 255) { + alpha = 255; + } + Log.d(TAG, "drawChild alpha=" + alpha + " l=" + l + " t=" + t + + " w=" + child.getWidth()); + if (alpha != 255 && mOld != null) { + mOldPaint.setAlpha(255-alpha); + //canvas.drawBitmap(mOld, l, t, mOldPaint); + } + } else { + alpha = 255; + } + int restoreTo = canvas.saveLayerAlpha(l, t, child.getWidth(), child.getHeight(), alpha, + Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); + boolean rv = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(restoreTo); + if (alpha < 255) { + invalidate(); + } else { + mFadeStartTime = -1; + if (mOld != null) { + mOld.recycle(); + mOld = null; + } + } + return rv; + } else { + return super.drawChild(canvas, child, drawingTime); + } + } + + /** + * Prepare the given view to be shown. This might include adjusting + * {@link FrameLayout.LayoutParams} before inserting. + */ + protected void prepareView(View view) { + // Take requested dimensions from parent, but apply default gravity. + ViewGroup.LayoutParams requested = view.getLayoutParams(); + if (requested == null) { + requested = new FrameLayout.LayoutParams(LayoutParams.FILL_PARENT, + LayoutParams.FILL_PARENT); + } + + FrameLayout.LayoutParams params = + new FrameLayout.LayoutParams(requested.width, requested.height); + params.gravity = Gravity.CENTER; + view.setLayoutParams(params); + } + + /** + * Inflate and return the default layout requested by AppWidget provider. + */ + protected View getDefaultView() { + View defaultView = null; + Exception exception = null; + + try { + if (mInfo != null) { + Context theirContext = mContext.createPackageContext( + mInfo.provider.getPackageName(), 0 /* no flags */); + LayoutInflater inflater = (LayoutInflater) + theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater = inflater.cloneInContext(theirContext); + inflater.setFilter(sInflaterFilter); + defaultView = inflater.inflate(mInfo.initialLayout, this, false); + } else { + Log.w(TAG, "can't inflate defaultView because mInfo is missing"); + } + } catch (PackageManager.NameNotFoundException e) { + exception = e; + } catch (RuntimeException e) { + exception = e; + } + + if (exception != null && LOGD) { + Log.w(TAG, "Error inflating AppWidget " + mInfo, exception); + } + + if (defaultView == null) { + if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error"); + defaultView = getErrorView(); + } + + return defaultView; + } + + /** + * Inflate and return a view that represents an error state. + */ + protected View getErrorView() { + TextView tv = new TextView(mContext); + tv.setText(com.android.internal.R.string.gadget_host_error_inflating); + // TODO: get this color from somewhere. + tv.setBackgroundColor(Color.argb(127, 0, 0, 0)); + return tv; + } +} |