The process of app widgets creation is quite common and described in official docs very well. Google uses almost all the features from this guide in the Google Calendar that offers a few widgets with lists of scheduled events for nearest time.
Source code for this application is available in Google Git for everybody and it has the free license. But if you want to extend the functions and use your server API, you may encounter with some problems.
In this article I will try to write some code for widget with a list of images using RemoteViewsFactory. App widget will load the data about my meetings for the next week.
First of all, you should be familiar with appwidget’s Google guide. Second, create your project and configure app widget as usual. Then let’s write some code for AppWidgetProvider:
public class AppWidgetProvider extends android.appwidget.AppWidgetProvider
{
private static String APP_WIDGET_UPDATE = "your.package.name.MY_APPWIDGET_UPDATE";
public static final long UPDATE_TIME_MILLIS = 15 * 60 * 1000; // 15 minutes
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetIds[])
{
final int N = appWidgetIds.length;
for (int i = 0; i < N; i++)
{
int appWidgetId = appWidgetIds[i];
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget);
appWidgetManager.updateAppWidget(appWidgetId, views);
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
public static void updateAppWidget(Context context,
AppWidgetManager appWidgetManager,
int appWidgetId)
{
Intent updateIntent = new Intent(context, AppWidgetRefreshService.class);
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
updateIntent.setData(Uri.parse(updateIntent.toUri(Intent.URI_INTENT_SCHEME)));
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget);
// Calendar header
Calendar calendar = Calendar.getInstance();
//long millis = calendar.getTimeInMillis();
final String dayOfWeek = calendar.getDisplayName(Calendar.DAY_OF_WEEK,
Calendar.LONG, Locale.getDefault());
final String date = Utils.getReadableDate(calendar.getTime());
views.setTextViewText(R.id.day_of_week, dayOfWeek);
views.setTextViewText(R.id.date, date);
// Attach to list of events
views.setRemoteAdapter(R.id.events_list, updateIntent);
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.events_list);
// Launch calendar app when the user taps on the header
final Intent launchServiceIntent = new Intent(Intent.ACTION_VIEW);
launchServiceIntent.setClass(context, MainActivity.class);
final PendingIntent launchCalendarPendingIntent = PendingIntent.getActivity(
context, 0 /* no requestCode */, launchServiceIntent, 0 /* no flags */);
views.setOnClickPendingIntent(R.id.header, launchCalendarPendingIntent);
// Each list item will call setOnClickExtra() to let the list know
// which item
// is selected by a user.
final PendingIntent updateEventIntent = getLaunchPendingIntentTemplate(context);
views.setPendingIntentTemplate(R.id.events_list, updateEventIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
/**
* Build a {@link PendingIntent} to launch the Calendar app. This should be used
* in combination with {@link RemoteViews#setPendingIntentTemplate(int, PendingIntent)}.
*/
public static PendingIntent getLaunchPendingIntentTemplate(Context context)
{
Intent launchIntent = new Intent();
launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_TASK_ON_HOME);
launchIntent.setData(Uri.parse(launchIntent.toUri(Intent.URI_INTENT_SCHEME)));
launchIntent.setClass(context, OnItemClickActivity.class);
return PendingIntent.getActivity(context, 0 , launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
private static PendingIntent createClockTickIntent(Context context)
{
Intent intent = new Intent(APP_WIDGET_UPDATE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
return pendingIntent;
}
public static void startAppWidgetUpdater(Context context)
{
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.setRepeating(AlarmManager.RTC,
System.currentTimeMillis(),// start with now
UPDATE_TIME_MILLIS,
createClockTickIntent(context));
}
public static boolean isUpdaterStarted(Context context)
{
return (PendingIntent.getBroadcast(context, 0, new Intent(APP_WIDGET_UPDATE),
PendingIntent.FLAG_NO_CREATE) != null);
}
@Override
public void onEnabled(Context context)
{
startAppWidgetUpdater(context);
super.onEnabled(context);
}
@Override
public void onDisabled(Context context)
{
AlarmManager alarmManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(createClockTickIntent(context));
super.onDisabled(context);
}
@Override
public void onReceive(Context context, Intent intent)
{
if (APP_WIDGET_UPDATE.equals(intent.getAction()))
{
// Get the widget manager and ids for this widget provider, then
// call the shared
// clock update method.
ComponentName thisAppWidget = getComponentName(context);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int ids[] = appWidgetManager.getAppWidgetIds(thisAppWidget);
for (int appWidgetID : ids)
{
updateAppWidget(context, appWidgetManager, appWidgetID);
}
}
else
super.onReceive(context, intent);
}
/**
* Build {@link ComponentName} describing this specific
* {@link android.appwidget.AppWidgetProvider}
*/
public static ComponentName getComponentName(Context context)
{
return new ComponentName(context, AppWidgetProvider.class);
}
}
Android does not allow us to use widget update time less than 60 minutes. So we use AlarmManager to refresh app widget with the period we need. AlarmManager helps to save the battery and in some cases can avoid unnecessary call of expensive app widget refresh methods.
Note: always set the update frequency as less as you can!
This code creates header for widget. The header contains today’s day of week, date and starts MainActivity on click. Also we specified PendingIntent template that we will use for launching OnItemClickActivity when user touches list item.
appwidget.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:focusable="true"
android:clickable="true"
>
<!-- Header -->
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="56dip"
android:gravity="center_vertical|start"
android:orientation="vertical"
android:background="@drawable/widget_header_shape">
<TextView
android:id="@+id/day_of_week"
android:paddingLeft="8dip"
android:paddingRight="8dip"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:textColor="@color/appwidget_week"
android:shadowColor="#0dffffff"
android:shadowRadius="3"
style="@style/WidgetDayOfWeekStyle" />
<TextView
android:id="@+id/date"
android:paddingLeft="8dip"
android:paddingRight="8dip"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:textColor="@color/appwidget_month"
style="@style/WidgetDateStyle"/>
</LinearLayout>
<!-- Event list -->
<ListView
android:id="@+id/events_list"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:divider="@null"
android:listSelector="@android:color/transparent"
android:cacheColorHint="@null"
android:background="@android:color/white"/>
</LinearLayout>
Add some code to AndroidManifest.xml:
<receiver
android:name=".appwidget.AppWidgetProvider"
android:label="@string/appwidget_name_large"
android:enabled="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="your.package.name.MY_APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info" />
</receiver>
<service
android:name=".appwidget.AppWidgetRefreshService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false"/>
<receiver android:name=".appwidget.AppWidgetRefreshService$CalendarFactory"
android:permission="android.permission.BIND_REMOTEVIEWS">
<intent-filter>
<action android:name="your.package.name.MY_APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
Here we have one additional receiver for custom intent your.package.name.MY_APPWIDGET_UPDATE that we use for fire from AlarmManager.
Now all components are ready for custom RemoteViewsService that will contain a list of scheduled meetings from server.
public class AppWidgetRefreshService extends RemoteViewsService
{
private static final String TAG = "MY_APPWIDGET_UPDATE";
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent)
{
return new CalendarFactory(getApplicationContext(), intent);
}
public static class CalendarFactory extends BroadcastReceiver implements
RemoteViewsService.RemoteViewsFactory
{
private Context mContext;
private Resources mResources;
private int mAppWidgetId;
private int mStandardColor;
private int mFadedColor;
private static List<Event> queryResults = new ArrayList<>();
private LruCache<String, Bitmap> bitmapLruCache;
public CalendarFactory() {
// This is being created as part of onReceive
}
protected CalendarFactory(Context context, Intent intent)
{
mContext = context;
mResources = context.getResources();
mAppWidgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
mStandardColor = mResources.getColor(R.color.appwidget_item_standard_color);
mFadedColor = mResources.getColor(R.color.appwidget_item_declined_color);
initializeCache();
}
protected void initializeCache()
{
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/10th of the available memory for this memory cache.
final int cacheSize = maxMemory / 10;
bitmapLruCache = new LruCache<String, Bitmap>(cacheSize)
{
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
}
@Override
public void onCreate()
{
loadData();
}
@Override
public void onDataSetChanged()
{
Log.d(TAG, "onDataSetChanged");
}
@Override
public void onDestroy()
{
}
@Override
public RemoteViews getLoadingView()
{
RemoteViews views = new RemoteViews(mContext.getPackageName(),
R.layout.appwidget_loading);
return views;
}
@Override
public RemoteViews getViewAt(int position)
{
// we use getCount here so that it doesn't return null when empty
if (position < 0 || position >= getCount()) return null;
if (queryResults == null)
{
return new RemoteViews(mContext.getPackageName(),
R.layout.appwidget_loading);
}
if (queryResults.isEmpty())
{
return new RemoteViews(mContext.getPackageName(),
R.layout.appwidget_no_events);
}
final RemoteViews views;
views = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
final Event event = queryResults.get(position);
if(event == null) return views;
if(position == 0)
if(!Utils.dateOfSameDay(event.getDate(), new Date()))
updateTextView(views, R.id.date,
View.VISIBLE, Utils.getReadableDate(event.getDate()));
else
views.setViewVisibility(R.id.date, View.GONE);
else
{
Event prevEvent = queryResults.get(position - 1);
if(!Utils.dateOfSameDay(event.getDate(), prevEvent.getDate()))
updateTextView(views, R.id.date, View.VISIBLE,
Utils.getReadableDate(event.getDate()));
else
views.setViewVisibility(R.id.date, View.GONE);
}
if (event.getState() == Event.EventState.waiting)
views.setInt(R.id.widget_row, "setBackgroundColor",
mContext.getResources().getColor(event.isApprovedByMe()
? R.color.event_state_bg_yellow
: R.color.event_state_bg_red));
else
views.setInt(R.id.widget_row, "setBackgroundResource",
R.drawable.agenda_item_bg_primary);
updateTextView(views, R.id.when,
View.VISIBLE,
Utils.getReadableTime(event.getStartHour()));
updateTextView(views, R.id.title, View.VISIBLE, event.getTitle());
updateTextView(views, R.id.who, View.VISIBLE, event.getName());
updateImageView(views, R.id.avatar, event.getAvatar());
views.setInt(R.id.who, "setTextColor", mStandardColor);
views.setInt(R.id.when, "setTextColor", mStandardColor);
views.setInt(R.id.title, "setTextColor", mFadedColor);
final Intent eventViewIntent = getLaunchFillInIntent(
mContext, event.getEventId());
views.setOnClickFillInIntent(R.id.widget_row, eventViewIntent);
return views;
}
@Override
public int getViewTypeCount() {
return 3;
}
@Override
public int getCount()
{
// if there are no events, we still return 1 to represent the "no
// events" view
if (queryResults == null)
return 1;
return Math.max(1, queryResults.size());
}
@Override
public long getItemId(int position)
{
if(queryResults == null || queryResults.isEmpty()) return 0;
return queryResults.get(position).getEventId();//result;
}
@Override
public boolean hasStableIds() {
return true;
}
/**
* Load events from server
*/
public void loadData()
{
//fill the queryResults
}
void updateTextView(RemoteViews views, int id, int visibility, String string) {
views.setViewVisibility(id, visibility);
if (visibility == View.VISIBLE) {
views.setTextViewText(id, string);
}
}
void updateImageView(RemoteViews views, int resourceId, final String imageUrl)
{
Bitmap bitmap = bitmapLruCache.get(imageUrl);
if (bitmap == null)
bitmapLruCache.put(imageUrl, bitmap);
views.setImageViewBitmap(resourceId, bitmap);
}
@Override
public void onReceive(Context context, Intent intent)
{
mContext = context;
loadData();
}
}
/**
* Build an {@link Intent} available to launch the Calendar app.
* This should be used in combination with
* If the go to time is 0, then calendar will be launched without a starting time.
*
*/
static Intent getLaunchFillInIntent(Context context, int id)
{
Intent eventViewIntent = new Intent();
if (id != 0)
{
eventViewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_TASK_ON_HOME);
eventViewIntent.setClass(context, OnItemClickActivity.class);
}
else
{
// If we do not have an event id - start MainActivity
eventViewIntent.setClass(context, MainActivity.class);
}
return eventViewIntent;
}
}
First important thing in this code is caching drawables with LruCache. This approach helps us to fix weird issue with memory leak in list with images.
Second, in getViewAt method I realize grouping by the day in quite unusual manner:
if(position == 0)
if(!Utils.dateOfSameDay(event.getDate(), new Date()))
updateTextView(views, R.id.date,
View.VISIBLE, Utils.getReadableDate(event.getDate()));
else
views.setViewVisibility(R.id.date, View.GONE);
I think when you need just a divider between days without actual grouping them (for collapse, for example), this solution is quite quick and beautiful (but follows to some excessive TextViews on the other hand).
widget_item layout:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="72dp"
android:background="@drawable/bg_event_cal_widget_holo">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textSize="14sp"
android:ellipsize="marquee"
android:textColor="@color/appwidget_item_declined_color"
style="?android:attr/textAppearanceMediumInverse"
android:visibility="gone"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="5dp"
android:paddingBottom="8dp"
android:gravity="center_vertical"/>
<LinearLayout
android:id="@+id/widget_row"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingBottom="4dp"
android:paddingTop="4dp"
android:orientation="horizontal"
android:minHeight="72dp" >
<ImageView
android:id="@+id/avatar"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
android:layout_width="40dip"
android:layout_height="40dip"
android:scaleType="fitXY"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_launcher"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="16dip"
android:orientation="vertical">
<TextView
android:id="@+id/who"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:textSize="14sp"
android:textStyle="bold"
android:ellipsize="marquee"
android:textColor="@color/appwidget_title"
style="?android:attr/textAppearanceMediumInverse" />
<TextView
android:id="@+id/when"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textSize="12sp"
android:ellipsize="marquee"
android:textColor="@color/appwidget_when"
style="?android:attr/textAppearanceSmallInverse" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="marquee"
android:paddingBottom="2dip"
android:textSize="12sp"
android:textColor="@color/appwidget_where"
style="?android:attr/textAppearanceSmallInverse" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
Third, I intentionally keep the code that use my Event class fields with aim of showing you how to change some TextView and ImageView properties in RemoteViews. I believe the sense of this code is clear.
The result is: