SystemUI修改

修改需求

1、新增Tile,点击后可以收起下拉状态栏,延时发送广播弹吐司

2、对锁屏界面的布局进行调整

3、对下拉状态栏的月日周时间格式,改为周日月

一、下拉菜单创建流程

1、先看布局图
展开前的布局

展开后的布局

下拉菜单有两种布局,需要分析两种布局的创建流程。

2、QsFragment的创建

先看status_bar_expanded.xml

 <!-- 主要看这个几个view的布局-->
   <!-- 这个是锁屏界面的View-->
    <include
        layout="@layout/keyguard_status_view"
        android:visibility="gone" />

        <!-- QS快捷面板的Fragment-->
        <FrameLayout
            android:id="@+id/qs_frame"
            android:layout="@layout/qs_panel"
            android:layout_width="@dimen/qs_panel_width"
            android:layout_height="match_parent"
            android:layout_gravity="@integer/notification_panel_layout_gravity"
            android:clipToPadding="false"
            android:clipChildren="false"
            systemui:viewType="com.android.systemui.plugins.qs.QS" />
       
         <!-- 通知栏布局-->
        <com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
            android:id="@+id/notification_stack_scroller"
            android:layout_marginTop="@dimen/notification_panel_margin_top"
            android:layout_width="@dimen/notification_panel_width"
            android:layout_height="match_parent"
            android:layout_gravity="@integer/notification_panel_layout_gravity"
            android:layout_marginBottom="@dimen/close_handle_underlap" />

​ 在StatusBar.java中有如下代码,用于加载QSqs_frame的界面控制就被转移到QSFragment,相应的布局也就变成了qs_panel,(从QSFragmentonCreateView中可以看出,返回的是一个布局,而这个布局就是qs_panel

// Set up the quick settings tile panel
final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame);
if (container != null) {
    FragmentHostManager fragmentHostManager = FragmentHostManager.get(container);
    ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame,
            mExtensionController
                    .newExtension(QS.class)
                    .withPlugin(QS.class)
                    .withDefault(this::createDefaultQSFragment)
                    .build());
    mBrightnessMirrorController = new BrightnessMirrorController(
            mNotificationShadeWindowView,
            mNotificationPanelViewController,
            mNotificationShadeDepthControllerLazy.get(),
            (visible) -> {
                mBrightnessMirrorVisible = visible;
                updateScrimController();
            });
    fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> {
        QS qs = (QS) f;
        if (qs instanceof QSFragment) {
            mQSPanel = ((QSFragment) qs).getQsPanel();
            mQSPanel.setBrightnessMirror(mBrightnessMirrorController);
            /* UNISOC: Bug 1074234, 885650, Super power feature @{ */
            mFooter = ((QSFragment) qs).getFooter();
            /* @} */
        }
    });
}

转到qs_panel.xml

   <!--这就是快捷面板的容器,布局风格对应下拉通知栏时展开的快捷面板,对应开头第二幅图-->
        <com.android.systemui.qs.QSPanel
            android:id="@+id/quick_settings_panel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/transparent"
            android:focusable="true"
            android:accessibilityTraversalBefore="@android:id/edit">
            <include layout="@layout/qs_footer_impl" />
            <include layout="@layout/qs_media_divider"
                android:id="@+id/divider"/>
        </com.android.systemui.qs.QSPanel>
    </com.android.systemui.qs.NonInterceptingScrollView>

        <!--这就是快捷面板的容器,布局风格对应下拉通知栏时收起的快捷面板对应开头第一幅图-->
    <include layout="@layout/quick_status_bar_expanded_header" />

​ 在QSFragment中有onCreateViewonViewCreated两个方法先后执行,在onViewCreated方法中有调用setHost(mHost)方法,分析setHost方法

//QSFragment.java
public void setHost(QSTileHost qsh) {
    mQSPanel.setHost(qsh, mQSCustomizer);
    mHeader.setQSPanel(mQSPanel);
    mFooter.setQSPanel(mQSPanel);
    mQSDetail.setHost(qsh);

    if (mQSAnimator != null) {
        mQSAnimator.setHost(qsh);
    }
}

