欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android時(shí)間設(shè)置的3個(gè)小彩蛋分享

 更新時(shí)間:2023年03月28日 16:36:55   作者:小迪vs同學(xué)  
這篇文章主要給大家介紹了關(guān)于Android時(shí)間設(shè)置的3個(gè)小彩蛋,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下

問題現(xiàn)象

最近處理了一個(gè)非常有意思的系統(tǒng)bug,修改系統(tǒng)時(shí)間,重啟后居然沒有生效

注意要關(guān)閉使用網(wǎng)絡(luò)提供的時(shí)間和使用網(wǎng)絡(luò)提供的時(shí)區(qū)這兩個(gè)開關(guān)。

重啟后顯示的時(shí)間日期為

顯示的時(shí)間既不是我設(shè)置的時(shí)間,也不是當(dāng)前時(shí)間(當(dāng)前時(shí)間為2023-03-20 15:49),那么顯示的這個(gè)時(shí)間到底是什么時(shí)間呢?

為了弄清楚這個(gè)問題,我研究了一下Android設(shè)置時(shí)間的邏輯,研究過程中還發(fā)現(xiàn)了一些彩蛋。

源碼分析

首先是設(shè)置時(shí)間的邏輯,源碼位于packages/apps/Settings/src/com/android/settings/datetime/DatePreferenceController.java

public class DatePreferenceController extends AbstractPreferenceController
        implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
    //省略部分代碼
    private final DatePreferenceHost mHost;
    
	@Override
    public boolean handlePreferenceTreeClick(Preference preference) {
        //點(diǎn)擊日期后處理
        if (!TextUtils.equals(preference.getKey(), KEY_DATE)) {
            return false;
        }
        //顯示日期選擇框
        mHost.showDatePicker();
        return true;
    }
    //省略部分代碼
}

mHostDatePreferenceHost接口,接口實(shí)現(xiàn)在packages/apps/Settings/src/com/android/settings/DateTimeSettings.java中,因此,showDatePicker()的邏輯位于該實(shí)現(xiàn)類中

@SearchIndexable
public class DateTimeSettings extends DashboardFragment implements
        TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost {
    //省略部分代碼
	@Override
    public void showDatePicker() {
        //顯示日期選擇對(duì)話框
        showDialog(DatePreferenceController.DIALOG_DATEPICKER);
    }
    //省略部分代碼
}

showDialog()定義在父類packages/apps/Settings/src/com/android/settings/SettingsPreferenceFragment.java

public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment
        implements DialogCreatable, HelpResourceProvider, Indexable {    
	protected void showDialog(int dialogId) {
        if (mDialogFragment != null) {
            Log.e(TAG, "Old dialog fragment not null!");
        }
        //創(chuàng)建SettingsDialogFragment并進(jìn)行show
        mDialogFragment = SettingsDialogFragment.newInstance(this, dialogId);
        mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId));
    }
}

