0

How to set custom time to Android AnalogClock placed in app widget?

As an alternative I was thinking to override default AnalogClock to set the time through codes. Or in other words, I would create my custom Clock which extends from default View or AnalogClock. Then put my custom Clock on widget layout UI.

Is this possible? I'm afraid we are limited on RemoteViews to have our own custom Component.

UPDATE: This is error log I have following the solution given by vArDo at first.

enter image description here

vArDo
  • 4,408
  • 1
  • 17
  • 26
stuckedunderflow
  • 3,551
  • 8
  • 46
  • 63

2 Answers2

6

Intro

It's not possible to include custom views in app widgets. According to documentation only small predefined subset of views can be used in layout. So, as you've mentioned, it's not possible to create your own AnalogClock view (as I've shown in my other answer) and include it in app widget.

However there are other means that can be used to create custom AnalogClock in app widget. In such implementation setting time directly in AnalogClock should not be a problem.

Short answer

One of the ways to accomplish this is draw custom AnalogClock into ImageView (which is one of the allowed views in app widgets). Repeating PendingIntent is using Service to draw into ImageView every 60 seconds (via RemoteViews).

Note on battery usage

Keep in mind that invoking Service action to update app widget via RemoteViews every 60 seconds is not so lightweight on battery. On Jelly Bean example implementation used 2% of battery during night (screen off, average network strength). However, I've been updating the screen every 15 seconds, which is not necessary when it comes to analog clocks. So a rough estimate would be that an app widget with one update per minute will consume about 0.5% of battery juice during night - this might be significant for your users.

Solution details

Changes drawing service

First of all you'll have to create Service that will draw into ImageView present in your view. Drawing code is pretty much taken from AnalogClock implementation and rearrange according to needs. First of all, there are no checks on whether time has changed - this is because Service will be invoked only when we decide too (e.g. every 60 seconds). Seconds change is that we create Drawables from resources in the method.

Another change is that code draws analog clock into local Bitmap using Canvas:

    // Creating Bitmap and Canvas to which analog clock will be drawn.
    Bitmap appWidgetBitmap = Bitmap.createBitmap(availableWidth, availableHeight, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(appWidgetBitmap);

Update is drawn into ImageView via RemoteViews. Changes are aggregated and then pushed into app widget on home screen - see docs for details). In our case there is only one change, and it is done through special method:

    remoteViews.setImageViewBitmap(R.id.imageView1, appWidgetBitmap);

Bare in mind that for code to work you have to declare what is available space for drawing your analog clock. Based on what is declare in your widget_provider.xml, you can determine the dimensions of ImageView. Note that you'll have to convert dp unit to px unit, before drawing:

    // You'll have to determine tour own dimensions of space to which analog clock is drawn.
    final int APP_WIDGET_WIDTH_DP = 210; // Taken from widget_provider.xml: android:minWidth="210dp"
    final int APP_WIDGET_HEIGHT_DP = 210; // Taken from widget_provider.xml: android:minHeight="210dp"

    final int availableWidth = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_WIDGET_WIDTH_DP, r.getDisplayMetrics()) + 0.5f);
    final int availableHeight = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_WIDGET_HEIGHT_DP, r.getDisplayMetrics()) + 0.5f);

I've pasted full code of Service subclass here.

Also at the top you'll find the code that is responsible for setting time that will be displayed - modify as needed:

    mCalendar = new Time();

    // Line below sets time that will be displayed - modify if needed.
    mCalendar.setToNow();

    int hour = mCalendar.hour;
    int minute = mCalendar.minute;
    int second = mCalendar.second;

    mMinutes = minute + second / 60.0f;
    mHour = hour + mMinutes / 60.0f;

Also don't forget to declare this Service and app widget in your AndroidManifest.xml file, e.g.:

    <receiver 
        android:name="TimeSettableAnalogClockAppWidget"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name">

        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            <action android:name="android.appwidget.action.APPWIDGET_DISABLED"/>
        </intent-filter>

        <meta-data 
            android:name="android.appwidget.provider"
            android:resource="@xml/widget_provider"/>            

    </receiver>
    <service android:name="TimeSettableAnalogClockService"/>

Most important info about using services to update app widget is described shortly in this blog post.

Invoking drawing

Scheduling updates is done in onUpdate() method of your AppWidgetProvider subclass - the on that is declare in your AndroidManifest.xml file. We'll also need to remove PendingIntent when app widget is removed from home screen. This is done in overridden onDisabled() method. Remember to declare both actions that will call one of each methods: APPWIDGET_UPDATE and APPWIDGET_DISABLED. See excerpt from AndroidManifest.xml above.