​ 其中mQSPanel.setHost(qsh, mQSCustomizer)

//QSpanel.java
public void setHost(QSTileHost host, QSCustomizer customizer) {
    mHost = host;
    mHost.addCallback(this);
    setTiles(mHost.getTiles());
    if (mSecurityFooter != null) {
        mSecurityFooter.setHostEnvironment(host);
    }
    mCustomizePanel = customizer;
    if (mCustomizePanel != null) {
        mCustomizePanel.setHost(mHost);
    }
}

​ 这里的setTiles(mHost.getTiles())就是快捷面板添加的入口,而mHost.getTiles()的数据从QSTileHost.onTuningChanged中获取。

//QSTileHost.java
public void onTuningChanged(String key, String newValue) {
    if (!TILES_SETTING.equals(key)) {
        return;
    }
    Log.d(TAG, "Recreating tiles");
    if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
        newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
    }
    //声明一个tile的列表用于存储加载的tile
    final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
    int currentUser = ActivityManager.getCurrentUser();
    if (currentUser != mCurrentUser) {
        mUserContext = mContext.createContextAsUser(UserHandle.of(currentUser), 0);
        if (mAutoTiles != null) {
            mAutoTiles.changeUser(UserHandle.of(currentUser));
        }
    }
    if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
    mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
            tile -> {
                Log.d(TAG, "Destroying tile: " + tile.getKey());
                mQSLogger.logTileDestroyed(tile.getKey(), "Tile removed");
                tile.getValue().destroy();
            });
            //根据键值对匹配所有的tile
    final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>();
    for (String tileSpec : tileSpecs) {
        QSTile tile = mTiles.get(tileSpec);
        if (tile != null && (!(tile instanceof CustomTile)
                || ((CustomTile) tile).getUser() == currentUser)) {
            if (tile.isAvailable()) {
                if (DEBUG) Log.d(TAG, "Adding " + tile);
                tile.removeCallbacks();
                if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) {
                    tile.userSwitch(currentUser);
                }
                newTiles.put(tileSpec, tile);
                mQSLogger.logTileAdded(tileSpec);
            } else {
                tile.destroy();
                Log.d(TAG, "Destroying not available tile: " + tileSpec);
                mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
            }
        } else {
            // This means that the tile is a CustomTile AND the user is different, so let's
            // destroy it
            if (tile != null) {
                tile.destroy();
                Log.d(TAG, "Destroying tile for wrong user: " + tileSpec);
                mQSLogger.logTileDestroyed(tileSpec, "Tile for wrong user");
            }
            Log.d(TAG, "Creating tile: " + tileSpec);
            try {
                tile = createTile(tileSpec);
                if (tile != null) {
                    tile.setTileSpec(tileSpec);
                    if (tile.isAvailable()) {
                        newTiles.put(tileSpec, tile);
                        mQSLogger.logTileAdded(tileSpec);
                    } else {
                        tile.destroy();
                        Log.d(TAG, "Destroying not available tile: " + tileSpec);
                        mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
                    }
                }
            } catch (Throwable t) {
                Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
            }
        }
    }
    mCurrentUser = currentUser;
    List<String> currentSpecs = new ArrayList<>(mTileSpecs);
    mTileSpecs.clear();
    mTileSpecs.addAll(tileSpecs);
    mTiles.clear();
    mTiles.putAll(newTiles);
    if (newTiles.isEmpty() && !tileSpecs.isEmpty()) {
        // If we didn't manage to create any tiles, set it to empty (default)
        Log.d(TAG, "No valid tiles on tuning changed. Setting to default.");
        changeTiles(currentSpecs, loadTileSpecs(mContext, ""));
    } else {
        for (int i = 0; i < mCallbacks.size(); i++) {
            mCallbacks.get(i).onTilesChanged();
        }
    }
}

