创造 CANDY 主题,只为更好的交互创造 CANDY 主题,只为更好的交互

创造 CANDY 主题,只为更好的交互

📚本系列文章,将分成四个板块更新

  1. 基础设计修改
  2. 配置 Twikoo 评论
  3. 增加深色模式支持
  4. 其他细节与模块

众所周知,Icarus[1] 是一个非常优秀的 Hexo 主题。它不仅提供清爽、简洁的界面,还与各种主流插件、组件兼容的很好。更令人欣喜的是——它几乎是最活跃的 Hexo 主题之一,有着非常良好的社区氛围。作者时刻保持更新,并提供了非常完善的文档。

种种因素使我最终选择了它,并准备在其基础上修改出自己满意的设计,我将其命名为 Candy,并将在 GitHub 上与 Icarus 保持更新。本文将是这一系列文章的第一节——基础设计修改。

本人前端零基础,如有不妥,请在评论区教我做事,谢谢🙏

真正的卡片式设计——让最频繁的交互更人性化

分析

Icarus 主题很美观,但对我来说它与许多 Hexo 主题一样都存在一个核心问题,即首页的文章如需访问全文,需要点击 Read More 按钮、图片或标题才可以进入,而点击主文字或卡片空白处是没有反应的,恰好很多博主喜欢把摘要写成长篇大论,此时的 Read More 按钮看起来更小,很容易被忽视,点击大段文字也许是很多读者的第一反应,虽然这并不会得到任何回应。

大佬们的博客,长长的「摘要」使 Read More 按钮显得特别小

点击、进入全文,这应该是每个博客最频繁的交互。尴尬的小按钮难免给读者造成一些细微的障碍,当然如果对自己的内容足够自信,或者没那么讲究的话,你就可以跳过这一节了。

尝试

为了达成真正的卡片效果,我探索过一些方法。

我的第一反应是在 <article> 标签外套一层 <a> 标签然后设置 href 属性,同时 <article> 要设置成块级元素。于是我便在 layout/common/article.jsx 中这样修改:

layout/common/article.jsx
<a class="card-link" href={index ? url_for(page.link || page.path):null}> <article class={`card-content article${'direction' in page ? ' ' + page.direction : ''}`} role="article"> ... </article> </a>

感觉非常合理,但其实并不行,如果这样操作,在实际页面中,<a> 标签并不会正确包裹,反而会被添加到 <article> 元素下每个子元素的首位。我并不确定是什么原因,但是根据 Stack Overflow 上的一篇回答中的评论:

You should really point out (by suggesting this way) that: using this solution you cannot have other Anchor elements inside the article.Roko C. Buljan Jul 14 '16 at 6:51

貌似如果想这样操作的话,子元素不能再有其他 <a> 元素。我便以为是不可以的,当时就作罢了。

接下来,我尝试在外面先套一层 <object> 标签,再套 <a> 标签链接到正文,这样我们就实现了整张卡片可点击的效果。

layout/common/article.jsx
<a class="card-link" href={index ? url_for(page.link || page.path) : null}> <object class="card-object"> <article class={`card-content article${'direction' in page ? ' ' + page.direction : ''}`} role="article"> ... </article> </object> </a>

这样确实可以了,本来这一段到这里就结束了,但我想起了文档里对 <object> 标签的描述:

HTML <object> 元素(或者称作 HTML 嵌入对象元素)表示引入一个外部资源,这个资源可能是一张图片,一个嵌入的浏览上下文,亦或是一个插件所使用的资源。MDN

啊这,有一丝隐隐的不安。迅速查了下 SEO,发现目前 Google 是会索引 object 元素的,但心里就是不舒服,毕竟不太符合语义。

于是乎,就换来了接下来的方法,也是最终的实现方法。

解法

这种方法的思路其实和第一种是一致的,但是具体实现有区别,这次我们不在 article.jsx 里修改了,直接在 source/js/main.js 最底下添加:

if ($('.article-licensing.box').length === 0){
  $(".card-content.article").each(function(){
    $(this).wrap('<a class="card-link" href="' + $(this).find('h1 a').attr('href') + '"></a>');
  })}

其实就是用了 jQuery 的 warp() 方法[2],这里我们指定 <a> 元素来包裹 <article> 元素。同时因为我们只需要在首页让这些卡片有链接,所以我们可以通过判断正文末尾的版权信息盒子是否存在,来控制这条语句是否生效。

