Android實現時鐘Widget

FraChristia 8年前發布 | 13K 次閱讀 安卓開發 Android開發 移動開發

本節內容,我將為大家帶來一個完整的時鐘 Widget 案例,為大家介紹實現一個桌面時鐘需要哪些步驟,并為大家剖析開發過程中會遇到哪些 Bug 以及相應的解決方法。相信大家認真閱讀后會有所收獲。

項目的效果就如同電腦右下角的時鐘,需要的硬件設備是經過 Root 的安卓設備。

項目的需求如下:

1、基本顯示如電腦桌面的時鐘,會自動更新日期、時間

2、點進去可以進行日期、時間、日期格式、時間格式、時區的設置(這點為了和系統同步,需要修改系統對應的參數,所以需要 Root)

3、能修改時鐘字體的大小、顏色及支持 Widget 整體大小的縮放

4、能進行模擬時鐘顯示和基本顯示的切換

實現步驟:

第一步,搭建一個 AppWidget

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
   android:initialLayout="@layout/clock_show_text"
   android:minHeight="40dp"
   android:minWidth="80dp"
   android:resizeMode="horizontal|vertical"
   android:updatePeriodMillis="86400000"/>

這里的 resizeMode 就是實現 Widget 整體大小的縮放,支持水平和垂直方向縮放。 updatePeriodMillis 是默認的 30分鐘,也就是調用 onUpdate 的周期,這個時間設置再小也沒用,系統為了避免頻繁的更新,在這里做了限制,30為最小值。

<?xml version="1.0" encoding="utf-8" ?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/touch_show_relativeLayout"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:clickable="true">
       <TextView 
           android:id="@+id/time_show_textView"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_centerHorizontal="true"
           android:layout_centerInParent="true"
           android:clickable="false"
           android:text="@string/time" />
       <TextView 
           android:id="@+id/date_show_textView"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_below="@+id/time_show_textView"
           android:layout_centerHorizontal="true" 
           android:layout_marginTop="5dp"
           android:clickable="false"
           android:text="@string/date" />
       <!--模擬時鐘控件-->
       <AnalogClock android:id="@+id/view_show_analogClock"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_centerHorizontal="true"
           android:layout_centerInParent="true"
           android:layout_gravity="center_horizontal" 
           android:visibility="gone" />
</RelativeLayout>

值得注意的是,在布局中,初始隱藏一個 AnalogClock ,為了實現需求4,用 Visibility 控制顯示的切換。而且設置只有最外層布局可以響應點擊事件,這為了優化體驗。

第二步,在后臺啟動 Service 用于計時

因為 onUpdate 方法執行周期最少為 30 分鐘,而時鐘需要實時更新

/**
     * 更新UI
     */
    public static void updateViews() {

        Date date = sCalendar.getTime();
        updateDate(date, sDateFormat);
        updateTime(date, sTimeFormat);

        //把時間加一秒,不然Calendar的參數值不會改變
        //不能用setTime,否則時區改變會出問題
        sCalendar.add(Calendar.SECOND, 1);
    }

難點一,Calendar 不會自動更新時間

剛接觸 Calendar 開發時,會發現 Calendar 的參數取值一直保持不變,實際上確實如此,Calendar 的參數需要我們用代碼進行修改,一個好的方法是用其 add 方法修改,如果用 set 方法修改,會因為后續改變時區的操作而導致錯誤。

這里順帶分析一下,時區 TimeZone 的使用問題

以下測試代碼摘自謀博客(具體哪個忘了,在此感謝博主分享):

public static void main(String[] args) throws InterruptedException {

  Calendar calendar1 = Calendar
    .getInstance(TimeZone.getTimeZone("GMT+8"));
  Calendar calendar2 = Calendar
    .getInstance(TimeZone.getTimeZone("GMT+1"));

  System.out.println("Millis = " + calendar1.getTimeInMillis());
  System.out.println("Millis = " + calendar2.getTimeInMillis());

  System.out.println("hour = " + calendar1.get(Calendar.HOUR));
  System.out.println("hour = " + calendar2.get(Calendar.HOUR));

  System.out.println("date = " + calendar1.getTime());
  System.out.println("date = " + calendar2.getTime());
}

輸出:

Millis = 1358614681203
Millis = 1358614681203
hour = 3
hour = 8
date = Thu Nov 19 15:11:21 CST 2011
date = Thu Nov 19 15:11:21 CST 2011

改變時區TimeZone后,只有獲取的Hour是不一樣的,獲取到的Date(getTime)和getTimeInMillis是一樣的,所以修改時區后,要顯示修改后時區的時間,只能單獨修改hour