//在此方法中可使用add()添加新建的Tile,效果与在config.xml中添加有所不同,在config.xml中添加需要对项目进行整编才有效果,
//但是在此使用add()添加时进入快捷面板的编辑界面时新添加的快捷图标会消失,但是不需要对项目整编就可实现
protected List<String> loadTileSpecs(Context context, String tileList ,boolean is_special_request) {
/* @} */
    final Resources res = context.getResources();
    if (TextUtils.isEmpty(tileList)) {
        tileList = res.getString(R.string.quick_settings_tiles);
        if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList);
    } else {
        if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
    }

    /* UNISOC: Bug 1074234,885650,Super power feature @{ */
    if (!is_special_request) {
        if (UnisocPowerManagerUtil.isSuperPower()) {
            tileList = res.getString(R.string.quick_settings_tiles_superpower);
        }
    }

    if (!mIsEnableWifiDisplay) {
        tileList = tileList.replaceAll( ",cast|cast,|cast","");
    }
    final ArrayList<String> tiles = new ArrayList<String>();
    boolean addedDefault = false;
    Set<String> addedSpecs = new ArraySet<>();
    for (String tile : tileList.split(",")) {
        tile = tile.trim();
        if (tile.isEmpty()) continue;
        if (tile.equals("default")) {
            if (!addedDefault) {
                List<String> defaultSpecs = getDefaultSpecs(context);
                for (String spec : defaultSpecs) {
                    /* UNISOC: modify for Bug1344100 @{ */
                    if (shouldRemovedInGuestUser(spec)) {
                        continue;
                    }
                    /* @} */
                    if (!addedSpecs.contains(spec)) {
                        tiles.add(spec);
                        addedSpecs.add(spec);
                    }
                }
                addedDefault = true;
            }
        } else {
            if (!addedSpecs.contains(tile)) {
                if (shouldRemovedInGuestUser(tile)) {
                    continue;
                }
                tiles.add(tile);
                addedSpecs.add(tile);
            }
        }
    }
    return tiles;
}

public QSTile createTile(String tileSpec) {
    if (!mIsEnableWifiDisplay && tileSpec.equals("cast"))
        return null;
    if (LocationFeaturesUtils.getInstance(mContext).isLocationDisabled() && tileSpec.equals("location"))
        return null;
    if (!BluetoothAdapter.isBluetoothSupported(mContext) && "bt".equals(tileSpec)) {
        return null;
    }
    for (int i = 0; i < mQsFactories.size(); i++) {
        QSTile t = mQsFactories.get(i).createTile(tileSpec);
        if (t != null) {
            return t;
        }
    }
    return null;
}

//创建Tile
public QSTile createTile(String tileSpec) {
    QSTileImpl tile = createTileInternal(tileSpec);//分析这个方法的功能,其中是一个Switch语句,
    if (tile != null) {
        tile.handleStale(); // Tile was just created, must be stale.
    }
    return tile;
}

mHost就是QSHost对像,它的 getTiles()方法返回的是mTiles.values(),而 mTilesLinkedHashMap mTiles = new LinkedHashMap<>();数据由方法onTuningChanged(String key, String newValue)加载而来,由于onTuningChanged这个函数的创建比QSFragment更早,所以当我们调用mHost.getTiles()时,数据就已经准备好了。在loadTileSpecs函数里面有这一行 String defaultTileList = res.getString(R.string.quick_settings_tiles_default)(在config.xml中)获取我们需要加载在快捷面板上面的设置。根据defaultTileList createTile(tileSpec)创建对应的QSTile。因此还需要在QSFactoryImpl中声明自定义添加的快捷选项,至此数据创建完毕,后续就是数据的使用。

​ 再回到QSPanelControllerBasesetTiles(Collection tiles, boolean collapsedView)方法。可以看见是先清空数据再加载数据,通过addTile(final QSTile tile, boolean collapsedView)方法中调用mView.addTile(r),通过布局对象QSPanel.addTile(QSPanelControllerBase.TileRecord tileRecord))将数据通过mTileLayout.addTile(tileRecord)加载到视图,mTileLayoutQSTileLayout,加载其实现类PagedTileLayout,通过实现类的addTile(TileRecord tile)方法去保存数据,此类中有两个数属性如下