我们还需要设置一下 card-linkcolorinherit,这样摘要的颜色就会恢复成白色,否则会是你 a 标签的颜色(自定义 CSS 的方法在后文):

.card-link
    color: inherit

这样我们就实现了整个卡片可点击,且卡片里原有的分类链接照样可以点击并不受影响。但我并不知道为何第一种和第三种方法会有这样的差异,如果有大佬清楚请在评论区解答我,谢谢。

加入一点动效会让体验更好吗?答案是肯定的

所谓「交互」,代表着交流和互动。一个好的交互设计,组件与用户的互动是必不可少的。互联网厂商们早已发现了这一逻辑,并将这些互动悄悄藏在设计里,通过微小的震动、令人愉悦的变换俘获用户的感官。

简单来说,光将整张卡片链接到正文还不够,它得是让用户有感知的,这个感知就由动效来实现。

废话少说,先上效果图👇:

动图可能看不太清楚,可自行在本博客体验效果,深色模式和浅色模式下会有细微的差别

为实现这个效果,我增加了:

  • 向上的位移

  • 阴影的变换

  • 题图的变大(深色模式下,还增加了题图的变暗)

我希望能用纯 CSS 解决的,尽量只用 CSS。为了后期与 Icarus 同步更方便,我们修改 source/css 目录下的 style.styl 来自定义样式。Icarus 使用 Stylus[3] 作为 CSS 预处理器。它的定义生效规则是:

一个变量不能影响在定义它以前的输出样式。[4]

所以为了方便,我们可以把所有自定义样式放在最前面,把 @import 放在最后。

位移与阴影

首先,我们将位移与阴影一起添加,效果模仿自 Gridsome Blog Starter 🙏。

因为我不希望在正文处也出现动效,同时希望尽可能用纯 CSS 解决,所以我在 article.jsx 最前面添加一个只在正文生效的空的 div 元素,并通过 class 名「controller」来控制:

article.jsx
return <Fragment> // 通过判断 index 是否存在,来决定是否是正文。 {!index ? <div class={'controller'}></div>: null} ... </Fragment>

这样一来,正文的最前面就会多一个空的、class 名为「controller」的 div 元素。接下来我们写伪类:

.order-2 .card:hover
    transform: translateY(-5px)
    box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)
    -webkit-transform: translateY(-5px)
    -webkit-box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)

// 将兄弟元素为 .controller 的 .card 的 :hover 样式重制(无效化)
.order-2 .controller ~ .card:hover
    transform: none
    box-shadow: 0 4px 10px rgba(0,0,0,0.05), 0 0 1px rgba(0,0,0,0.1)
    -webkit-transform: none
    -webkit-box-shadow: 0 4px 10px rgba(0,0,0,0.05), 0 0 1px rgba(0,0,0,0.1)

// 加入过渡效果
.card
    transition: opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.6s !important

可以看出来,这样写会有一些啰嗦和冗余,我尝试过一些想法都不太好,比如将 index 判断反写,把「controller」加在首页最上方,但 Icarus 原版的 <Fragment> 元素写法,会在每个卡片上加上一个 div 元素,从而打乱布局,让菜菜的我觉得麻烦,所以先就这样,日后再改。

如果有大佬们有更好的解决方案,请在评论区赐教。

点击展开查看:一个现在不兼容,但未来可能可以的做法。

菜菜的我本来很辛苦的摸索了出来这种写法,结果发现目前只有 Safari 和最新版本的 Firefox 可以比较好的支持,这取决于浏览器对 CSS Selector Level 4 的支持程度(Safari 91% 支持,而 Chrome 只有 88%,这个特性恰好在这不支持的 3% 里)。

简单来说就是 :not 里嵌套兄弟选192择器 ~

.order-2 > :not(.controller ~ .card):hover
    transform: translateY(-5px)
    box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)
    -webkit-transform: translateY(-5px)
    -webkit-box-shadow: 1px 10px 30px 0 rgba(0,0,0,.1)

很好理解,这个选择器会选择兄弟不为 .controller.card,这样只需要这一个语句即可。

关于这一特性的详细可以看 W3C Editor’s DraftCSS4 Selectors,另附 Can I use 兼容情况

题图