showDialog()中就是創(chuàng)建了SettingsDialogFragment然后顯示,SettingsDialogFragmentSettingsPreferenceFragment的一個(gè)內(nèi)部類,看一下SettingsDialogFragment的定義

    public static class SettingsDialogFragment extends InstrumentedDialogFragment {
        private static final String KEY_DIALOG_ID = "key_dialog_id";
        private static final String KEY_PARENT_FRAGMENT_ID = "key_parent_fragment_id";

        private Fragment mParentFragment;

        private DialogInterface.OnCancelListener mOnCancelListener;
        private DialogInterface.OnDismissListener mOnDismissListener;

        public static SettingsDialogFragment newInstance(DialogCreatable fragment, int dialogId) {
            if (!(fragment instanceof Fragment)) {
                throw new IllegalArgumentException("fragment argument must be an instance of "
                        + Fragment.class.getName());
            }

            final SettingsDialogFragment settingsDialogFragment = new SettingsDialogFragment();
            settingsDialogFragment.setParentFragment(fragment);
            settingsDialogFragment.setDialogId(dialogId);

            return settingsDialogFragment;
        }

        @Override
        public int getMetricsCategory() {
            if (mParentFragment == null) {
                return Instrumentable.METRICS_CATEGORY_UNKNOWN;
            }
            final int metricsCategory =
                    ((DialogCreatable) mParentFragment).getDialogMetricsCategory(mDialogId);
            if (metricsCategory <= 0) {
                throw new IllegalStateException("Dialog must provide a metrics category");
            }
            return metricsCategory;
        }

        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            if (mParentFragment != null) {
                outState.putInt(KEY_DIALOG_ID, mDialogId);
                outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId());
            }
        }

        @Override
        public void onStart() {
            super.onStart();

            if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) {
                ((SettingsPreferenceFragment) mParentFragment).onDialogShowing();
            }
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            if (savedInstanceState != null) {
                mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0);
                mParentFragment = getParentFragment();
                int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1);
                if (mParentFragment == null) {
                    mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId);
                }
                if (!(mParentFragment instanceof DialogCreatable)) {
                    throw new IllegalArgumentException(
                            (mParentFragment != null
                                    ? mParentFragment.getClass().getName()
                                    : mParentFragmentId)
                                    + " must implement "
                                    + DialogCreatable.class.getName());
                }
                // This dialog fragment could be created from non-SettingsPreferenceFragment
                if (mParentFragment instanceof SettingsPreferenceFragment) {
                    // restore mDialogFragment in mParentFragment
                    ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this;
                }
            }
            //通過DialogCreatable接口剝離了dialog的創(chuàng)建
            return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId);
        }

        @Override
        public void onCancel(DialogInterface dialog) {
            super.onCancel(dialog);
            if (mOnCancelListener != null) {
                mOnCancelListener.onCancel(dialog);
            }
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            super.onDismiss(dialog);
            if (mOnDismissListener != null) {
                mOnDismissListener.onDismiss(dialog);
            }
        }

        public int getDialogId() {
            return mDialogId;
        }

        @Override
        public void onDetach() {
            super.onDetach();

            // This dialog fragment could be created from non-SettingsPreferenceFragment
            if (mParentFragment instanceof SettingsPreferenceFragment) {
                // in case the dialog is not explicitly removed by removeDialog()
                if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) {
                    ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null;
                }
            }
        }

        private void setParentFragment(DialogCreatable fragment) {
            mParentFragment = (Fragment) fragment;
        }

        private void setDialogId(int dialogId) {
            mDialogId = dialogId;
        }
    }

很標(biāo)準(zhǔn)的自定義DialogFragment的模板代碼,核心代碼在onCreateDialog()方法當(dāng)中,但此方法通過DialogCreatable接口剝離了dialog的創(chuàng)建,這里也很好理解,因?yàn)椴粌H有設(shè)置日期的Dialog,還有設(shè)置時(shí)間的Dialog,如果寫死的話,那么就需要定義兩個(gè)DialogFragment,所以這里它給抽象出來了,DialogCreatable接口的實(shí)現(xiàn)仍然在DateTimeSettings當(dāng)中,它的父類SettingsPreferenceFragment實(shí)現(xiàn)了DialogCreatable

@SearchIndexable
public class DateTimeSettings extends DashboardFragment implements
        TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost {
    //省略部分代碼
	@Override
    public Dialog onCreateDialog(int id) {
        //根據(jù)選項(xiàng)創(chuàng)建對(duì)應(yīng)的dialog
        switch (id) {
            case DatePreferenceController.DIALOG_DATEPICKER:
                return use(DatePreferenceController.class)
                        .buildDatePicker(getActivity());
            case TimePreferenceController.DIALOG_TIMEPICKER:
                return use(TimePreferenceController.class)
                        .buildTimePicker(getActivity());
            default:
                throw new IllegalArgumentException();
        }
    }
    //省略部分代碼
}