private final ArrayList<TileRecord> mTiles = new ArrayList<>();    //存储Tile数据
private final ArrayList<TilePage> mPages = new ArrayList<>();    //存储页面数据

​ 由于PagedTileLayout继承自ViewPager,所以其中有一个方法用于确定首页,可以发现这里的index=0,可以知道只有在第一页才会加载指定数量的Tile。

//PagedTileLayout.java
private void distributeTiles() {
    emptyAndInflateOrRemovePages();

    final int tileCount = mPages.get(0).maxTiles();
    if (DEBUG) Log.d(TAG, "Distributing tiles");
    int index = 0;
    final int NT = mTiles.size();
    for (int i = 0; i < NT; i++) {
        TileRecord tile = mTiles.get(i);
        if (mPages.get(index).mRecords.size() == tileCount) index++;
        if (DEBUG) {
            Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
                    + index);
        }
        mPages.get(index).addTile(tile);
    }
}

至此QSTile的加载流程分析结束,下面就是创建一个自定义的QSTile

流程分析参考文档SystemUI之Qs Tile加载流程 - 简书 (jianshu.com)

一、添加自定义Tile并发送延时广播弹出吐司(代码中注释仅文档中是中文)

**1、 首先新建一个CustomTile类继承自QSTileImpl **

public class MyCustomTile extends QSTileImpl<QSTile.BooleanState>{
    public static final String ACTION = "com.MyCustomTile.MY_BROAD";
    protected final String TAG = "MyCustomTile";
    private Context mContext;
    private boolean isAction = false;
    private AcceptBroadcast recdiver;

    //在构造方法中注册广播,也可以在原有的LatencyTester.java文件中统一注册广播
    @Inject
    public MyCustomTile(QSHost host){
        super(host);
        mContext = host.getContext();
        //防重复注册
        if (recdiver == null) {
        recdiver = new AcceptBroadcast();
        IntentFilter filter = new IntentFilter(ACTION);
        mContext.registerReceiver(recdiver,filter);
        }
    }
    /*
     * LONGCLICK EVENT
     * @return
     * */
    @Override
    public Intent getLongClickIntent(){
        return new Intent(Settings.ACTION_DATE_SETTINGS);
    }

    @Override
    public int getMetricsCategory(){
        return 0;
    }

    @Override
    public boolean isAvailable(){
        return true;
    }

    @Override
    public BooleanState newTileState(){
        return new BooleanState();
    }

    //若在点击事件中注册广播且不做判断,会出现每次点击都注册一次,造成严重的内存泄漏
    @Override
    protected void handleClick( ){
        String MSG = mContext.getString(R.string.toast_msg);
        mHost.collapsePanels();
        isAction = !isAction;
        //利用线程的特性实现延时发送广播
       mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            Intent intent = new Intent(ACTION);
            Bundle bundle = new Bundle();
            bundle.putString("msg",MSG);
            intent.putExtras(bundle);
            Log.d(TAG, "run: Ready to broadcast");
            mContext.sendBroadcast(intent);
            Log.d(TAG, "run: End of broadcast transmission +"+intent.getExtras().getString("msg"));
        }
        },2000);
    }

    @Override
    protected void handleUpdateState(BooleanState state, Object agr){
        state.state = isAction ? Tile.STATE_ACTIVE: Tile.STATE_INACTIVE;
        state.icon = ResourceIcon.get(R.drawable.ic_custom_tile);
        state.label = mContext.getString(R.string.custom_tile);
    }

    @Override
    public CharSequence getTileLabel(){
        return getState().label;
    }

    static class AcceptBroadcast extends BroadcastReceiver{
        protected final String TAG = "AcceptBroadcast";
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent != null && intent.getAction().equals(ACTION)) ;
            {
                Log.d(TAG, "onReceive: Accept Broadcast");
                Bundle extras = intent.getExtras();
                String MSG = extras.getString("msg");
                Log.d(TAG, MSG);
                Toast.makeText(context,MSG,Toast.LENGTH_SHORT).show();
            }
        }
    }

    //在管理器中注销已经注册的广播,防止资源占用
    @Override
    protected void handleDestroy() {
        super.handleDestroy();
        mContext.unregisterReceiver(recdiver);
    }
}

