如何在应用小部件中为 Android AnalogClock 设置时间?

Posted

技术标签:

【中文标题】如何在应用小部件中为 Android AnalogClock 设置时间?【英文标题】:How to set time to Android AnalogClock in app widget? 【发布时间】:2012-07-27 10:31:11 【问题描述】:

如何将自定义时间设置为android AnalogClock 放置在应用小部件中?

作为替代方案,我正在考虑覆盖默认的 AnalogClock 以通过代码设置时间。或者换句话说,我将创建从默认视图或模拟时钟扩展的自定义时钟。然后将我的自定义时钟放在小部件布局 UI 上。

这可能吗?恐怕我们受限于 RemoteViews 来拥有我们自己的自定义组件。

更新: 这是我最初遵循 vArDo 给出的解决方案的错误日志。

【问题讨论】:

【参考方案1】:

简介

无法在应用小部件中包含自定义视图。 According to documentation 在布局中只能使用一小部分预定义的视图子集。因此,正如您所提到的,您无法创建自己的 AnalogClock 视图(正如我在其他答案中所展示的那样)并将其包含在应用小部件中。

但是,还有其他方法可用于在应用小部件中创建自定义 AnalogClock。在这样的实现中直接在AnalogClock中设置时间应该没有问题。

简答

实现此目的的方法之一是将自定义AnalogClock 绘制到ImageView(这是应用小部件中允许的视图之一)。重复 PendingIntent 是使用 Service 每 60 秒绘制一次 ImageView(通过 RemoteViews)。

电池使用注意事项

请记住,每 60 秒通过RemoteViews 调用Service 操作以更新应用程序小部件 对电池的消耗并不那么轻。 Jelly Bean 示例实现在夜间使用了 2% 的电池(屏幕关闭,平均网络强度)。但是,我每 15 秒更新一次屏幕,这对于模拟时钟来说是不必要的。因此粗略估计,每分钟更新一次的应用小部件在夜间会消耗大约 0.5% 的电池电量 - 这对您的用户来说可能意义重大。

解决方案详情

更改绘图服务

首先,您必须创建Service,它将绘制到您视图中的ImageView。绘图代码几乎取自AnalogClock 实现并根据需要重新排列。首先,没有检查时间是否已更改 - 这是因为 Service 只有在我们也做出决定时才会被调用(例如每 60 秒)。秒变是我们从方法中的资源创建Drawables。

另一个变化是代码使用Canvas将模拟时钟引入本地Bitmap

    // 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);

通过RemoteViews 将更新引入ImageView。更改会被汇总,然后推送到主屏幕上的应用小部件中 - 有关详细信息,请参阅 docs)。在我们的例子中,只有一个变化,它是通过特殊方法完成的:

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

请记住,要使代码正常工作,您必须声明可用于绘制模拟时钟的可用空间。根据widget_provider.xml 中声明的内容,您可以确定ImageView 的尺寸。请注意,在绘图之前,您必须将dp 单位转换为px 单位:

    // 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);

我已经粘贴了Service子类here的完整代码。

您还可以在顶部找到负责设置显示时间的代码 - 根据需要进行修改:

    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;

另外不要忘记在您的AndroidManifest.xml 文件中声明此Service 和应用小部件,例如:

    <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"/>

this blog post 中简要介绍了有关使用服务更新应用小部件的最重要信息。

调用绘图

计划更新是在您的AppWidgetProvider 子类的onUpdate() 方法中完成的——在您的AndroidManifest.xml 文件中声明的方法。当应用小部件从主屏幕移除时,我们还需要移除 PendingIntent。这是在重写的onDisabled() 方法中完成的。请记住声明将调用每个方法之一的两个操作:APPWIDGET_UPDATEAPPWIDGET_DISABLED。请参阅上述AndroidManifest.xml 的摘录。

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

onUpdate() 方法的最后一行安排每 60 秒发生一次重复事件,并立即发生第一次更新。

只画一次AnalogClock

使用上面的代码只调用一次服务的最简单方法是及时安排单个事件,而不是重复一个。使用以下行简单替换 alarmManager.setRepeating() 调用:

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

结果

以下是自定义AnalogClock 应用小部件的屏幕截图,该小部件仅设置了一次时间(注意模拟时钟时间与状态栏中显示的时间之间的差异):

【讨论】:

是的,我喜欢这个答案。而且我不需要每分钟刷新时钟。因为我想在这个自定义时钟上显示固定时间。我还想知道您是如何设法每 15 秒更新一次屏幕以节省电池寿命的。 @Halim 我添加了有关解决方案的详细信息。我还描述了如何修改代码,以便 Service 仅调用一次(根据您的评论)。但是,我不知道您所说的省电是什么意思 :) 我想指出的是,每 15 秒更新一次会消耗更多电池电量,而每 60 秒更新一次,这很自然 :) 我写了大约 15 的唯一原因秒间隔是因为这是我在手机上尝试过的。只是想说明这对手机的电池寿命有何影响。【参考方案2】:

