Angular Material 18+ 高级教程 – Custom Themes for Material Design 2 (自定义主题 Material 2)

v18 更新重要说明

从 Angular Material v18 开始,默认使用的是 Material 3 Design (简称 M3),本篇教的是旧版本的 Material 2 Design (简称 M2)。

虽然如此,但它们的调用手法和理念大同小异 (有些功能 M2 有,M3 没有),所以我建议大家从本篇学起,然后才去学下一篇 Material 3

或许 v19 后我会把两篇做合并。

阅读前有 2 个点要知道:

  1. v18 后 CLI 默认创建的是 M3 版本的 demo 代码,而本篇展示的是 M2 版本的 demo 代码。

  2. 有一些 (不是全部哦) CSS variables 和 mixin 需要加上 $m2 prefix 来表示使用 M2 版本。

    下图这样 (注:本篇 demo 代码没有加上 $m2 是因为本篇是 v17 时期写的,我暂时懒得改了)

     

前言

我们之所以选择使用 Angular Material 是因为我们喜欢 Material Design,然而,如果每个使用 Angular Material 开发的项目都看起来完全一样,那就不太理想了。

我们希望项目在保持 Material Design 的同时,也能够有一些个性化的差异。

为此,Angular Material 提供了自定义主题 (Custom Theme) 方案,它允许我们微调 Material Design 的风格,使我们的项目变得有点个性。

 

参考

Docs – Theming Angular Material

Docs – Theme your own components with Angular Material's theming system

Docs – Customizing Typography

Docs – Customizing Angular Material component styles

Material Design 2 Docs – The color system

 

Theme 可以微调什么?

Theme 可以调三个元素:

  1. 颜色 -- Color

    对颜色一窍不通的朋友,可以先看这 3 篇文章

    平面设计 – 颜色, 平面设计 – 色轮 & 配色The color system

  2. 字体 -- Typography

    对字体一窍不通的朋友,可以先看这篇 -- 平面设计 – 字体

  3. 亲密度 -- Density

    对排版亲密度一窍不通的朋友,可以先看这篇 -- 平面设计 – 排版四大原则

举例:两个项目,一个用红色作为主题,另一个用蓝色作为主题。

红主题项目中的 Material 组件就是红色的,蓝主题项目就是蓝色的。

 

Prebuilt Themes

创建 Angular 项目

ng new material --routing=false --ssr=false --skip-tests --style=scss

添加 Angular Material

ng add @angular/material

在添加 Angular Material 过程中它会有几个选项

第一个就是选择 Theme。

总共有 4 个 Prebuilt Themes 可选:indigo-pink,deeppurple-amber,pink-bluegrey,purple-green。

下图是不同主题下的 Button 组件

不同的主题定义了不同的主色 (primary color) 和强调色 (accent color)。

这些 Prebuilt Themes 是通过 angular.json 设置的

 

Custom Theme Get Started

在添加 Angular Material 时选择 Custom Theme

选择 Custom Theme 后,angular.json 就没用 Prebuilt Themes 了。

取而代之的是 styles.scss 多了许多代码

我们一行一行推敲看看,不用深入,下一 part 会细讲。

首先是执行了一个 core 方法,顾名思义就是输出一些 base styles。

接着是

palette 的意思是调色板,从命名和调用上可以看出来,这里主要是通过调色板生成了一些颜色,然后用变量 primary,accent,warn 装起来。

接着用三个颜色创建一个主题

最后 apply 到所有组件上

步骤总结:调色 -> 配色 -> 创建主题 -> aplly to 组件

 

Color Theme

上一 part 我们只是过个场,这一 part 来讲细节了。

Palette 調色盤

我们要自定义颜色,首先要搞懂颜色和調色盤。

Material Design 的颜色可不是简简单单的红黄蓝绿,它的颜色是包含明度的,下图是 3 个 Material 颜色 -- 红,粉,紫

每种颜色加入明度后派生出了 14 个 Level。50 - 900 是单纯的明度 Level,A100 的 A stand for Accent,也就是强调色 (不是所有颜色都会有 Accent,但大部分都有)。

Angular Material 有 built-in 所有 Material Design 的颜色 (完整 color list 看这里)