最后我们添加题图放大的动效,这里的效果模仿自 Apple 官网的 Newsroom 📰,注意 :hover 的写法,我希望鼠标放在卡片上时就激活(而不仅仅是放在题图上):

style.styl
.card:hover .card-image a -webkit-transform: scale(1.03) transform: scale(1.03) // 加入过渡效果 .card .card-image a -webkit-transition: opacity 1s cubic-bezier(0.4, 0, 0.25, 1), -webkit-transform 400ms cubic-bezier(0.4, 0, 0.25, 1),200ms -webkit-filter linear transition: opacity 1s cubic-bezier(0.4, 0, 0.25, 1), transform 400ms cubic-bezier(0.4, 0, 0.25, 1),200ms filter linear

细心的朋友可能会发现,光这样还不完美——图片在变换的过程中会先取消圆角,再恢复圆角,也就是说中间会有一段时间「四角方方」,这是我们不愿意看到的,解决方法也很简单,再添加一个 0 度的旋转[5]即可:

// 修复变换时的圆角
.card-image
    transform: rotate(0deg)
    -webkit-transform: rotate(0deg)

在黑暗模式下我还加入了题图变暗的效果。关于黑暗模式我会在之后详解,就不在本文中赘述了:

-webkit-filter: brightness(70%)
filter:brightness(70%)

至此,我们便完成了一个用户友好的卡片设计,它整张卡片可点击,且有合适的动效互动,但这并不是 Candy 主题的全部,还有很多细节性的动效就不在这里赘述了,感兴趣的话可以去查看 style.styl 源码

「跟着走」的导航栏,贴心而不恼人

最后,我想我需要一个会跟随页面的导航栏,它得是贴心的——当你需要它的时候,比如回首页、切换浅色/深色模式、搜索,它就在那里;它还得是不恼人的——它不应该遮挡太多,影响到正文或视觉平衡,它得是灵巧的,且更好的适配响应式设计。

跟着走?No!它只是固定在那里

导航栏不会真的跟着走的,我们只是给它添加一个 positionfixed的样式。这么做的同时还需要调整 width100% 并将下一个元素 section 下移:

.navbar-main
    position: fixed !important
    width: 100% !important
    top: 0px

.section
    margin-top: 45px

当然,也可以采用 sticky 不过兼容性会差一些,在此案例中效果是一样的。使用 sticky 就不需要设置 section 的下移,不过需要设置 top 属性:

.navbar-main
    position: sticky !important
    position: -webkit-sticky !important
    top: 0px

「瘦身」+「毛玻璃」,让导航栏灵巧一些

接下来我想让导航栏看起来更轻巧一些,于是缩窄了导航栏的高度并加入了所谓「毛玻璃」效果。

「瘦身」

通过修改 navbar-item 的内外边距来缩窄高度:

.navbar-item
    margin: 4px !important
    padding: 8.5px !important

同时 Icarus 原版对于移动端的处理是将 logo 放在一个自适应的 div 里,当屏幕宽度小于 1088 px 的时候,导航栏会分成上下两行。

但这对于一个一直固定在顶端的导航栏来说太大了,所以这里我做了个修改,将自适应去掉,把 logo 也放在 navbar-menu 里:

layout/common/navbar.jsx
- <div class="navbar-brand justify-content-center"> - <a class="navbar-item navbar-logo" href={siteUrl}>{navbarLogo}</a> - </div> <div class="navbar-menu"> + <a class="navbar-item navbar-logo" href={siteUrl}>{navbarLogo}</a> ... </div>

效果图:

改动后的导航栏更窄更清爽了(右)

当然,还有些更好的方案,比如制作下拉菜单栏,以解决导航栏里的标签页太多超出屏幕宽度的问题,可以见这个旧版本的 PR。有空的时候我会改进导航栏,如果真的有人看的话。

「毛玻璃」

所谓「毛玻璃」,更专业一点的说法叫「Backdrop Filter effect」,即背景过滤效果。所以核心就是 backdrop-filter 样式。我这里依旧模仿的 Apple Newsroom,将 navbar 背景色变透明并增加饱和度和模糊:

.navbar
    -webkit-backdrop-filter: saturate(180%) blur(20px)
    backdrop-filter: saturate(180%) blur(20px)
    background-color: rgba(255,255,255,0.7) !important
    //黑暗模式下颜色为 rgba(29,29,31,0.7)