根據(jù)用戶選擇的操作(設(shè)置日期or設(shè)置時(shí)間),創(chuàng)建對(duì)應(yīng)的dialog,最終的創(chuàng)建過程由DatePreferenceController來完成

public class DatePreferenceController extends AbstractPreferenceController
        implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
    //省略部分代碼
	public DatePickerDialog buildDatePicker(Activity activity) {
        final Calendar calendar = Calendar.getInstance();
        //創(chuàng)建DatePickerDialog
        final DatePickerDialog d = new DatePickerDialog(
                activity,
                this,
                calendar.get(Calendar.YEAR),
                calendar.get(Calendar.MONTH),
                calendar.get(Calendar.DAY_OF_MONTH));
        // The system clock can't represent dates outside this range.
        calendar.clear();
        calendar.set(2007, Calendar.JANUARY, 1);
        //設(shè)置最小時(shí)間為2007-01-01
        d.getDatePicker().setMinDate(calendar.getTimeInMillis());
        calendar.clear();
        calendar.set(2037, Calendar.DECEMBER, 31);
        //設(shè)置最大時(shí)間為2037-12-31
        d.getDatePicker().setMaxDate(calendar.getTimeInMillis());
        return d;
    }
    //省略部分代碼
}

這里可以看到,系統(tǒng)限制了可選的日期范圍為2007-01-01至2037-12-31,實(shí)際操作也確實(shí)是這樣子的(開發(fā)板和小米手機(jī)都是),此為彩蛋1。

看一下DatePickerDialog的定義

public class DatePickerDialog extends AlertDialog implements OnClickListener,
        OnDateChangedListener {
    private static final String YEAR = "year";
    private static final String MONTH = "month";
    private static final String DAY = "day";

    @UnsupportedAppUsage
    private final DatePicker mDatePicker;

    private OnDateSetListener mDateSetListener;

    //省略部分代碼

    private DatePickerDialog(@NonNull Context context, @StyleRes int themeResId,
            @Nullable OnDateSetListener listener, @Nullable Calendar calendar, int year,
            int monthOfYear, int dayOfMonth) {
        super(context, resolveDialogTheme(context, themeResId));

        final Context themeContext = getContext();
        final LayoutInflater inflater = LayoutInflater.from(themeContext);
        //初始化Dialog的View
        final View view = inflater.inflate(R.layout.date_picker_dialog, null);
        setView(view);

        setButton(BUTTON_POSITIVE, themeContext.getString(R.string.ok), this);
        setButton(BUTTON_NEGATIVE, themeContext.getString(R.string.cancel), this);
        setButtonPanelLayoutHint(LAYOUT_HINT_SIDE);

        if (calendar != null) {
            year = calendar.get(Calendar.YEAR);
            monthOfYear = calendar.get(Calendar.MONTH);
            dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
        }

        mDatePicker = (DatePicker) view.findViewById(R.id.datePicker);
        mDatePicker.init(year, monthOfYear, dayOfMonth, this);
        mDatePicker.setValidationCallback(mValidationCallback);

        mDateSetListener = listener;
    }

    //省略部分代碼

    /**
     * Sets the listener to call when the user sets the date.
     *
     * @param listener the listener to call when the user sets the date
     */
    public void setOnDateSetListener(@Nullable OnDateSetListener listener) {
        mDateSetListener = listener;
    }

    @Override
    public void onClick(@NonNull DialogInterface dialog, int which) {
        switch (which) {
            case BUTTON_POSITIVE:
                if (mDateSetListener != null) {
                    // Clearing focus forces the dialog to commit any pending
                    // changes, e.g. typed text in a NumberPicker.
                    mDatePicker.clearFocus();
                    //設(shè)置完成回調(diào)
                    mDateSetListener.onDateSet(mDatePicker, mDatePicker.getYear(),
                            mDatePicker.getMonth(), mDatePicker.getDayOfMonth());
                }
                break;
            case BUTTON_NEGATIVE:
                cancel();
                break;
        }
    }

    //省略部分代碼

    /**
     * The listener used to indicate the user has finished selecting a date.
     */
    public interface OnDateSetListener {
        /**
         * @param view the picker associated with the dialog
         * @param year the selected year
         * @param month the selected month (0-11 for compatibility with
         *              {@link Calendar#MONTH})
         * @param dayOfMonth the selected day of the month (1-31, depending on
         *                   month)
         */
        void onDateSet(DatePicker view, int year, int month, int dayOfMonth);
    }
}