2、在QSFactoryImpl中注册自定义的快捷图标一共有5步

  1. 导包
  2. 声明Provider变量
  3. 在构造方法中注入Provider
  4. QSTileImpl中装载自定义的Tile

具体的写法可以参照原有Tile的写法。

3、在QSTileHost中的loadTileSpecs方法中通过add( )添加自定义的Tile.
在这里插入图片描述
至此添加一个自定义快捷方式添加完成。

4、实现效果

在这里插入图片描述
在这里插入图片描述点击2秒后会发送广播并弹出一个来自广播接收器的吐司

二、将锁屏界面的日期移到时间上面
同样使用AS自带的uiautomatorviewer查看锁屏界面的布局
在这里插入图片描述

可以发现日期所在的布局文件id,此时就可以定位到锁屏界面的布局文件keyguar_status_view.xml
在这里插入图片描述
进而找到被包含的layoutkeyguard_clock_switch.xml

从获取的锁屏布局可以分析出这里的include标签就是日期的所在位置,至此分析完成。只需依据需求调整位置即可

android:layout_above="@id/xxx"  --将控件置于指定ID控件之上 
android:layout_below="@id/xxx"  --将控件置于指定ID控件之下
android:layout_alignParentTop="true"  --将当前控件和父控件的顶部对齐

实现效果如上图所示。

三、更改状态栏中时间的显示格式
在这里插入图片描述
获取到显示日期的id之后再OpenGrok search网站上搜索相关文件,可以定位到显示日期的控件是一个自定义的DateView在这里插入图片描述
跳转到DateView后分析其中的实现方法发现主要的实现日期格式的方法如下:

protected void updateClock() {
    if (mDateFormat == null) {
        final Locale l = Locale.getDefault();
        DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l);
        //TINNO BEGIN, add by hao.tang2@tinno.com for KFCBBNHYUPEA-7
        if (SystemProperties.getBoolean("ro.feature.date_format", false)) {
            String dateFormat = Settings.System.getString(getContext().getContentResolver(),
                Settings.System.DATE_FORMAT);
            if (!TextUtils.isEmpty(dateFormat)) {
                format = new SimpleDateFormat(dateFormat);
            }
        }
        //TINNO END
        format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
        mDateFormat = format;
    }

    mCurrentTime.setTime(System.currentTimeMillis());
    //        final String text = mDateFormat.format(mCurrentTime);

    //add   2022.08.19 start
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(getContext().getString(R.string.date_format));
    final String text = simpleDateFormat.format(mCurrentTime);
    //add  2022.08.19 end
    if (!text.equals(mLastText)) {
        //add by shaowen.chen for mut.
        if (SystemProperties.get("ro.product.brand").equals("Multilaser")) {
            final String replaceStr = text.replaceAll("\\.","");
            setText(replaceStr);
            mLastText = replaceStr;
        } else {
            setText(text);
            mLastText = text;
        }
    }
}

只需添加一个格式化日期的代码即可更改日期的显示格式。实现效果如上图所示。

日期格式

格式示例
dd/MM/yyyy06/03/2007
dd-MMM-yyyy06-Mar-2007
MM/dd/yyyy03/06/2007
MMM dd, yyyyMar 06, 2007
MMMMM dd, yyyyMarch 06, 2007
yyyy.MM.dd2007.06.03
yyyy/MM/dd2007/06/03
yyyy-MM-dd2007-06-03

日期格式清单

