一个通用的H5多行文本折叠组件

1,128 阅读8分钟

前言

image.png

涉及公司H5端业务,包括APP分享业务以及各类H5活动页面,在各类主题简介栏目都会有多行文本超出折叠的交互设计。虽然在这块为了提升用户侧更好的交互体验,但是在本篇文章中主要是从技术和业务价值的角度去思考以下几点:

  1. 有哪些可能实现折叠文本的方法?
  2. 实现方法背后的相关原理及属性是什么?
  3. 有没有可能实现一个相对通用组件供团队使用?如何实现?
  4. 假如实现方式不是很合理,我们是否可以通过其他类似的交互方式来替代?这样对用户使用又有什么区别?

n行文本折叠

一看到文本超出隐藏折叠,就会想到使用overflow的方式去实现,比如:给一段文本设置固定宽度,强制不换行,并且设置text-overflow属性实现超出效果。这种简单的实现方式只适用于单行文本的情况,对于多行文本我们就需要用另一个属性去实现:

line-clamp

line-clamp正常可以接收一个整数值,用来限制当前元素内部最多能显示的文本内容行数,但是他有一个前提要求: 当前元素的display属性必须设置成-webkit-box或者-webkit-inline-box,同时box-orient属性设置成vertical才会有对应的效果,我们可以通过一段代码简单展示一下:

Pasted Graphic 1.png

当把display属性去掉时,这里高度就没有限制了。

Pasted Graphic 2.png

虽然达到了显示高度两行的效果,但我们还需要添加overflow属性,将超出高度的部分隐藏。 最后就实现了多行文本控制超出隐藏的效果。

Pasted Graphic 4.png

实现动态折叠展开的几种方式

  • 第一种:可以通过给文本元素本身添加js点击事件,动态切换元素的样式。
  • 第二种:在文本元素外部添加一个箭头按钮,将点击事件绑定在箭头按钮上,动态切换元素的样式。 这种方式其实和第一种相似,只是触发的目标对象变了。
  • 第三种:目前项目使用的交互方式,按钮紧跟在文本段尾部,其他的逻辑一样。

前面两种方式比较简单,利用当前已有的CSS属性就能实现,重点是第三种方式的具体实现。

如何让一个元素始终在文本段的右下角?

首先将其拆分为:右侧 、底部,如果想要元素保持在文本段的右侧,最先想到的就是右浮动。要想在底部,直接可以将按钮元素放在text的下方:

<div class="multi-clamp">
    这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段
    <div class="multi-clamp-btn clamp-open">展开</div>
</div>
.multi-clamp-btn{
    float: right;
    clear: both;
}

结果如图:

Pasted Graphic 1.png

多行折叠隐藏时,如何让元素在文本段右下角?

当给文本段添加最多显示3行时,看到右下角的按钮也被隐藏掉了。当前的实现方式只能保证全部显示的时候可以让按钮始终在文本段的右下角,当需要折叠展开,在折叠状态下无法简单通过浮动的方式实现。

那么需要转换一下思路: 可以将按钮放在文本段的上方,元素还是会始终保持在右侧,但需要有另一个隐藏元素将按钮推到文本段的下方去。这个时候因为事先知道本文需要保留几行,文本的字体行高都可以拿到,我们就可以手动设置隐藏元素的高度,将按钮排列到想要的位置了。

我们可以直接设置伪元素:

<div class="multi-clamp-wrapper">
<div class="multi-clamp">
    <div class="multi-clamp-btn clamp-open">展开</div>
    这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段
</div>
</div>
.multi-clamp-wrapper{
    display: flex;
}
.multi-clamp{
    max-height: 60px;
    overflow: hidden;
    line-height:20px;
}
.multi-clamp-btn{
    height: 20px;
    float: right;
    clear: both;
}
.multi-clamp-btn::before{
    content: '';
    height: calc(100% - 20px);
    float: right;
}

这里要注意,在设置辅助隐藏元素时,他的高度计算需要使用calc百分比来计算,正常情况下高度百分比无法计算,需要在最外层添加一个父级元素,并且给父级元素设置flex布局,这样内部的子元素就可以通过100%拿到高度值进行计算。 为了模拟...的效果,需要在按钮内部包一层,在外层添加before伪元素

<div class="multi-clamp-wrapper">
    <div class="multi-clamp">
        <div class="multi-clamp-btn">
            <div class="clamp-open">展开</div>
        </div>
        这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段qq测试内容啊哈哈哈哈哈哈测是内容就是在很痒的这是一段
    </div>
</div>
.multi-clamp-btn{
    position:relative;
    margin-top: 2px;
    float: right;
    clear: both;
}
.multi-clamp-btn::before{
    content: '...';
    position: absolute;
    transform: translateX(-100%);
}

结果如下:

Pasted Graphic 2.png

展开收起状态切换

切换展开收起状态:动态切换multi-clamp最大高度值,展开状态max-height:none;收起状态max-height:行高 * 行数