可以看到也是標(biāo)準(zhǔn)的自定義Dialog,不過它是繼承的AlertDialog,設(shè)置完成后通過OnDateSetListener進(jìn)行回調(diào),而DatePreferenceController實(shí)現(xiàn)了該接口

public class DatePreferenceController extends AbstractPreferenceController
        implements PreferenceControllerMixin, DatePickerDialog.OnDateSetListener {
	//省略部分代碼
    @Override
    public void onDateSet(DatePicker view, int year, int month, int day) {
        //設(shè)置日期
        setDate(year, month, day);
        //更新UI
        mHost.updateTimeAndDateDisplay(mContext);
    }
    //省略部分代碼
    
    @VisibleForTesting
    void setDate(int year, int month, int day) {
        Calendar c = Calendar.getInstance();

        c.set(Calendar.YEAR, year);
        c.set(Calendar.MONTH, month);
        c.set(Calendar.DAY_OF_MONTH, day);
        //設(shè)置日期與定義的最小日期取最大值,也就意味著設(shè)置的日期不能小于定義的最小日期
        long when = Math.max(c.getTimeInMillis(), DatePreferenceHost.MIN_DATE);

        if (when / 1000 < Integer.MAX_VALUE) {
            //設(shè)置系統(tǒng)時(shí)間
            ((AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE)).setTime(when);
        }
    }
}

可以看到系統(tǒng)定義了一個(gè)最小日期DatePreferenceHost.MIN_DATE,其值為2007-11-05 0:00

public interface UpdateTimeAndDateCallback {
    // Minimum time is Nov 5, 2007, 0:00.
    long MIN_DATE = 1194220800000L;

    void updateTimeAndDateDisplay(Context context);
}

最終顯示日期會(huì)在目標(biāo)日期和最小日期中取最大值,也就是說設(shè)定的日期不能小于最小日期,而上文說到,選擇的日期范圍為2007-01-01至2037-12-31,因此,如果你設(shè)置的日期在2007-01-01至2007-11-05之間,最終都會(huì)顯示2007-11-05,實(shí)際測(cè)試也是如此(開發(fā)板和小米手機(jī)都是),此為彩蛋2。

選擇完時(shí)間后,最后通過AlarmManagerService來設(shè)置系統(tǒng)內(nèi)核的時(shí)間,此處涉及到跨進(jìn)程通信,使用的通信方式是AIDL,直接到AlarmManagerService看看如何設(shè)置內(nèi)核時(shí)間的

class AlarmManagerService extends SystemService {
    //省略部分代碼
	/**
     * Public-facing binder interface
     */
    private final IBinder mService = new IAlarmManager.Stub() {
        //省略部分代碼
		@Override
        public boolean setTime(long millis) {
            //先授權(quán)
            getContext().enforceCallingOrSelfPermission(
                    "android.permission.SET_TIME",
                    "setTime");
			//然后設(shè)置系統(tǒng)內(nèi)核時(shí)間
            return setTimeImpl(millis);
        }
        //省略部分代碼
    }
    
    //省略部分代碼
    