除了AnalogClock 中的当前时间之外,无法将时间设置为其他时间。这个小部件非常封装。您可以轻松更改其外观(在 XML 中使用来自 R.styleable 的属性,但没有简单的方法可以更改其行为。

我可能会制作 AnalogClock.java 的副本(将其放在单独的包中可能是个好主意,例如 com.example.widget)和可绘制对象的本地副本 clock_dial、clock_hand_hour、clock_hand_minute(注意:为了获得最佳结果,您必须为所有密度创建本地副本:ldpi、mdpi、hdpi 和 xhdpi)。有了这些并且对代码进行了小的更改,您将能够获得您想要的行为。

使用可从 XML 布局设置属性的自定义可绘制对象

为了能够从 XML 布局文件中设置 dialhand_hourhand_minute 可绘制对象,您需要将这些属性声明为可用于自定义小部件的样式。

注意:在以下示例中,我创建了TimeSettableAnalogClock 并将其放入com.example.widget 包中。

创建res/values/attrs.xml 文件,内容如下:

<?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 表示值只能是对其他现有元素的引用,例如可绘制对象 (@drawable/some_custom_dial)。

现在可以在 XML 布局文件中使用自定义属性:

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

    <com.example.widget.TimeSettableAnalogClock
        android:layout_
        android:layout_        
        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>

最后,您需要在创建TimeSettableAnalogClock 对象时处理这些自定义属性:

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();

注意,我几乎只是从对任何 drawablestyleable 的引用中删除了 com.android.internal.。其余与AnalogClock constructor相同。

使用自定义可绘制对象,但不能从 XML 布局设置属性

如果您不需要通过 XML 设置属性(例如,所有模拟时钟在每个地方看起来都一样),您可以简单地解决问题。您只需要可绘制对象的本地副本(如开头所述),不需要 attrs.xml 文件。您的构造函数应如下所示:

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();

如您所见,要短得多。 XML布局中的用法是一样的,但是你不需要xmlns:custom部分,当然你不能再设置自定义属性了:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_ >

    <com.example.widget.TimeSettableAnalogClock
        android:layout_
        android:layout_/>

</RelativeLayout>

如果您在这方面需要更多帮助,我可以使用更多建议解决方案的代码进一步更新我的答案。

【讨论】:

我试过这个解决方案。我从 Android 复制了 AnalogClock 源代码并将其放在我自己的包中。但我坚持使用 com.android.internal.R.styleable 我不知道在 Android 源代码上哪里可以找到它。你能帮忙吗? @Halim com.android.internal.R.styleable 用于处理在 XML 中为 AnalogClock 设置的属性(例如 dialhand_hour)。如果您想在自定义模拟时钟小部件中处理此类属性,则必须将它们声明为可样式化。我已经更新了我的答案以展示如何做到这一点。如果您不需要此功能(即仅在构造函数中设置的小部件自定义可绘制对象),我还添加了有关如何简化小部件代码的部分。 非常感谢您提供上述代码。但是,我还有一些问题。新类 TimeSettableAnalogClock 它是从 View 还是 AnalogClock 扩展而来的?如果它从 View 扩展,我们是否需要覆盖 onAttachedToWindow()、onDetachedFromWindow()、onMeasure(int widthMeasureSpec、int heightMeasureSpec)、onSizeChanged(int w、int h、int oldw、int oldh)、onDraw(Canvas canvas) 等方法, onTimeChanged() 等? @Halim 您的TimeSettableAnalogClock.java 应该是AnalogClock.java精确 副本,但我的回答中提到的更改除外。这意味着 1. 将所有 AnalogClock 字符串替换为 TimeSettableAnalogClock 字符串。 2. 您需要所有三个构造函数(不仅是具有三个参数的构造函数) - 这应该可以解决您的膨胀错误 3. 新类扩展 View 4。 您需要覆盖您提到的所有方法(只需将它们保留在AnalogClock.java 中)。 @Halim As,你提到的那个网站——你总是可以使用Google cache :)

以上是关于如何在应用小部件中为 Android AnalogClock 设置时间?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Android Studio 中为小部件设置不高度?

如何在 android 中探索样式

如何在app小部件中使用ListView并在android中填充数据?

如何在primarySwatch小部件中为应用栏添加颜色,但颜色来自(fromARGB和fromRGBO)

如何在android中为按钮添加图像?

如何在 Flutter 的主小部件中为小部件设置背景颜色?