int preHour = TimerService.sCalendar.get(Calendar.HOUR_OF_DAY);
TimerService.sCalendar.setTimeZone(TimeZone.getTimeZone(timeZoneIDs[timeZonePosition]));
int currentHour = TimerService.sCalendar.get(Calendar.HOUR_OF_DAY);
TimerService.sCalendar.add(Calendar.HOUR_OF_DAY, currentHour - preHour);

難點二,怎么在后臺執行更新

筆者第一次開發時,是用 Timer 做計時活動,每 1000 毫秒,讓 Calendar add 1 秒,一切看起來運行正常。但在測試中發現,當減少系統時間時(如把日期從大到小設置,從21改為20,把分鐘從4改成3),AppWidget 就像卡住了,時間不再更新顯示了。經過原因排查,筆者發現是 Timer 停止運行了。

筆者于是翻閱 Timer 的源碼,發現停止運行的原因在其 run 方法中

long currentTime = System.currentTimeMillis();

task = tasks.minimum();
long timeToSleep;

synchronized (task.lock) {
       if (task.cancelled) {
                   tasks.delete(0);
                   continue;
       }

       // check the time to sleep for the first task scheduled
       timeToSleep = task.when - currentTime;//改小了時間,會使timeToSleep>0
}

if (timeToSleep > 0) {
       // sleep!
       try {
            this.wait(timeToSleep);
       } catch (InterruptedException ignored) {
       }
       continue;
}

timeToSleep = task.when - currentTime; //改小了時間,會使timeToSleep>0

實際上Timer會等待兩者的差值,之后再重新運行

為了解決這個問題,筆者采用 Handler 的 sendMessageDelayed 解決,其內部實現為

public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

/**
 * Returns milliseconds since boot, not counting time spent in deep sleep.
 *
 * @return milliseconds of non-sleep uptime since boot.
 */

那么 SystemClock.uptimeMillis 和 System.currentTimeMillis 這兩種方法有何區別呢?

SystemClock.uptimeMillis() // 從開機到現在的毫秒數(手機睡眠的時間不包括在內);

System.currentTimeMillis() // 從1970年1月1日 UTC到現在的毫秒數

但是,第2個時間,是可以通過System.setCurrentTimeMillis修改的,那么,在某些情況下,一但被修改,時間間隔就不準了。

當一切安好時,筆者就在無意中發現了一個 Bug, 用Handler計時,一旦鎖屏,就會停止計時,導致錯誤 ,由此,筆者最終找到了解決方法:使用 AlarmManager

/**
     * 初始化AlarmManager
     * 用Handler取代Timer,解決時間往后設(即大到小22~21)Timer的run方法就會等待對應的時間差值(22-21)
     * Timer核心是System.currentTimeMillis,Handler的核心是uptimeMillis
     * 但Handler在鎖屏狀態下就會停止計時,所以用AlarmManager取代之
     */
    private void initAlarmManager() {
        AlarmManager alarmManager = (AlarmManager) this.getSystemService(ALARM_SERVICE);
        String action = "RUN";
        Intent intent = new Intent(action);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), 1000, pendingIntent);
    }

第三步,開發點擊后彈出的設置界面及相應的操作