    boolean setTimeImpl(long millis) {
        if (!mInjector.isAlarmDriverPresent()) {
            Slog.w(TAG, "Not setting time since no alarm driver is available.");
            return false;
        }

        synchronized (mLock) {
            final long currentTimeMillis = mInjector.getCurrentTimeMillis();
            //設(shè)置系統(tǒng)內(nèi)核時(shí)間
            mInjector.setKernelTime(millis);
            final TimeZone timeZone = TimeZone.getDefault();
            final int currentTzOffset = timeZone.getOffset(currentTimeMillis);
            final int newTzOffset = timeZone.getOffset(millis);
            if (currentTzOffset != newTzOffset) {
                Slog.i(TAG, "Timezone offset has changed, updating kernel timezone");
                //設(shè)置系統(tǒng)內(nèi)核時(shí)區(qū)
                mInjector.setKernelTimezone(-(newTzOffset / 60000));
            }
            // The native implementation of setKernelTime can return -1 even when the kernel
            // time was set correctly, so assume setting kernel time was successful and always
            // return true.
            return true;
        }
    }
    
    //省略部分代碼
    
    @VisibleForTesting
    static class Injector {
    	//省略部分代碼
        void setKernelTime(long millis) {
            Log.d("jasonwan", "setKernelTime: "+millis);
            if (mNativeData != 0) {
                //在native層完成內(nèi)核時(shí)間的設(shè)置
                AlarmManagerService.setKernelTime(mNativeData, millis);
            }
        }
        //省略部分代碼
    }
    
    //native層完成
    private static native int setKernelTime(long nativeData, long millis);
    private static native int setKernelTimezone(long nativeData, int minuteswest);
    //省略部分代碼
}

可以看到最終是在native層完成內(nèi)核時(shí)間的設(shè)置,這也理所當(dāng)然,畢竟java是應(yīng)用層,觸及不到kernel層。

回到最開始的問題,為啥開機(jī)之后卻不是我們?cè)O(shè)置的時(shí)間呢,這就要看看開機(jī)之后系統(tǒng)是怎么設(shè)置時(shí)間的。同樣在AlarmManagerService里面,因?yàn)樗?code>SystemService的子類,所以會(huì)隨著開機(jī)啟動(dòng)而啟動(dòng),而Service啟動(dòng)后必定會(huì)執(zhí)行它的生命周期方法,設(shè)置時(shí)間的邏輯就是在onStart()生命周期方法里面

class AlarmManagerService extends SystemService {
    //省略部分代碼
    
	@Override
    public void onStart() {
        mInjector.init();

        synchronized (mLock) {
            //省略部分代碼

            // We have to set current TimeZone info to kernel
            // because kernel doesn't keep this after reboot
            //設(shè)置時(shí)區(qū),從SystemProperty中讀取
            setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));