以上就是实现文本段结尾展开收起的整体思路,实际开发中,应该有很多地方都需要这种交互功能,可以将这种功能封装成一个通用组件。

React Hook下如何实现一个通用的文本段折叠组件

需要考虑的几点:

  1. 不同页面组件设置的文本子图和行高不一样,这里需要开放设置组件的样式(字体、行高、最多显示几行);
  2. 兼容不同行高的文本,需要将行高和最大高度设置成state动态渲染;
  3. 状态切换通过一个状态标志动态设置相关元素的高度值;
  4. 安卓手机部分app内置浏览器的像素比不一致,为了兼容需要根据机型调整高度的计算比例,比如目前发现的:安卓端qq内置浏览器会比计算的小;
  5. 需要有一个默认是否显示按钮的标志,即有可能当前的文本段没有超出置顶最多显示的行数,需要获取当前文本段展示全部的真实高度和最大显示高度,两者进行比较,判断是否需要显示展开收起的功能;
  6. 因为行高的不确定性,需要将辅助元素设置成一个具体的dom元素,从而可以动态设置当前辅助元素的高度;
  7. 展开收起按钮元素高度会和实际的文本行高有差异,为了保证按钮的居中显示,需要设置top偏移量,这个top偏移量也是需要进行动态计算的;
  8. 上述需要动态设置的都可以通过设置style实现。

代码实现:

import { useEffect, useState, useRef } from "react";
export default (props) => {
    const { className, line = 3, text, openIcon, closeIcon, initOpen = false } = props;
    let u = navigator.userAgent;
    let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
    const isXiaomi = /MI|REDMI|MIX|RC/i.test(u);
    const isAndroidQQ = (isAndroid && !isXiaomi && /MQQBrowser/i.test(u) && / QQ/i.test((u).split("MQQBrowser")));

    const [needShow, setNeedShow] = useState(false)
    const [maxHeight, setMaxHeight] = useState()
    const [lineHeight, setLineHeight] = useState()
    const [showAll, setShowAll] = useState(false)

    const textRef = useRef()
    const trueTextRef = useRef()

    const handleTextClick = () => {
        if(needShow) {
            setShowAll(!showAll)
        }
    }

    useEffect(()=>{
        if(text) {
            let trueLinHeight = window.getComputedStyle(trueTextRef.current).lineHeight || 20
            if(!isNaN(parseInt(trueLinHeight))) {
                trueLinHeight = parseInt(trueLinHeight)
            }
            setLineHeight(trueLinHeight)
            const maxHeight = line * trueLinHeight
            setMaxHeight(maxHeight)
            const trueHeight = trueTextRef.current.getBoundingClientRect().height
            if(trueHeight > maxHeight) {
                setNeedShow(true)
                if(initOpen) {
                    setShowAll(true)
                }
            }else {
                setNeedShow(false)
            }
        }
    }, [text])

    return (
        <div className="multi-clamp-wrapper" onClick={handleTextClick}>
            <div className={`line-clamp-${line} multi-clamp-text ${className || ''}`}  ref={textRef} style={{maxHeight: showAll ? 'none' : maxHeight + 'px', lineHeight: `${isAndroidQQ ? lineHeight * 1.13 : lineHeight}px`}}>
                <div className="multi-clamp-btn-before" style={{height: lineHeight ? `calc(100% - ${Math.round(isAndroidQQ ? lineHeight * 0.87 : isAndroid ? lineHeight * 0.97 : lineHeight)}px)` : 0}}></div>
                <label className={`multi-clamp-btn ${needShow ? '' : 'd-n'}`} style={{height: lineHeight + 'px', top: lineHeight ? isAndroidQQ ? `${(lineHeight*0.87 - 16)/2}px` : `${(lineHeight - 16)/2}px` : 0}}>
                   {
                        showAll ? 
                        <span className="close">{closeIcon || '收起'}</span> : 
                        <span className="open">{openIcon || '展开'}</span>
                   } 
                </label>
                <div className="multi-clamp-true-text" ref={trueTextRef}>{text}</div>
            </div>
        </div>
    )
}

使用方式:

<MutiClampText
    className="f-s-14 l-h-142858 c-565f71"
    text={text}
    line={3}
    openIcon={<span className="d-f f-c-c clamp-icon clamp-open-icon">展开</span>}
    closeIcon={<span className="d-f f-c-c clamp-icon clamp-close-icon">收起</span>}
/>

总结

其实在我看来这种交互的实现,单从业务的角度来讲,没有什么实质上的作用,但是如果是作为一个组件库平台,我们可以通过这种方式将很多类似的交互or功能型的组件统一开发统一维护,形成自己的一套规范。这样从长远的角度来讲是有效的提高了工作效率。同时记录这次实现也是为了熟悉更多相关css的细节,也算是一次归纳总结吧。文章中的内容都是自己的总结输出,有错误或者描述不恰当的地方欢迎指正。