格式示例
dd-MM-yyyy HH’h’mm06-03-2007 13h44
dd-MM-yyyy HH’h’MM’min’06-03-2007 13h44min
dd-MMM-yyyy HH:mm06-Mar-2007 13:44
dd/MM/yyyy HH’h’mm06/03/2007 13h44
dd/MM/yyyy HH:mm06/03/2007 13:44
dd/MM/yyyy hh:mm a zzz06/03/2007 01:44 PM PST
dd/MM/yyyy HH:mm zzz06/03/2007 13:44 PST
dd/MM/yyyy HH:mm:ss06/03/2007 13:44:25
dd/MM/yyyy hh:mm:ss a zzz06/03/2007 01:44:25 PM PST
dd.MM.yyyy HH:mm:ss06.03.2007 13:44:25
MM/dd/yyyy HH:mm03/06/2007 13:44
MM/dd/yyyy hh:mm a zzz03/06/2007 01:44 PM PST
MM/dd/yyyy HH:mm zzz03/06/2007 13:44 PST
MM/dd/yyyy HH:mm:ss03/06/2007 13:44:25
MM/dd/yyyy hh:mm:ss a zzz03/06/2007 01:44:25 PM PST
MMMMM dd, yyyy hh:mm a zzzMarch 06, 2007 01:44 PM PST
yyyy-MM-dd HH.mm2007-03-06 13.44
yyyy-MM-dd hh:mm a zzz2007-03-06 01:44 PM PST
yyyy.MM.dd hh:mm a zzz2007.03.06 01:44 PM PST
yyyy/MM/dd hh:mm a zzz2007/03/06 01:44 PM PST
yyyy/MM/dd HH:mm zzz2007/03/06 13:44 PST

从 A 到 Z 和从 a 到 z 的字母是解释的字符。所有其他字符都是复制的字符。

虽然从 A 到 Z 和从 a 到 z 的所有字母都是解释的字符,但并非所有字母都分配了解释。

虽然所有信件从 下面的段落描述格式中的字母的解释。

以下图表显示用于设置部分日期的字母的解释。字母重复的次数会影响其设置日期的一部分的方式。另外,大写和小写字母具有不同的解释。

字母序列描述示例
d一个月中一位或两位数的日期1 - 31
dd一个月中两位数的日期01 - 31
DDD一年中三位数的日期001 - 366
EEE一周中缩写的日期Mon - Sun
EEEE一周中日期的全称Monday - Sunday
M一位或两位数的月份1 - 12
MM两位数的月份01 - 12
MMM三个字母的月份缩写Jan - Dec
MMMMM月份的全称January - December
y一位或两位数的年份0 - 99
yy两位数的年份00 - 99
yyyy四位数的年份1999、2000、2010

以下图表显示用于设置部分时间的字母的解释。

字母的重复次数会影响设置时间的一部分的方式,大写和小写字母具有不同的解释。

如果仅具有日期值的格式中包含字母用于设置其中的时间的格式,那么该值的时间部分将看起来像是午夜。

字母序列描述示例
a表示 AM 或 PMAM 或 PM
h上午/下午 (1-12) 中一位或两位数的小时数1 - 12
hh上午/下午 (01-12) 中两位数的小时数01 - 12
H一天 (0-23) 中一位或两位数的小时数0 - 23
HH一天 (00-23) 中两位数的小时数00 - 23
m一小时中一位或两位数的分钟数0 - 59
mm一小时中两位数的分钟数00 - 59
s一分钟中一位或两位数的秒数0 - 59
S一位、两位或三位数的毫秒数0 - 999
ss一分钟中两位数的秒数00 - 59
SSS三位数的毫秒数000 - 999
z 或 zzz三个字母的时区缩写EST、CST 等。
Z相对于 GMT 的时区-0500、-0600 等。

一天 (0-23) 中一位或两位数的小时数 | 0 - 23 |
| HH | 一天 (00-23) 中两位数的小时数 | 00 - 23 |
| m | 一小时中一位或两位数的分钟数 | 0 - 59 |
| mm | 一小时中两位数的分钟数 | 00 - 59 |
| s | 一分钟中一位或两位数的秒数 | 0 - 59 |
| S | 一位、两位或三位数的毫秒数 | 0 - 999 |
| ss | 一分钟中两位数的秒数 | 00 - 59 |
| SSS | 三位数的毫秒数 | 000 - 999 |
| z 或 zzz | 三个字母的时区缩写 | EST、CST 等。 |
| Z | 相对于 GMT 的时区 | -0500、-0600 等。 |

日期格式参考链接:日期格式 - 简书 (jianshu.com)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值