            // Ensure that we're booting with a halfway sensible current time.  Use the
            // most recent of Build.TIME, the root file system's timestamp, and the
            // value of the ro.build.date.utc system property (which is in seconds).
            //設(shè)置時(shí)區(qū)
            //先讀取系統(tǒng)編譯時(shí)間
            long utc = 1000L * SystemProperties.getLong("ro.build.date.utc", -1L);
            //再讀取根目錄最近的修改的時(shí)間
            long lastModified = Environment.getRootDirectory().lastModified();
            //然后讀取系統(tǒng)構(gòu)建時(shí)間,三個(gè)時(shí)間取最大值
            final long systemBuildTime =  Long.max(
                    utc,
                    Long.max(lastModified, Build.TIME));
            //代碼1
            Log.d("jasonwan", "onStart: utc="+utc+", lastModified="+lastModified+", BuildTime="+Build.TIME+", currentTimeMillis="+mInjector.getCurrentTimeMillis());
            //設(shè)置的時(shí)間小于最大值,則將最大值設(shè)置為系統(tǒng)內(nèi)核的時(shí)間,注意,因?yàn)槲覀儎倓傄呀?jīng)設(shè)置了內(nèi)核時(shí)間,所以重啟后通過System.currentTimeMillis()得到的時(shí)間戳為我們?cè)O(shè)置的時(shí)間,此判斷意味著,系統(tǒng)編譯時(shí)間、根目錄最近修改時(shí)間、系統(tǒng)構(gòu)建時(shí)間、設(shè)置的時(shí)間,這四者當(dāng)中取最大值作為重啟后的內(nèi)核時(shí)間
            if (mInjector.getCurrentTimeMillis() < systemBuildTime) {
                //這里mInjector.getCurrentTimeMillis()其實(shí)就是System.currentTimeMillis()
                Slog.i(TAG, "Current time only " + mInjector.getCurrentTimeMillis()
                        + ", advancing to build time " + systemBuildTime);
                mInjector.setKernelTime(systemBuildTime);
            }
            //省略部分代碼

    }
    //省略部分代碼
        
    @VisibleForTesting
    static class Injector {
        //省略部分代碼
        void setKernelTimezone(int minutesWest) {
            AlarmManagerService.setKernelTimezone(mNativeData, minutesWest);
        }

        void setKernelTime(long millis) {
            //代碼2
            Log.d("jasonwan", "setKernelTime: "+millis);
            if (mNativeData != 0) {
                AlarmManagerService.setKernelTime(mNativeData, millis);
            }
        }

        //省略部分代碼
        long getElapsedRealtime() {
            return SystemClock.elapsedRealtime();
        }

        long getCurrentTimeMillis() {
            return System.currentTimeMillis();
        }
        //省略部分代碼
    }
}

實(shí)踐驗(yàn)證

根據(jù)源碼分析得知,系統(tǒng)最終會(huì)在系統(tǒng)編譯時(shí)間、根目錄最近修改時(shí)間、系統(tǒng)構(gòu)建時(shí)間、設(shè)置的時(shí)間,這四者當(dāng)中取最大值作為重啟后的內(nèi)核時(shí)間,這里我在代碼1和代碼2處埋下了log,看看四個(gè)時(shí)間的值分別是多少,以及最終設(shè)置的內(nèi)核時(shí)間是多少,我在設(shè)置中手動(dòng)設(shè)置的日期為2022-10-01,重啟后的日志如下

四個(gè)值分別為:

  • 系統(tǒng)編譯時(shí)間:1669271830000,格式化后為2022-11-24 14:37:10
  • 根目錄最近修改時(shí)間:1678865533000,格式化后為2023-03-15 15:32:13
  • 構(gòu)建時(shí)間:1669271830000,同系統(tǒng)編譯時(shí)間
  • 設(shè)置的時(shí)間:1664609754998,格式化后為2022-10-01 15:35:54

注意,我們只需要注意日期,不需要關(guān)注時(shí)分秒,可以看到四個(gè)時(shí)間當(dāng)中,最大的為根目錄最近修改時(shí)間,所以最終顯示的日期為2023-03-15,此為彩蛋3。

我在開發(fā)板和小米手機(jī)上測(cè)試的結(jié)果相同,說明MIUI保留了這一塊的邏輯,但是MIUI也有一個(gè)bug,就是明明我關(guān)閉了使用網(wǎng)絡(luò)提供的時(shí)間和使用網(wǎng)絡(luò)提供的時(shí)區(qū),它還是給我自動(dòng)更新了日期和時(shí)間,除非開啟飛行模式之后才不自動(dòng)更新。

同時(shí)我們還注意到,系統(tǒng)編譯時(shí)間ro.build.date.utc跟系統(tǒng)構(gòu)建時(shí)間Build.TIME是相同的,這很好理解,編譯跟構(gòu)建是一個(gè)意思,而且Build.TIME的取值其實(shí)也來自于ro.build.date.utc

/**
 * Information about the current build, extracted from system properties.
 */
public class Build {
    //省略部分代碼
	/** The time at which the build was produced, given in milliseconds since the UNIX epoch. */
    public static final long TIME = getLong("ro.build.date.utc") * 1000;
    //省略部分代碼
}