$material-primary: mat.define-palette(mat.$red-palette);

mat.$red-palette,mat.$pink-palette,mat.$purple-palette 就代表了下图三个颜色

Custom Base Palette

如果我们想要的颜色不在 built-in color list 里,我们可以自己创建一个。

通过工具 Tools for picking colors (不使用工具也行,自己拿捏明度 Level 就可以了)

用 Scss declare a Base Palette

$my-base-palette: (
  50: #efe6ff,
  100: #d5c1fd,
  200: #b897fe,
  300: #986aff,
  400: #7c45fe,
  500: #5b16fc,
  600: #4b10f6,
  700: #3002ee,
  800: #0000e9,
  900: #0000e5,

  contrast: (
    50: rgba(black, 0.87),
    100: rgba(black, 0.87),
    200: rgba(black, 0.87),
    300: white,
    400: white,
    500: white,
    600: white,
    700: white,
    800: white,
    900: white,
  ),
);

50 - 900 的颜色来自工具,contrast (对比色) 只能是黑或白,不过黑不是纯黑,而是黑的 0.87%😂,这个是 Material Design 的规范。

验证 contrast 的工具:WebAIM

Primary,Accent,Warn Color

一个主题能配置 3 个颜色,每一个的用途都不一样:

  1. Primary -- 主色

    主色就是主角啦,戏份最多,一眼看过去出现最多的就是主色啦。

  2. Accent -- 强调色

    万绿丛中一点红,Accent 通常会用在一些要引起用户交互的地方,比如 Call to Action。

  3. Warn -- 警告色

    通常出现在一些 Error 的地方。

Define Primary Palette

上面我们创建的是 Base Palette,它太底层了,Angular Material 无法直接使用,我们还需要经过一轮加工 -- mat.define-palette

$material-primary: mat.define-palette($my-base-palette);

mat.define-palette 的源码在 _theming.scss

主要是扩展了原本的 Base Palette,增加了多几个属性,方便上层使用。比如:

  1. default

    默认就是 Base Palette 的 500 Level 明度

  2. lighter

    亮一点,默认 100 明度

  3. darker

    暗一点,默认 700 明度

  4. 还有 text 和一堆颜色对应的 contrast

Define Accent Palette

如果 Base Palette 支持 Accent 的话,比如

在 define Accent Palette 时可以传入参数,自定义 default,lighter,darker 的明度 Level。

$material-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);

Define Warn Palette

$material-warn: mat.define-palette(mat.$red-palette);

Warn Palette 通常就是用 mat.$red-palette。

Define Color Theme

把 Primary,Accent,Warn 3 个颜色传入 mat.define-light-theme 方法,创建出最终的 Theme。

$material-primary: mat.define-palette($my-base-palette);
$material-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
$material-warn: mat.define-palette(mat.$red-palette);

$material-theme: mat.define-light-theme(
  (
    color: (
      primary: $material-primary,
      accent: $material-accent,
      warn: $material-warn,
    ),
  )
);

这个方法还有一个 dark 版本的 mat.define-light-theme,顾名思义就是暗黑版本。效果是这样的

可以看到不同的主题,Angular Material Card 组件会呈现不同的颜色 (e.g. background-color)

Apply Theme

Apply Theme 给所有组件

@include mat.all-component-themes($material-theme);

如果我们的项目没用使用那么多组件,其实可以不需要 apply to all components。

all-component-themes 源码在 _all-theme.scss

第一句是 core-theme,其它的是每一个组件 theme。我们也可以像它这样

// @include mat.all-component-themes($material-theme);
@include mat.core-theme($material-theme);   // for core
@include mat.button-theme($material-theme); // for button
@include mat.card-theme($material-theme);   // for card

 

Dark Mode

上一 part 我们有提到了 Dark Theme,这一 part 我们就来具体实现 Dark Mode 吧。

首先创建多一个 Dark Theme。

@use '@angular/material' as mat;

@include mat.core();

$material-primary: mat.define-palette(mat.$indigo-palette);
$material-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
$material-warn: mat.define-palette(mat.$red-palette);