//修改日期
        mOnDateSetListener = new DatePickerDialog.OnDateSetListener() {
            public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
                //修改系統日期
                try {
                    SystemUtil.setSystemDate(year, monthOfYear, dayOfMonth);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        mDatePickerDialog = new DatePickerDialog(this, mOnDateSetListener, TimerService.sCalendar.get(Calendar.YEAR), TimerService.sCalendar.get(Calendar.MONTH), TimerService.sCalendar.get(Calendar.DAY_OF_MONTH));

        //修改時間
        mOnTimeSetListener = new TimePickerDialog.OnTimeSetListener() {
            public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
                //修改系統時間
                try {
                    SystemUtil.setSystemTime(hourOfDay, minute);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        mTimePickerDialog = new TimePickerDialog(this, mOnTimeSetListener, TimerService.sCalendar.get(Calendar.HOUR_OF_DAY), TimerService.sCalendar.get(Calendar.MINUTE), true);

在 Root 設備上可用如下方法對系統進行日期、時間、時區的修改

public class SystemUtil {

    //設置系統時區
    public static void setSystemTimeZone(Context context, String timeZoneId) {
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.setTimeZone(timeZoneId);
    }

    //設置系統日期和時間
    public static void setSystemDateTime(int year, int month, int day, int hour, int minute) throws IOException, InterruptedException {

        requestPermission();

        Calendar c = Calendar.getInstance();

        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month - 1);
        c.set(Calendar.DAY_OF_MONTH, day);
        c.set(Calendar.HOUR_OF_DAY, hour);
        c.set(Calendar.MINUTE, minute);


        long when = c.getTimeInMillis();

        if (when / 1000 < Integer.MAX_VALUE) {
            SystemClock.setCurrentTimeMillis(when);
        }

        long now = Calendar.getInstance().getTimeInMillis();

        if (now - when > 1000)
            throw new IOException("failed to set Date.");

    }

    //設置系統日期
    public static void setSystemDate(int year, int month, int day) throws IOException, InterruptedException {

        requestPermission();

        Calendar c = Calendar.getInstance();

        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month);
        c.set(Calendar.DAY_OF_MONTH, day);
        long when = c.getTimeInMillis();

        if (when / 1000 < Integer.MAX_VALUE) {
            SystemClock.setCurrentTimeMillis(when);
        }

        long now = Calendar.getInstance().getTimeInMillis();

        if (now - when > 1000)
            throw new IOException("failed to set Date.");
    }

    //設置系統時間
    public static void setSystemTime(int hour, int minute) throws IOException, InterruptedException {

        requestPermission();

        Calendar c = Calendar.getInstance();

        c.set(Calendar.HOUR_OF_DAY, hour);
        c.set(Calendar.MINUTE, minute);
        long when = c.getTimeInMillis();

        if (when / 1000 < Integer.MAX_VALUE) {
            SystemClock.setCurrentTimeMillis(when);
        }

        long now = Calendar.getInstance().getTimeInMillis();

        if (now - when > 1000)
            throw new IOException("failed to set Time.");
    }

    private static void requestPermission() throws InterruptedException, IOException {
        createSuProcess("chmod 666 /dev/alarm").waitFor();
    }

    private static Process createSuProcess(String cmd) throws IOException {

        DataOutputStream os = null;
        Process process = createSuProcess();

        try {
            os = new DataOutputStream(process.getOutputStream());
            os.writeBytes(cmd + "\n");
            os.writeBytes("exit $?\n");
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                }
            }
        }
        return process;

    }

    private static Process createSuProcess() throws IOException {
        File rootUser = new File("/system/xbin/ru");
        if (rootUser.exists()) {
            return Runtime.getRuntime().exec(rootUser.getAbsolutePath());
        } else {
            return Runtime.getRuntime().exec("su");
        }
    }
}

第四步,監聽系統廣播,在監聽到系統時區、日期、時間的修改后,立刻同步修改桌面時鐘

//時間、日期改變都能檢測到
    private static final String ACTION_TIME_CHANGED = Intent.ACTION_TIME_CHANGED;
    //時區改變能檢測到
    private static final String ACTION_TIMEZONE_CHANGED = Intent.ACTION_TIMEZONE_CHANGED;
    //計時廣播
    private static final String ACTION_RUN = "RUN";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        switch (action) {
            case ACTION_TIME_CHANGED:
            case ACTION_TIMEZONE_CHANGED:
                //改變時區TimeZone后,只有獲取的Hour是不一樣的,獲取到的Date(getTime)和getTimeInMillis是一樣的
                //所以修改時區后,要顯示修改后時區的時間,只能單獨修改hour
                //這里widget為了和系統顯示同步,就不改變Calendar時區
                updateDateTime();
                break;
            case ACTION_RUN:
                TimerService.updateViews();
                break;
            default:
                break;
        }
    }

在開發項目過程中,筆者還注意到以下問題:

1、修改 Widget 的代碼,要卸載原應用后再次安裝才能生效

2、如 Widget 突然失效,則可通過重啟手機設備的方式激活,原因是你的設備變得卡頓

3、在沒有卸載應用并重新安裝應用的情況下,注意測試過程中要真正停止 App 運行后,才能再次添加 widget,否則可能出現計時速度變成原來兩倍甚至多倍的情況,原因是 Service 沒被停止

// 最后一個widget被從屏幕移除
    @Override
    public void onDisabled(Context context) {
        super.onDisabled(context);
        //必須在后臺真正停止首次打開的APP運行(非ClockSettingActivity),才能停止服務,僅從桌面上移除widget不行
        context.stopService(new Intent(context, TimerService.class));
    }

 

 

來自:http://www.jianshu.com/p/ddf25d0ac5d7

 

 本文由用戶 FraChristia 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!