我也搞不懂Google為什么要設(shè)計(jì)兩個(gè)概念,搞得我一開始還去研究這兩個(gè)概念的區(qū)別,結(jié)果沒區(qū)別,數(shù)據(jù)源是一樣的,尷尬。

結(jié)論

設(shè)置系統(tǒng)時(shí)間必須大于系統(tǒng)編譯時(shí)間和根目錄最近修改時(shí)間才會(huì)生效。

最后我在想,MIUI是不是可以在這一塊優(yōu)化一下,直接設(shè)置里面告訴用戶我能設(shè)置的時(shí)間區(qū)域豈不是更人性化,畢竟細(xì)節(jié)決定成敗。

到此這篇關(guān)于Android時(shí)間設(shè)置的3個(gè)小彩蛋的文章就介紹到這了,更多相關(guān)Android時(shí)間設(shè)置彩蛋內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • android仿支付寶密碼輸入框效果

    android仿支付寶密碼輸入框效果

    這篇文章主要為大家詳細(xì)介紹了android仿支付寶密碼輸入框效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-12-12
  • Android基于自帶的DownloadManager實(shí)現(xiàn)下載功能示例

    Android基于自帶的DownloadManager實(shí)現(xiàn)下載功能示例

    這篇文章主要介紹了Android基于自帶的DownloadManager實(shí)現(xiàn)下載功能,結(jié)合實(shí)例形式分析了DownloadManager實(shí)現(xiàn)下載功能的具體操作步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下
    2017-08-08
  • Android ListView在Fragment中的使用示例詳解

    Android ListView在Fragment中的使用示例詳解

    這篇文章主要介紹了Android ListView在Fragment中的使用,因?yàn)楣ぷ饕恢痹谟胢vvm框架,因此這篇文章是基于mvvm框架寫的,本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-09-09
  • Flutter之自定義Dialog實(shí)現(xiàn)版本更新彈窗功能的實(shí)現(xiàn)

    Flutter之自定義Dialog實(shí)現(xiàn)版本更新彈窗功能的實(shí)現(xiàn)

    這篇文章主要介紹了Flutter之自定義Dialog實(shí)現(xiàn)版本更新彈窗功能的實(shí)現(xiàn),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-07-07
  • Android客戶端與服務(wù)端交互

    Android客戶端與服務(wù)端交互

    這篇文章主要為大家詳細(xì)介紹了Android客戶端與服務(wù)端交互之登陸示例,感興趣的小伙伴們可以參考一下
    2016-02-02
  • Android運(yùn)用BroadcastReceiver實(shí)現(xiàn)強(qiáng)制下線

    Android運(yùn)用BroadcastReceiver實(shí)現(xiàn)強(qiáng)制下線

    本篇文章主要介紹了Android運(yùn)用BroadcastReceiver實(shí)現(xiàn)強(qiáng)制下線,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-07-07
  • flutter 輸入框組件TextField的實(shí)現(xiàn)代碼

    flutter 輸入框組件TextField的實(shí)現(xiàn)代碼

    這篇文章主要介紹了flutter 輸入框組件TextField的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-07-07
  • Android實(shí)現(xiàn)合并生成分享圖片功能

    Android實(shí)現(xiàn)合并生成分享圖片功能

    這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)合并生成分享圖片功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-03-03
  • Android自定義荷載進(jìn)度的兩種方法

    Android自定義荷載進(jìn)度的兩種方法

    進(jìn)度條在App中非常常見,例如下載進(jìn)度、加載圖片、打開文章、打開網(wǎng)頁(yè)等等……本篇文章主要介紹了Android自定義荷載進(jìn)度的兩種方法,有需要的朋友可以了解一下。
    2016-10-10
  • 詳解Android廣播Broadcast的啟動(dòng)流程

    詳解Android廣播Broadcast的啟動(dòng)流程

    這篇文章主要為大家介紹了Android廣播Broadcast啟動(dòng)流程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03

最新評(píng)論