$material-light-theme: mat.define-light-theme((
  color: (
    primary: $material-primary,
    accent: $material-accent,
    warn: $material-warn,
  )
));

$material-dark-theme: mat.define-dark-theme((
  color: (
    primary: $material-primary,
    accent: $material-accent,
    warn: $material-warn,
  )
));

Primary,Accent,Warn 颜色是一样的 (当然你要不一样也可以),只是创建 Theme 用了不同的方法,一个用 define-light-theme 另一个用 define-dark-theme。

接着写 media query apply Theme。

@include mat.all-component-themes($material-light-theme);

@media (prefers-color-scheme: dark) {
  @include mat.all-component-colors($material-dark-theme);
}

注意,media query 里使用的方法是 mat.all-component-colors 而不是 mat.all-component-themes,因为 Theme 除了 Color 还包含 Typography 和 Density。

如果我们使用 mat.all-component-themes 那 Typography 和 Density 就 duplicate styles 了,会报错的。

使用 mat.all-component-colors 就只会 override Color Theme 而已。

 

Multiple Theme

Dark Mode 其实也算是 Multiple Theme,只是它比较简单,我们来点复杂的 -- 让用户自己能切换主题。

预期效果

有 4 个 Themes,点击 Chip 就可以切换。

首先,我们封装一个简单的 create theme 函数,方便复用

@function create-theme($primary-palette, $accent-palette, $warn-palette, $prefers-color-scheme: 'light') {
  $primary: mat.define-palette($primary-palette);
  $accent: mat.define-palette($accent-palette, A200, A100, A400);
  $warn: mat.define-palette($warn-palette);

  @if($prefers-color-scheme == 'dark') {
    @return mat.define-dark-theme((
      color: (
        primary: $primary,
        accent: $accent,
        warn: $warn,
      )
    ));
  }
  @else {
    @return mat.define-light-theme((
      color: (
        primary: $primary,
        accent: $accent,
        warn: $warn,
      )
    ));
  }
}

然后创建 4 个 Themes。

$theme1: create-theme(mat.$deep-purple-palette, mat.$amber-palette, mat.$red-palette, 'light');
$theme2: create-theme(mat.$indigo-palette, mat.$pink-palette, mat.$red-palette, 'light');
$theme3: create-theme(mat.$pink-palette, mat.$blue-grey-palette, mat.$red-palette, 'dark');
$theme4: create-theme(mat.$purple-palette, mat.$green-palette, mat.$red-palette, 'dark');

接着 apply to 组件

@include mat.all-component-themes($theme1);
 
body.theme2 {
  @include mat.all-component-colors($theme2);
}
body.theme3 {
  @include mat.all-component-colors($theme3);
}
body.theme4 {
  @include mat.all-component-colors($theme4);
}

默认是 theme1,当 body 有 theme class 时就覆盖上其它的 Theme。

App 组件