package com.example.anlogclocksettimeexample;

import java.util.Calendar;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;

public class TimeSettableAnalogClockAppWidget extends AppWidgetProvider {

    private PendingIntent service = null;

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
//      super.onUpdate(context, appWidgetManager, appWidgetIds);
        final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

        final Calendar time = Calendar.getInstance();
        time.set(Calendar.SECOND, 0);
        time.set(Calendar.MINUTE, 0);
        time.set(Calendar.HOUR, 0);

        final Intent intent = new Intent(context, TimeSettableAnalogClockService.class);

        if (service == null) {          
            service = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
        }

        alarmManager.setRepeating(AlarmManager.RTC, time.getTime().getTime(), 1000 * 60 /* ms */, service);
    }

    @Override
    public void onDisabled(Context context) {
        final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(service);
    }
}

Last line of onUpdate() method schedules a repeating event to happen every 60 seconds, with first update to happen right away.

Drawing AnalogClock once only

Using above code the simplest way to invoke service only once is to schedule single event in time instead of repeating one. Simple replace alarmManager.setRepeating() call with following line:

alarmManager.set(AlarmManager.RTC, time.getTime().getTime(), service);

Results

Below is a screen shot with custom AnalogClock app widget that had time set only once (note difference between analog clock time and time shown in status bar):

enter image description here

vArDo
  • 4,408
  • 1
  • 17
  • 26
  • Yes I like this answer. And I do not need to refresh the clock every minute. Because I want to show fixed time on this custom clock. I would also like to know how you managed to update the screen every 15 secs to save the battery life. – stuckedunderflow Aug 13 '12 at 23:32
  • @Halim I've added details on solution. I've also describe how to modify code so that `Service` is invoke only once (this per your comment). However, I don't know what you mean talking about battery saving :) What I wanted to point out is that update every 15 seconds is more battery draining that update every 60 seconds which is pretty natural :) The only reason I wrote about 15 seconds interval is because this is what I had tried on my phone. Just wanted to show how does this impact battery life on phone. – vArDo Aug 15 '12 at 10:21
2

There's no way to set time to something other than current time in AnalogClock. This widget is very encapsulated. You can easily change how it looks (in XML using attributes from R.styleable, but there's no easy way to change its behavior.

I'd probably go with making copy of AnalogClock.java (might be a good idea to put it in separate package, e.g. com.example.widget) and local copies of drawables clock_dial, clock_hand_hour, clock_hand_minute (NOTE: to get best results you'll have to create local copies for all densities: ldpi, mdpi, hdpi and xhdpi). With these in place and small changes to code, you'll be able to get the bahavior you want.

Using custom drawables WITH attributes settable from XML layout

To be able to set dial, hand_hour and hand_minute drawables from XML layout files, you need to declare those attributes as styleable for your custom widget.

NOTE: In following example I've created TimeSettableAnalogClock and placed it in com.example.widget package.

Create res/values/attrs.xml file with following content:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TimeSettableAnalogClock">        
        <attr name="dial" format="reference"/>
        <attr name="hand_hour" format="reference"/>
        <attr name="hand_minute" format="reference"/>
    </declare-styleable>
</resources>

format=reference means that values can only be references to other existing elements, e.g. drawables (@drawable/some_custom_dial).

Now custom attributes can be used in XML layout file:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/lib/com.example.widget"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.widget.TimeSettableAnalogClock
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"        
        custom:dial="@drawable/some_custom_clock_dial"
        custom:hand_hour="@drawable/some_custom_clock_hand_hour"
        custom:hand_minute="@drawable/some_custom_clock_hand_minute"/>

</RelativeLayout>

Finally, you'll need to handle these custom attributes when TimeSettableAnalogClock object is created:

public TimeSettableAnalogClock(Context context, AttributeSet attrs,
                   int defStyle) {
    super(context, attrs, defStyle);
    Resources r = getContext().getResources();
    TypedArray a =
            context.obtainStyledAttributes(
                    attrs, R.styleable.TimeSettableAnalogClock, defStyle, 0);

    mDial = a.getDrawable(R.styleable.TimeSettableAnalogClock_dial);
    if (mDial == null) {
        mDial = r.getDrawable(R.drawable.clock_dial);
    }

    mHourHand = a.getDrawable(R.styleable.TimeSettableAnalogClock_hand_hour);
    if (mHourHand == null) {
        mHourHand = r.getDrawable(R.drawable.clock_hand_hour);
    }

    mMinuteHand = a.getDrawable(R.styleable.TimeSettableAnalogClock_hand_minute);
    if (mMinuteHand == null) {
        mMinuteHand = r.getDrawable(R.drawable.clock_hand_minute);
    }

    mCalendar = new Time();

    mDialWidth = mDial.getIntrinsicWidth();
    mDialHeight = mDial.getIntrinsicHeight();
}

