-->

How to set time to Android AnalogClock in app widg

2020-08-05 09:59发布

问题:

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.

回答1:

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):



回答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.