import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [MatButtonModule, MatCardModule, MatChipsModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {
  selectedTheme = 'theme1';
  themes = ['theme1', 'theme2', 'theme3', 'theme4'];

  updateTheme(selectedTheme: string) {
    document.body.classList.remove(this.selectedTheme);
    this.selectedTheme = selectedTheme;
    document.body.classList.add(this.selectedTheme);
  }
}

切换 Theme 时就修改 body class。

App Template

<mat-chip-listbox>
  @for (theme of themes; track theme) {
    <mat-chip-option (selectionChange)="updateTheme(theme)" [selected]="theme === selectedTheme">{{ theme }}</mat-chip-option>
  }
</mat-chip-listbox>

<mat-card>
  <mat-card-header>
    <mat-card-title>Title</mat-card-title>
    <mat-card-subtitle>Subtitle</mat-card-subtitle>
  </mat-card-header>
  <mat-card-content>
    <div class="button-list">
      <button mat-raised-button color="primary">Primary</button>
      <button mat-raised-button color="accent">Accent</button>
      <button mat-raised-button color="warn">Warn</button>
      <button mat-raised-button disabled>Disabled</button>
    </div>
  </mat-card-content>
</mat-card>

App 组件和 Template 不是重点,你想如何切换 Theme 都可以,这里只是给一个例子。

Lazy Load Theme (styles.css)

在做 Multiple Theme 时,我们可以通过 Angular CLI 做 split bundle 达到 Lazy Load Theme 的效果,这样可以提升 first page load performance。

首先,创建一个 _styles-theme-shared.scss,内容是上面的 create-theme 函数

@use '@angular/material' as mat;

@function create-theme($primary-palette, $accent-palette, $warn-palette, $prefers-color-scheme: 'light') {
  $primary: mat.define-palette($primary-palette);
  $accent: mat.define-palette($accent-palette, A200, A100, A400);
  $warn: mat.define-palette($warn-palette);

  @if($prefers-color-scheme == 'dark') {
    @return mat.define-dark-theme((
      color: (
        primary: $primary,
        accent: $accent,
        warn: $warn,
      )
    ));
  }
  @else {
    @return mat.define-light-theme((
      color: (
        primary: $primary,
        accent: $accent,
        warn: $warn,
      )
    ));
  }
}
View Code

然后把 4 个 Themes 拆成 4 个 styles.scss

styles-theme1.scss

@use '@angular/material' as mat;
@use './styles-theme-shared' as shared;

@include mat.core();

$theme1: shared.create-theme(mat.$deep-purple-palette, mat.$amber-palette, mat.$red-palette, 'light');

@include mat.all-component-themes($theme1);

styles-theme2.scss

@use '@angular/material' as mat;
@use './styles-theme-shared' as shared;

$theme2: shared.create-theme(mat.$indigo-palette, mat.$pink-palette, mat.$red-palette, 'light');
 
body.theme2 {
  @include mat.all-component-colors($theme2);
}

styles-theme3.scss

@use '@angular/material' as mat;
@use './styles-theme-shared' as shared;

$theme3: shared.create-theme(mat.$pink-palette, mat.$blue-grey-palette, mat.$red-palette, 'dark');
 
body.theme3 {
  @include mat.all-component-colors($theme3);
}
View Code

styles-theme4.scss

@use '@angular/material' as mat;
@use './styles-theme-shared' as shared;

$theme4: shared.create-theme(mat.$purple-palette, mat.$green-palette, mat.$red-palette, 'dark');
 
body.theme4 {
  @include mat.all-component-colors($theme4);
}
View Code

接着在 angular.json 做配置

"styles": [
  "src/styles-theme1.scss",
  {
    "input": "src/styles-theme2.scss",
    "inject": false
  },
  {
    "input": "src/styles-theme3.scss",
    "inject": false
  },
  {
    "input": "src/styles-theme4.scss",
    "inject": false
  }
],

声明 theme2,theme3,theme4 不要放入 index.html。

这样 bundle 出来的结果是 

  1. index.html 只有 theme1.scss

  2. theme2.scss,theme3.scss,theme4.scss 会被独立 bundle 出来

接着在 App 组件切换 Theme 时才 append link to load theme CSS file。

export class AppComponent {
  selectedTheme = 'theme1';
  themes = ['theme1', 'theme2', 'theme3', 'theme4'];

  updateTheme(selectedTheme: string) {
    document.body.classList.remove(this.selectedTheme);
    this.selectedTheme = selectedTheme;
    document.body.classList.add(this.selectedTheme);
    const href = `styles-${selectedTheme}.css`;
    const existingLink = document.head.querySelector(`link[href="${href}"]`);
    if(existingLink === null) {
      const link =  document.createElement('link');
      link.rel = 'stylesheet';
      link.href = `styles-${selectedTheme}.css`;
      document.head.appendChild(link);
    }
  }
}

Lazy Load Theme (styles.css) for Dark Mode

如果只是做 Dark Mode,我们不需要自己去 append link 这么麻烦。

用 <link> element + [media] attirbute 就可以了

<link rel="stylesheet" href="theme-dark.css" media="(prefers-color-scheme: dark)" />

提醒:即便 media not match,游览器依然会 preload theme-dark.css 的哦。

Medium – Why Browsers Download Stylesheets With Non-Matching Media Queries

 

Default Typography Theme

Material Design 默认的 font-family 是 RobotoType Scale 有 13 个 Level。

不过 Angular Material 对它的理解只有 12 个 Level (少了 OVERLINE,我也不知道为什么🤔)

而最终 Angular Material 只实现了 11 个 Level,外加 2 个不在 Material Design 规范内的 Level

正版的 overline 和 button 没有实现,但是多了两个山寨版的 mat-h5 和 mat-h6,也是醉啦😅。

mat-h5 和 mat-h6 其实是取之于 body-2,它是缩小版的 body-2。

Use Default Typography Theme

添加 Angular Material 到项目

ng add @angular/material

顺便选一个 Theme (我选了 Indigo/Pink)。

Angular CLI 会替我们在 index.html 引入 Roboto 字体。

在 style.scss 会添加 font-family Roboto

在 App Template 使用 Angular Material 组件 (我拿 Material Card 组件做例子)

<mat-card>
  <mat-card-header>
    <mat-card-title>Title</mat-card-title>
    <mat-card-subtitle>Subtitle</mat-card-subtitle>
  </mat-card-header>
  <mat-card-content>
    <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Consequuntur ullam ex totam iste natus, odtio error delectus!</p>
  </mat-card-content>
</mat-card>

效果

组件的 font-size,font-weight 都是依据 Default Typography Theme,也就是 Material Design 规范。

查看最终的 HTML,title 和 subttile 分别使用 class 'mat-mdc-card-title' 和 'mat-mdc-card-title'

查看 card.scss 源码

继续追踪会去到 _card.scss

title 使用的是 Level headline-6 也就是 size 20px,weight 500。

subtitle 使用的是 Level subtitle-2 也就是 size 14px,weight 500。

我们也可以使用这些 Level,在 element 添加 class 就可以了

<h1 class="mat-headline-5">Headline 5</h1>
<h2 class="mat-headline-6">Headline 6</h2>
<h3 class="mat-h5">Mat h5</h3>
<h4 class="mat-h6">Mat h6</h4>
<h5 class="mat-subtitle-1">Subtitle 1</h5>
<h6 class="mat-subtitle-2">Subtitle 2</h6>

效果

注:margin-bottom 也是 Headline 负责的哦,字体越大 margin-bottom 越大。

有时候项目会使用 h1 – h6 element 来管理 font-size,我们只需要添加一个 class mat-typography 到 container (通常是放在 body)

<div class="mat-typography">
  <h1>Headline 5</h1>
  <h2>Headline 6</h2>
  <h3>Subtitle 1</h3>
  <h4>Body 1</h4>
</div>

旗下的 h1 - h6 就会对应到不同的 Material Typography Scale Level。

效果

Class Name,Level Name,h1 – h6 的配对是很混乱的,使用的时候要看清楚哦。

有些 Class Name 还有 alias 别名,更乱😓。

另外,上面这个图是 v15 和以后的版本,下图是 v14 和之前的版本,它们的名字还不一样,就问你晕不晕🤮。

 

Custom Typography Theme

Angular Material 允许我们修改所有的 Type Scale Level 的尺寸,包括 font-size, font-weight, line-height, letter-spacing, font-family。

Use Default Typography Theme

我们先用 Custom Typography Theme 的方式来使用 Default Typography Theme。

添加 Angular Material 到项目

ng add @angular/material

选择 Custom Theme

App Template

<mat-card>
  <mat-card-header>
    <mat-card-title>Title</mat-card-title>
    <mat-card-subtitle>Subtitle</mat-card-subtitle>
  </mat-card-header>
  <mat-card-content>
    <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Consequuntur ullam ex totam iste natus, odtio error delectus!</p>
  </mat-card-content>
</mat-card>

效果

由于我们选了 Custom Theme,那原本 Default Typography Theme 的 font-size 和 font-weight 就没了。

我们手动把 Default Typography Theme 添加回去,在 styles.scss 添加

@include mat.core();

$material-primary: mat.define-palette(mat.$indigo-palette);
$material-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
$material-warn: mat.define-palette(mat.$red-palette);

// 1. 创建 Default Typography
$default-typography: mat.define-typography-config();
$material-theme: mat.define-light-theme((
  color: (
    primary: $material-primary,
    accent: $material-accent,
    warn: $material-warn,
  ),
  // 2. 把 Typography 添加到 Custom Theme
  typography: $default-typography
));

@include mat.all-component-themes($material-theme);

效果

虽然 Angular Material 组件有 Default Typography Theme 了,但仅限于 Angular Material 组件,我们自己还用不到这个 Default Typography Theme。

<h1 class="mat-headline-5">Headline 5</h1>
<h2 class="mat-headline-6">Headline 6</h2>
<h3 class="mat-h5">Mat h5</h3>
<h4 class="mat-h6">Mat h6</h4>
<h5 class="mat-subtitle-1">Subtitle 1</h5>
<h6 class="mat-subtitle-2">Subtitle 2</h6>

效果

我们需要在 styles.scss 添加这句

效果

Create Custom Typography Theme

首先我们要定义每一个 Type Scale Level 的尺寸。

$my-headline-1--level: mat.define-typography-level(
  $font-weight: 700,
  $font-size: 48px,
  $line-height: 56px
);

$my-headline-2--level: mat.define-typography-level(
  $font-weight: 700,
  $font-size: 32px,
  $line-height: 40px
);

mat.define-typography-level 方法的源码在 _definition.scss

Material Design 完整 13 个 Level 都可以 set,不过 button 和 overline 是没有对应的 class 的哦。(有 class mat-headline-1,但是没有 mat-button 和 mat-overline)

另外,class mat-h5 等于 body-2 缩写 0.83,class mat-h6 等于 body-2 缩小 0.67。

接着把 Custom Level 放入 Typography Config

$my-custom-typography-config: mat.define-typography-config(
  $headline-1: $my-headline-1--level,
  $headline-2: $my-headline-2--level
);

没有 override 的它会用 default。

然后 set to theme 和 typography hierarchy。

效果

<h1 class="mat-headline-1">Headline 1</h1>
<h2 class="mat-headline-2">Headline 2</h2>

如果要换 font-family 可以直接换 index.html 的 <link> 和 styles.scss 的 body styles。

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
body { margin: 0; font-family: "Open Sans", sans-serif; }

然后在 define-typography-level 时设置 $font-family

$my-headline-1--level: mat.define-typography-level(
  $font-family: 'Open Sans'
  $font-weight: 700,
  $font-size: 48px,
  $line-height: 56px,
);

效果

 

Density Theme

Density 是密度的意思,这里的密度指的是排版四大原则之一的亲密度 -- 元素之间的间距。

下面 button 的高度是 36px

如果我们想让它密集一点,可以通过调整它的 Density。

在 define-light-theme 传入 density 值。

$theme: mat.define-light-theme((
  color: (
    primary: $primary,
    accent: $accent,
    warn: $warn,
  ),
  typography: $typography,
  density: -1
));

0 代表 normal,-1 代表更密集 (通常是 4px),-2 就 8px 以此类推

效果

注:density 最小是 -3,因为 density 只能减少空间 (spacing),字体是不缩小的。

如果想单独控制一个区域也可以

.density-1 {
  $density-1-theme: mat.define-light-theme((
     density: -1
  ));
  @include mat.all-component-densities($density-1-theme);
}

把 density -1 apply under class density-1。

注:和 Color Theme 一样,这里用的是 mat.all-component-densities 而不是 mat.all-component-themes,

因为 all-component-themes 包含 Color 和 Typography,apply 2 次会导致 duplicated,而 all-component-densities 只覆盖 Density。

然后 container 加上 class density-1 就可以了

<div class="button-list density-1">
  <button mat-raised-button color="primary">Primary</button>
  <button mat-raised-button color="accent">Accent</button>
  <button mat-raised-button color="warn">Warn</button>
</div>

 

How Theme Work?

要了解 Theme 如何工作,最彻底的方式自然是逛源码咯。

但是 Scss 源码可不好阅读,因此我们还是看看 ng build 出来的 CSS,然后做点推敲就好。

首先,关闭 ng build 的 Uglify 功能,不然乱码我们读不了。

接着关闭 bundle size error,因为不 Uglify 文件会变大很多。

Base Theme

styles.scss

@use '@angular/material' as mat;

// 1. base styles 我们就不研究了,看组件就好了
// @include mat.core();

$base-theme: mat.define-light-theme(());
@include mat.button-base($base-theme);

mat.button-base 输出的是 Material Button 组件的 base styles,它和 Color、Typography、Density Theme 无关,

虽然如此,但我们依然需要传入一个 $theme,因为 $theme 里面还有一些小属性是它会用到的,比如 version。

styles.css (after ng build)

@include mat.button-base($base-theme) 最终输出的是一堆 CSS Variables

Angular Material 的所有组件都是靠 CSS Variables 来做调整的,倘若没有对应的 CSS Variables 那就表示这个设计不能调整。

当然如果我们硬硬要改的话,还是可以使用 ::ng-deep 这类黑科技来达到目的。对魔改感兴趣的朋友建议看看这篇:Docs – Customizing Angular Material component styles

好,回到主题

--mdc-text-button-container-shape: 4px;
--mat-text-button-horizontal-padding: 8px;
--mat-text-button-icon-spacing: 8px;

从上面几个例子可以看出它的命名规范,我们自定义的属性也可以依据这个命名规范,比如

--my-component-container-shape: 4px;
--my-component-horizontal-padding: 8px;
--my-component-icon-spacing: 8px;

Color Theme

styles.scss

$primary-color: mat.define-palette(mat.$indigo-palette);
$accent-color: mat.define-palette(mat.$pink-palette, A200, A100, A400);
$warn-color: mat.define-palette(mat.$red-palette);
$color-theme: mat.define-light-theme((
  color: (
    primary: $primary-color,
    accent: $accent-color,
    warn: $warn-color,
  ),
));
@include mat.button-color($color-theme);

每个 Angular Material 组件都有 base,color,typography,density 四个方法。

上一 part 的 mat.button-base 输出 Button 组件 base styles CSS Variables,这里的 mat.button-color 则输出和 Color 相关的 CSS Variables。

styles.css

全部都是和 Color 相关的 CSS Variables。

不同主题色是通过 class mat-primary,mat-accent,mat-warn 覆盖 base CSS Variables 来实现的。

Typography Theme

styles.scss

$typography-config: mat.define-typography-config();
$typography-theme: mat.define-light-theme((
  typography: $typography-config,
));
@include mat.button-typography($typography-theme);

styles.css

全部都是和 Typography 相关的 CSS Variables。

Density Theme

styles.scss

$density-theme: mat.define-light-theme((
  density: -3,
));
@include mat.button-density($density-theme);

style.css

上面 density 是 -3,build 出来的 CSS Variables 是 24px,如果 density 是 0 那 build 出来的是 36px。

Angular Material 组件使用 Theme CSS Variables

我们看看 Button 组件如何使用这些 CSS Variables 的。

App Template

main.js (after ng build)

没什么特别的,就是直接拿来用就对了。

总结

每个 Angular Material 组件统一提供了 base,color,typography,density 方法。

我们在 styles.scss 创建 $theme,然后调用这些方法并传入 $theme,这些方法内部会读取 $theme 配置然后输出最终的 CSS Variables。

比如说 mat.button-density($density-theme) 方法内部会读取 $density-theme 中的 density -3 然后输出 CSS Variables 24px。

 

Custom component reads theme information

上一 part 我们说到

每个 Angular Material 组件统一提供了 base,color,typography,density 方法。

比如 _button-theme.scss (每个组件都有一个 _component-theme.scss 文件)

这些方法要从 $theme 中读取信息并且输出 CSS Variables 到 :root 或者 html,接着组件就可以读取这些 CSS Variables 了。

这 part 我们来看如何从 $theme 中读取相关信息。

首先创建一个 Hello World 组件

ng g c hello-world

HelloWorld Template

<h1>Hello World</h1>

HelloWorld Styles

h1 {
  background-color: var(--hello-world-title-bg-color);
  color: var(--hello-world-title-color);

  padding: 12px 16px;
  width: max-content;
}

引用了 2 个 CSS Variables

创建 _hello-world-theme.scss,里头有一个 color 方法,这个方法是给 styles.scss 调用的。

@mixin color($theme) {
  :root {
    --hello-world-title-bg-color: red;
    --hello-world-title-color: white;
  }
}

颜色暂时放 hardcode,待会我们才 read from $theme,先看完整体。

styles.scss

$primary-color: mat.define-palette(mat.$indigo-palette);
$accent-color: mat.define-palette(mat.$pink-palette, A200, A100, A400);
$warn-color: mat.define-palette(mat.$red-palette);
$color-theme: mat.define-light-theme((
  color: (
    primary: $primary-color,
    accent: $accent-color,
    warn: $warn-color,
  ),
));
@include hello-world.color($color-theme);

效果

read primary color from theme

@use '@angular/material' as mat;

@mixin color($theme) {

  $primary: mat.get-theme-color($theme, primary, default);
  $primary-contrast: mat.get-theme-color($theme, primary, default-contrast);

  :root {
    --hello-world-title-bg-color: #{$primary};
    --hello-world-title-color: #{$primary-contrast};
  }
}

通过 mat.get-theme-color 方法读取 primary color

效果

mat.get-theme-color 有 3 个参数:

  1. theme

  2. palette name

    有 5 个选项 primary,accent,warn,background,foreground

  3. color name

    不同 palette name 有不同的 color name 选项

    下图是 primary,accent,warn 的 color name 选项


    上面例子就是用了 primary 配 default 和 default-contrast。

    下图是 background 可选的 color name

    比如:那 dialog background 将得到白色

    $dialog-bg-color: mat.get-theme-color($theme, background, dialog); // white

    下图是 foreground 可选的 color name

除了拿各种颜色,还可以拿 prefers-color-scheme,看是 dark theme 还是 light theme。

$prefers-color-scheme: mat.get-theme-type($theme); // dark | light
$background: if($prefers-color-scheme == dark, black, white);

在 HelloWorld Styles 添加 font-size

styles.scss

$typography-config: mat.define-typography-config();
$typography-theme: mat.define-light-theme((
  typography: $typography-config,
));
@include hello-world.typography($typography-theme);

_hello-world-theme.scss

@mixin typography($theme) {
  $headline-1-font-size: mat.get-theme-typography($theme, headline-1, font-size);
  
  :root {
    --hello-world-title-font-size: #{$headline-1-font-size};
  }
}

mat.get-theme-typography 方法可以获取所有 Typography Scale Level 的 information,它有两个参数

  1. Level Name

  2. Property Name 选项有 font-size, font-weight, line-height, letter-spacing, font-family

如果没有指定参数两 Property Name 它会返回 font shorthand,比如

$headline-1-font-size: mat.get-theme-typography($theme, headline-1); // 300 96px / 96px Roboto, sans-serif

Density 和 Color, Typography 一样我就不重复写了。

最关键的是

$density: mat.get-theme-density($theme);

mat.get-theme-density 方法会返回一个数,从 0 到 -5。

依据 Material 规范每 -1 Level 密度就要靠近 4px。

大概长这样

@mixin density($theme) {
  $density: mat.get-theme-density($theme);
  $default-height: 36px;
  $height-after-density: calc($default-height - ($density * -1 * 4px));
  
  :root {
    --hello-world-container-height: #{$height-after-density};
  }
}

Check has Theme

下面这个是完整的 Theme

$theme: mat.define-light-theme((
  color: (
    primary: $primary-color,
    accent: $accent-color,
    warn: $warn-color,
  ),
  typography: $typography-config,
  density: -1
));

下面这个是只有 Density 的 Theme

$density-theme: mat.define-light-theme((
  density: -1,
));

它们都调用 define-light-theme 方法,只是传入的配置可能不完整。

当我们在使用 Theme 时如果有可能出现不完整,那最好检查一下才使用

$has-base: mat.theme-has($theme, base);
$has-color: mat.theme-has($theme, color);
$has-typography: mat.theme-has($theme, typography);
$has-density: mat.theme-has($theme, density);

 

 

目录

上一篇 Angular Material 18+ 高级教程 – Get Started

下一篇 Angular Material 18+ 高级教程 – Custom Themes for Material Design 3 (自定义主题 Material 3)

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻

 

posted @ 2024-02-19 15:43  兴杰  阅读(814)  评论(1编辑  收藏  举报