Note, I've pretty much only removed com.android.internal. from reference to any drawable or styleable. The rest is the same as in AnalogClock constructor.

Using custom drawables WITHOUT attributes settable from XML layout

If you don't need your attributes to be settable through XML (e.g. all your analog clock will look the same in every place), you can simply the solution. You'll only need local copies of drawables (as mentioned at the beginning), no attrs.xml file is needed. Your constructor should look like this:

public TimeSettableAnalogClock(Context context, AttributeSet attrs,
                   int defStyle) {
    super(context, attrs, defStyle);
    Resources r = getContext().getResources();

    mDial = r.getDrawable(R.drawable.clock_dial);
    mHourHand = r.getDrawable(R.drawable.clock_hand_hour);
    mMinuteHand = r.getDrawable(R.drawable.clock_hand_minute);

    mCalendar = new Time();

    mDialWidth = mDial.getIntrinsicWidth();
    mDialHeight = mDial.getIntrinsicHeight();
}

As you can see, much shorter. The usage in XML layout is the same, but you won't need xmlns:custom part, and of course you can't set custom attributes anymore:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.widget.TimeSettableAnalogClock
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>

I can update my answer further with more code of proposed solution, if you need more help on this one.

vArDo
  • 4,408
  • 1
  • 17
  • 26
  • I tried this solution. I copied AnalogClock source code from Android and put it on my own package. But I stuck on com.android.internal.R.styleable I don't know where to find this on Android source codes. Can you help? – stuckedunderflow Aug 02 '12 at 02:34
  • @Halim The `com.android.internal.R.styleable` is used to handle attributes set in XML for `AnalogClock` (e.g. `dial` or `hand_hour`). If you want to handle such attributes in your custom analog clock widget, you'll have to declare them as styleable. I've updated my answer to show how to do this. I've also added part on how to simplify widget code if you don't need this feature (i.e. widget custom drawables set only in constructor). – vArDo Aug 02 '12 at 07:40
  • thanks a lot for your codes above. However, I still have some questions. The new class TimeSettableAnalogClock does it extends from View or AnalogClock? If it extends from View, do we need to override method like onAttachedToWindow(), onDetachedFromWindow(), onMeasure(int widthMeasureSpec, int heightMeasureSpec), onSizeChanged(int w, int h, int oldw, int oldh), onDraw(Canvas canvas), onTimeChanged(), etc? – stuckedunderflow Aug 03 '12 at 03:55
  • And I'm still having problem on android.view.InflateException: Binary XML file line #24: Error inflating class my.package.com.TimeSettableAnalogClock Caused by: java.lang.ClassNotFoundException: – stuckedunderflow Aug 03 '12 at 04:00
  • 1
    @Halim Your `TimeSettableAnalogClock.java` should be **exact** copy of `AnalogClock.java`, except for changes mentioned in my answer. Which mneans **1.** Replace all *AnalogClock* strings with *TimeSettableAnalogClock* string. **2.** You need all three constructors (not only the one with three parameters) - this should fix your inflate error **3.** New class extends `View` **4.** You need to override all the methods you've mentioned (simply leave them as they are in `AnalogClock.java`). – vArDo Aug 03 '12 at 07:37
  • Tried as you mentioned above but still having problem on on android.view.InflateException: Binary XML file line #24: Error inflating class my.package.com.TimeSettableAnalogClock Caused by: java.lang.ClassNotFoundException :( Yesterday I found http://www.kevindion.com/2011/01/custom-xml-attributes-for-android-widgets/ which has a good tutorial explaining how to call custom component on xml but today the site is down. – stuckedunderflow Aug 03 '12 at 08:17
  • @Halim Can you put your whole `TimeSettableAnalogClock.java` somewhere, e.g. on [pastebin.com](http://pastebin.com). Also how do you use this class? Please post your layout XML on pastebin. I have a working project created as I've explained above, so this must be some kind of small mistake in your code. – vArDo Aug 03 '12 at 09:01
  • 1
    @Halim As, of the site you've mentioned - you can always use [Google cache](http://webcache.googleusercontent.com/search?q=cache%3Awww.kevindion.com%2F2011%2F01%2Fcustom-xml-attributes-for-android-widgets%2F&sugexp=chrome,mod=4&sourceid=chrome&ie=UTF-8) :) – vArDo Aug 03 '12 at 09:11
  • http://pastebin.com/0bAGZdn2 http://pastebin.com/NfwURKK0 http://pastebin.com/0z5HAHq4 Can you please take a look? I'm desperate and cannot think clearly already. Perhaps you could help me point out something missing. – stuckedunderflow Aug 03 '12 at 09:15
  • @Halim after quickview. In your layout XML file change line `xmlns:custom="http://schemas.android.com/apk/res/upper.duper.widget.alarmclock.full"` to `xmlns:custom="http://schemas.android.com/apk/lib/upper.duper.widget.alarmclock.full"`. Note the change from **res** to **lib**. Check if this helps. – vArDo Aug 03 '12 at 09:29
  • @Halim Another hint: uncomment lines 175-176 changing all attributes to method, e.g. `mRight` to `mRight()`, `mTop` to `mTop()`. Also why are lines 156-157 commented out? – vArDo Aug 03 '12 at 09:44
  • namespace changes from res to lib does not impact anything. Still not working and the same error message. I cannot change mRight to mRight() instead I could do getRight(). Is that what you mean? And about line 156 - 157 commented out because the method resolveSizeAndState() is undefined. And on earlier version of AnalogClock they dont have these lines, so I think it should not a problem to comment it. – stuckedunderflow Aug 03 '12 at 15:44
  • @Halim Yes, this is want I meant: `getRight()` and `getTop()`. Does `Eclipse` indicate any errors in `.xml` files? (Please `Clean` project and check for *red* indicator in `Package Explorer` view) Also checkout `Error Log` view in `Eclipse` - it sometimes shows additional info. – vArDo Aug 03 '12 at 15:49
  • @Halim Also, I think that calling `setMeasuredDimension()` (lines 156-157) in `onMeasure()` is necessary (see docs for this method). What API version are you targeting? Lower than API 11? If so, please use [following lines](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3_r1/android/widget/AnalogClock.java/#149) - these use `resolveSize()` method. – vArDo Aug 03 '12 at 16:05
  • No errors during clean and compile. Just runtime error. LogCat shows the above message error. Arrghh...haha I'm stressed. – stuckedunderflow Aug 03 '12 at 16:07
  • I updated the code accordingly to the link you gave. I copy and paste CustomAlarmClock with AnalogClock replace the class name and made necessary adjustments. http://pastebin.com/0JcqL3Q1 And still giving me the error. I also tried solution at http://stackoverflow.com/questions/2376250/custom-fonts-and-xml-layouts-android and the same error happened. So I think something small is missing here. I don't know what. – stuckedunderflow Aug 03 '12 at 16:40
  • Anything extra if this is to do on widget? http://stackoverflow.com/questions/9239772/extend-linearlayout-for-app-widget says descendants is not supported – stuckedunderflow Aug 03 '12 at 18:15
  • https://groups.google.com/forum/?fromgroups#!topic/android-developers/AEe1lu1M0Zs also says is not possible. So how you manage to do it? Can you share? – stuckedunderflow Aug 03 '12 at 18:19
  • @Halim You never mentioned that you want to place your `AnalogClock` in **App** Widget. :) I thought that you just want to create your own `android.` **`widget`** `.AnalogClock`. So, I never tried placing it in app widget - I've placed customized `AnalogClock` in normal `Activity`. I'll try to do, what you want to achieve, later. Maybe, in fact, it's impossible (due to security reasons), as is mentioned in one of the links that you've provided. – vArDo Aug 04 '12 at 12:07
  • Haha... so we had misunderstanding. Ok I will wait you try what I want to achieve as you said. Then you put it on another answer. I will accept your answer no matter the result is. Just to make this case closed. – stuckedunderflow Aug 04 '12 at 13:46
  • @Halim I've been able to create custom `AnalogClock` app widget by drawing into `ImageView` every 60 seconds (`AlarmManager` used to schedule `PendingIntent`). See if this is acceptable solution for you - I'll add details if you're interested. – vArDo Aug 13 '12 at 21:11
  • Yes absolutely. I'm interested to know how to draw hour and minute into ImageView. Could you share something? I would appreciate it. – stuckedunderflow Aug 13 '12 at 23:24