再将 navbar-menu 背景设为透明,这样小尺寸屏幕下也能正常显示:

.navbar-main .navbar-menu
    background-color: transparent

效果图:

最后,修复锚元素定位

我们固定了导航栏,紧接着问题就来了。你会发现之前的页内锚元素跳转全都错位了(比如目录、脚注),更准确的来说是正好被现在的导航栏所遮挡,所以我们要来修复这个 bug。

脚注修复

先来修复简单的,修复脚注我们只需要用到 CSS(因为新版目录是用 JavaScript 来控制 href 属性值的,这个方法就不行了),这里我们用到一个小技巧,即用 :target 去定位目标元素,并用 ::before 在前面加一个空的行内元素,然后通过 padding-topmargin-top 配合来控制位置这样就修复了正文到脚注的跳转错位:

:target::before 
    content: ""
    display: inline-block
    padding-top: 73px
    margin-top: -73px

稍微解释一下,这里就是先用 padding-top 将这个空的 inline-block 移动到距离其容器上方 N px 的位置,然后通过 margin-top 将这个容器整体上移 N px 的位置,这样我们就可以通过调整 N 的值来控制元素的位置。

同理,我们还需要修复下脚注到正文的跳转:

.footnote-item::before 
    display: block
    padding-top: 55px
    margin-top: -55px
    margin-bottom: -25px

这里有一些细微的差别,因为脚注是一个列表,列表的元素应该按一整条一整条来看,所以我们把 display 改为 block,再按照之前的方法适当调整一下距离,最后我们还需要通过 margin-bottom 来对齐文本和序号。

目录修复

如果你不需要一个「随着滚动到不同位置,动态折叠展开」的目录,你可以使用这个旧版本的 toc.js,并且采用上面修复脚注的方式来修复错位,这里我就不赘述了。

如果你需要这样一个「智能」的目录,请采用最新版本toc.js 并把它放在 source/js 里,这里我只说下对于错位的修复。

首先在 source/js/toc.js 中创建一个 scrollTo 方法来代替不支持偏移量的 scrollIntoView 方法:

function scrollTo(id) {
  var element = document.getElementById(id);
  var headerOffset = -20; // 偏移量
  var elementPosition = element.offsetTop;
  var offsetPosition = elementPosition - headerOffset;
  document.documentElement.scrollTop = offsetPosition;
  document.body.scrollTop = offsetPosition; // 适配 Safari
}

没有什么可说的,非常好理解,然后在下面替换即可:

if (typeof $heading.scrollTo === 'function') {
- $heading.scrollIntoView({ behavior: 'smooth' });
+ scrollTo($heading.id);
}

就是这么简单,我们完成了所有错位修复。

写在最后的、其他的一些玩意儿

感谢你看到这里,零基础的我可能比较啰嗦,这当然不是我在视觉和交互上做出的全部修改,尽量挑了些对我来说比较有代表性的「坑」,大佬们见笑了。一些细枝末节的修改,网络上可能已经有无数篇相同的文章教你如何去操作了,我就不费口舌了。当然如果你有任何疑问、评价或想教我做事儿,也请不要吝啬,请在文末的评论区给我留言,我会第一时间回复。

这是 Candy 主题修改系列的第一篇,应该也是最零碎的一篇,我非常迫不及待地想分享关于黑暗模式的实现,但是先忍住,下一篇将会是关于 Twikoo 的,一个非常崭新的评论系统,美观、安全、易用、免登录、免费等等,虽然它还有很长的路要走,但目前配得上这些美好的词汇。希望我能在官方作者写出详细教程之前发布(逃)。

本文完。

原创封面图,使用需授权,请勿盗用 

脚注


  1. Icarus 主题的 GitHub 仓库主页 ↩︎

  2. .wrap( wrappingElement ): Wrap an HTML structure around each element in the set of matched elements. ↩︎

  3. Stylus 是富于表现力、健壮、功能丰富的 CSS 预处理器,详见官网↩︎

  4. 再谈 CSS 预处理器|变量作用域 - Justineo ↩︎

  5. 图片transform其父级的border-radius失效 - 沈志勇说 ↩︎

创造 CANDY 主题,只为更好的交互

https://anzifan.com/post/icarus_to_candy_1/

作者

安子璠

发布于

2020-11-19

更新于

2021-02-11

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×