热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android中TextView限制最大行数并在最后用显示...全文

TextView在android开发中是一个经常用到的基础控件,功能也很强大,限制输入字符类型,字数什么的,下面这篇文章主要给大家介绍了关于Android中TextView限制最大行数并在最后用显示全文的相关资料,需要的朋友可以参考下

一、场景

我们知道通常在列表页面会有很多内容,而且每条内容可能会很长,如果每条内容都全部显示用户体验就很不好。所以,我们通常的处理方案是限制每条内容的行数,这个时候如果想更加明显的提示用户该条内容有更多的内容,可以进入详情页查看时会在内容最后加上“全文”之类的字眼。尤其是社区内的APP里经常会看到这样的场景,比如:微博。

二、方案的实现

那如果我们想限制最大行数且在最后显示...全文该怎么实现呢?我们知道我们通常设置TextView的最大行数是设置maxLines属性,并设置android:ellipsize="end"表示在内容最后显示...。但是类似"全文“这样的文字怎么显示呢?我想这时大家肯定会想到:在内容最后拼上去啊!没错,是需要拼上去,那要怎么拼?怎么拼上去正好在内容的最后,既不提前、又完整显示”全文“?

1、”常规”方案

网上大多关于这个需求的实现方案都是在textView.setText()之后调用textView.post方法,伪代码:

textView.post(new Runnable() {
            @Override
            public void run() {
                //进行内容的截取和拼接
            }
     });

或者是设置addOnGlobalLayoutListener监听,伪代码:

textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                    //进行内容的截取和拼接
                }
            }
        });

其本质和核心都是为了获取内容的行数,来判断是否大于我们想设置的最大行数,来进行内容的截取和”全文“的拼接。

但是该方案是在setText()之后进行的截取,也就是TextView已经显示了内容然后再进行内容的处理再次setText()。那么会有以下明显的缺点:

1:在性能差的设备上会有闪现全部内容然后再显示处理后的内容。

2:这样做会有两次的setText()操作,在内容很多的列表页会加大性能的损耗。

2、"优化"的处理方案

这个时候可能有人会说既然绘制完成后再处理会有问题,提前获取到textView的行数进行处理不就好了吗?没错,我们可以设置addOnPreDrawListener监听提前获取行数来进行处理,伪代码:

textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
     @Override
     public boolean onPreDraw() {
        //进行内容的截取和拼接
        return false
    }
});

但是这种方案只适合单独一条内容,不适合在列表中使用,因为这样只有在第一屏有效,且滑动多屏后回到第一屏也会重置为原始数据。

3、最终方案

既然设置addOnPreDrawListener监听提前获取行数来进行处理的方案在列表中不可行还有没有其他方法呢?那当然是在TextView的onMeasure()中测量textView的高度时进行内容的处理,并设置相对应的高度了,这样就可以保证性能问题又能保证列表中的每条内容都能得到处理。先上代码:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    if (lineCount > maxLine) {
        //如果大于设置的最大行数
        val (layout, stringBuilder, sb) = clipContent()
        stringBuilder.append(sb)
        setMeasuredDimension(measuredWidth, getDesiredHeight(layout))
        text = stringBuilder
    }
}

/**
 * 裁剪内容
 */
private fun clipContent(): Triple {
    var offset = 1
    val layout = layout
    val staticLayout = StaticLayout(
            text,
            layout.paint,
            layout.width,
            Layout.Alignment.ALIGN_NORMAL,
            layout.spacingMultiplier,
            layout.spacingAdd,
            false
    )
    val indexEnd = staticLayout.getLineEnd(maxLine - 1)
    val tempText = text.subSequence(0, indexEnd)
    var offsetWidth =
            layout.paint.measureText(tempText[indexEnd - 1].toString()).toInt()
    val moreWidth =
            ceil(layout.paint.measureText(moreText).toDouble()).toInt()
    //表情字节个数
    var countEmoji = 0
    while (indexEnd > offset && offsetWidth <= moreWidth ) {
        //当前字节是否位表情
        val isEmoji = PublicMethod.isEmojiCharacter(tempText[indexEnd - offset])
        if (isEmoji){
            countEmoji += 1
        }
        offset++
        val pair = getOffsetWidth(
                indexEnd,
                offset,
                tempText,
                countEmoji,
                offsetWidth,
                layout,
                moreWidth
        )
        offset = pair.first
        offsetWidth = pair.second
    }
    val ssbShrink = tempText.subSequence(0, indexEnd - offset)
    val stringBuilder = SpannableStringBuilder(ssbShrink)
    val sb = SpannableString(moreText)
    sb.setSpan(
            ForegroundColorSpan(moreTextColor), 3, sb.length,
            Spanned.SPAN_INCLUSIVE_INCLUSIVE
    )
    //设置字体大小
    sb.setSpan(
            AbsoluteSizeSpan(moreTextSize, true), 3, sb.length,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
    )
    if (moreCanClick){
        //设置点击事件
        sb.setSpan(
                MyClickSpan(context, onAllSpanClickListener), 3, sb.length,
                Spanned.SPAN_INCLUSIVE_INCLUSIVE
        )
    }
    return Triple(layout, stringBuilder, sb)
}

private fun getOffsetWidth(
        indexEnd: Int,
        offset: Int,
        tempText: CharSequence,
        countEmoji: Int,
        offsetWidth: Int,
        layout: Layout,
        moreWidth: Int
): Pair {
    var offset1 = offset
    var offsetWidth1 = offsetWidth
    if (indexEnd > offset1) {
        val text = tempText[indexEnd - offset1 - 1].toString().trim()
        if (text.isNotEmpty() && countEmoji % 2 == 0) {
            val charText = tempText[indexEnd - offset1]
            offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
            //一个表情两个字符,避免截取一半字符出现乱码或者显示不全...全文
            if (offsetWidth1 > moreWidth && PublicMethod.isEmojiCharacter(charText)) {
                offset1++
            }
        }
    } else {
        val charText = tempText[indexEnd - offset1]
        offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
    }
    return Pair(offset1, offsetWidth1)
}

/**
 * 获取内容高度
 */
private fun getDesiredHeight(layout: Layout?): Int {
    if (layout == null) {
        return 0
    }
    val lineTop: Int
    val lineCount = layout.lineCount
    val compoundPaddingTop = compoundPaddingTop + compoundPaddingBottom - lineSpacingExtra.toInt()
    lineTop = when {
        lineCount > maxLine -> {
            //文字行数超过最大行
            layout.getLineTop(maxLine)
        }
        else -> {
            layout.getLineTop(lineCount)
        }
    }
    return (lineTop + compoundPaddingTop).coerceAtLeast(suggestedMinimumHeight)
}

大概思路就是判断内容行数大于我们想要的内容行数时进行内容的裁剪,内容最后显示的文案moreText可以按照需求配置,我们测量出moreText的宽度,从最大行数的最后一个文字向前遍历截取,直至截取文字的宽度大于等于moreText的宽度,然后我们通过使用SpannableString来拼接moreText文案和moreText的点击事件。这里还处理了截取到表情字符的情况,我们知道一个表情两个字符,如果正好截取到表情的一半可以放下moreText就会导致表情变成一个?的乱码。 另外这里,我们设置了moreText的点击事件,那如果textView本身需要设置点击事件怎么办?这个时候就需要处理触摸事件了,代码如下:

    val text = text
    val spannable = Spannable.Factory.getInstance().newSpannable(text)

    if (event.action == MotionEvent.ACTION_DOWN) {
        //手指按下
        onDown(spannable, event)
    }

    if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) {
        //如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent
        return MyLinkMovementMethod.instance
                .onTouchEvent(this, text as Spannable, event)
    }

    if (event.action == MotionEvent.ACTION_MOVE) {
        //手指移动
        val mClickSpan = getPressedSpan(this, spannable, event)
        if (mPressedSpan != null && mPressedSpan !== mClickSpan) {
            mPressedSpan = null
            Selection.removeSelection(spannable)
        }
    }
    if (event.action == MotionEvent.ACTION_UP) {
        //手指抬起
        onUp(event, spannable)
    }
    return result
}

/**
 * 手指按下逻辑
 */
private fun onDown(spannable: Spannable, event: MotionEvent) {
    //按下时记下clickSpan
    mPressedSpan = getPressedSpan(this, spannable, event)
    if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
        result = true
        Selection.setSelection(
                spannable, spannable.getSpanStart(mPressedSpan),
                spannable.getSpanEnd(mPressedSpan)
        )
    } else {
        result = if (moreCanClick){
            super.onTouchEvent(event)
        }else{
            false
        }
    }
}

/**
 * 手指抬起逻辑
 */
private fun onUp(event: MotionEvent, spannable: Spannable?) {
    result = if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
        (mPressedSpan as MyClickSpan).onClick(this)
        true
    } else {
        if (moreCanClick) {
            super.onTouchEvent(event)
        }
        false
    }
    mPressedSpan = null
    Selection.removeSelection(spannable)
}

/**
 * 设置尾部...全文点击事件
 */
fun setOnAllSpanClickListener(
        onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener
) {
    this.OnAllSpanClickListener= onAllSpanClickListener
}

private fun getPressedSpan(
        textView: TextView, spannable: Spannable,
        event: MotionEvent
): ClickableSpan? {
    var mTouchSpan: ClickableSpan? = null

    var x = event.x.toInt()
    var y = event.y.toInt()
    x -= textView.totalPaddingLeft
    x += textView.scrollX
    y -= textView.totalPaddingTop
    y += textView.scrollY
    val layout = layout
    val line = layout.getLineForVertical(y)
    val off = layout.getOffsetForHorizontal(line, x.toFloat())

    val spans: Array =
            spannable.getSpans(
                    off, off,
                    MyClickSpan::class.java
            )
    if (spans.isNotEmpty()) {
        mTouchSpan = spans[0]
    } else {
        val linkSpans = spannable.getSpans(off, off, MyLinkClickSpan::class.java)
        if (linkSpans != null && linkSpans.isNotEmpty()) {
            mTouchSpan = linkSpans[0]
        }
    }
    return mTouchSpan
}

其中 if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) { //如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent return MyLinkMovementMethod.instance .onTouchEvent(this, text as Spannable, event) }是对链接的兼容处理,如果对这个有疑问请看我的上一篇关于链接描述的文章#Android 仿微博正文链接交互

三、完整代码

class ListMoreTextView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = R.attr.MoreTextViewStyle
) :
        AppCompatTextView(context, attrs, defStyleAttr) {

    /**
     * 最大行数
     */
    private var maxLine: Int

    private val moreTextSize: Int

    /**
     * 尾部更多文字
     */
    private val moreText: String?

    /**
     * 尾部更多文字颜色
     */
    private val moreTextColor: Int

    /**
     * 是否可以点击尾部更多文字
     */
    private val moreCanClick : Boolean

    private var mPaint: Paint? = null

    /**
     * 尾部更多文字点击事件接口回调
     */
    private var onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener? = null

    /**
     * 实现span的点击
     */
    private var mPressedSpan: ClickableSpan? = null
    private var result = false


    init {
        val array = getContext().obtainStyledAttributes(
            attrs,
            R.styleable.ListMoreTextView, defStyleAttr, 0
        )
        maxLine = array.getInt(R.styleable.MoreTextView_more_action_text_maxLines, Int.MAX_VALUE)
        moreText = array.getString(R.styleable.MoreTextView_more_action_text)
        moreTextSize = array.getInteger(R.styleable.MoreTextView_more_action_text_size, 13)
        moreTextColor = array.getColor(R.styleable.MoreTextView_more_action_text_color, Color.BLACK)
        moreCanClick = array.getBoolean(R.styleable.MoreTextView_more_can_click,false)
        array.recycle()
        init()
    }

    private fun init() {
        mPaint = paint
    }

    /**
     * 设置最大行数
     */
    fun setMaxLine (maxLine : Int){
        this.maxLine = maxLine
    }

    /**
     * 使用者主动调用
     * 如果有显示链接需求一定要调用此方法
     */
    fun setMovementMethodDefault() {
        movementMethod = MyLinkMovementMethod.instance
        highlightColor = Color.TRANSPARENT
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (lineCount > maxLine) {
            //如果大于设置的最大行数
            val (layout, stringBuilder, sb) = clipContent()
            stringBuilder.append(sb)
            setMeasuredDimension(measuredWidth, getDesiredHeight(layout))
            text = stringBuilder
        }
    }

    /**
     * 裁剪内容
     */
    private fun clipContent(): Triple {
        var offset = 1
        val layout = layout
        val staticLayout = StaticLayout(
                text,
                layout.paint,
                layout.width,
                Layout.Alignment.ALIGN_NORMAL,
                layout.spacingMultiplier,
                layout.spacingAdd,
                false
        )
        val indexEnd = staticLayout.getLineEnd(maxLine - 1)
        val tempText = text.subSequence(0, indexEnd)
        var offsetWidth =
                layout.paint.measureText(tempText[indexEnd - 1].toString()).toInt()
        val moreWidth =
                ceil(layout.paint.measureText(moreText).toDouble()).toInt()
        //表情字节个数
        var countEmoji = 0
        while (indexEnd > offset && offsetWidth <= moreWidth ) {
            //当前字节是否位表情
            val isEmoji = PublicMethod.isEmojiCharacter(tempText[indexEnd - offset])
            if (isEmoji){
                countEmoji += 1
            }
            offset++
            val pair = getOffsetWidth(
                    indexEnd,
                    offset,
                    tempText,
                    countEmoji,
                    offsetWidth,
                    layout,
                    moreWidth
            )
            offset = pair.first
            offsetWidth = pair.second
        }
        val ssbShrink = tempText.subSequence(0, indexEnd - offset)
        val stringBuilder = SpannableStringBuilder(ssbShrink)
        val sb = SpannableString(moreText)
        sb.setSpan(
                ForegroundColorSpan(moreTextColor), 3, sb.length,
                Spanned.SPAN_INCLUSIVE_INCLUSIVE
        )
        //设置字体大小
        sb.setSpan(
                AbsoluteSizeSpan(moreTextSize, true), 3, sb.length,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )
        if (moreCanClick){
            //设置点击事件
            sb.setSpan(
                    MyClickSpan(context, onAllSpanClickListener), 3, sb.length,
                    Spanned.SPAN_INCLUSIVE_INCLUSIVE
            )
        }
        return Triple(layout, stringBuilder, sb)
    }

    private fun getOffsetWidth(
            indexEnd: Int,
            offset: Int,
            tempText: CharSequence,
            countEmoji: Int,
            offsetWidth: Int,
            layout: Layout,
            moreWidth: Int
    ): Pair {
        var offset1 = offset
        var offsetWidth1 = offsetWidth
        if (indexEnd > offset1) {
            val text = tempText[indexEnd - offset1 - 1].toString().trim()
            if (text.isNotEmpty() && countEmoji % 2 == 0) {
                val charText = tempText[indexEnd - offset1]
                offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
                //一个表情两个字符,避免截取一半字符出现乱码或者显示不全...全文
                if (offsetWidth1 > moreWidth && PublicMethod.isEmojiCharacter(charText)) {
                    offset1++
                }
            }
        } else {
            val charText = tempText[indexEnd - offset1]
            offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
        }
        return Pair(offset1, offsetWidth1)
    }

    /**
     * 获取内容高度
     */
    private fun getDesiredHeight(layout: Layout?): Int {
        if (layout == null) {
            return 0
        }
        val lineTop: Int
        val lineCount = layout.lineCount
        val compoundPaddingTop = compoundPaddingTop + compoundPaddingBottom - lineSpacingExtra.toInt()
        lineTop = when {
            lineCount > maxLine -> {
                //文字行数超过最大行
                layout.getLineTop(maxLine)
            }
            else -> {
                layout.getLineTop(lineCount)
            }
        }
        return (lineTop + compoundPaddingTop).coerceAtLeast(suggestedMinimumHeight)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val text = text
        val spannable = Spannable.Factory.getInstance().newSpannable(text)

        if (event.action == MotionEvent.ACTION_DOWN) {
            //手指按下
            onDown(spannable, event)
        }

        if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) {
            //如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent
            return MyLinkMovementMethod.instance
                    .onTouchEvent(this, text as Spannable, event)
        }

        if (event.action == MotionEvent.ACTION_MOVE) {
            //手指移动
            val mClickSpan = getPressedSpan(this, spannable, event)
            if (mPressedSpan != null && mPressedSpan !== mClickSpan) {
                mPressedSpan = null
                Selection.removeSelection(spannable)
            }
        }
        if (event.action == MotionEvent.ACTION_UP) {
            //手指抬起
            onUp(event, spannable)
        }
        return result
    }

    /**
     * 手指按下逻辑
     */
    private fun onDown(spannable: Spannable, event: MotionEvent) {
        //按下时记下clickSpan
        mPressedSpan = getPressedSpan(this, spannable, event)
        if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
            result = true
            Selection.setSelection(
                    spannable, spannable.getSpanStart(mPressedSpan),
                    spannable.getSpanEnd(mPressedSpan)
            )
        } else {
            result = if (moreCanClick){
                super.onTouchEvent(event)
            }else{
                false
            }
        }
    }

    /**
     * 手指抬起逻辑
     */
    private fun onUp(event: MotionEvent, spannable: Spannable?) {
        result = if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
            (mPressedSpan as MyClickSpan).onClick(this)
            true
        } else {
            if (moreCanClick) {
                super.onTouchEvent(event)
            }
            false
        }
        mPressedSpan = null
        Selection.removeSelection(spannable)
    }

    /**
     * 设置尾部...全文点击事件
     */
    fun setOnAllSpanClickListener(
            onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener
    ) {
        this.OnAllSpanClickListener= onAllSpanClickListener
    }

    private fun getPressedSpan(
            textView: TextView, spannable: Spannable,
            event: MotionEvent
    ): ClickableSpan? {
        var mTouchSpan: ClickableSpan? = null

        var x = event.x.toInt()
        var y = event.y.toInt()
        x -= textView.totalPaddingLeft
        x += textView.scrollX
        y -= textView.totalPaddingTop
        y += textView.scrollY
        val layout = layout
        val line = layout.getLineForVertical(y)
        val off = layout.getOffsetForHorizontal(line, x.toFloat())

        val spans: Array =
                spannable.getSpans(
                        off, off,
                        MyClickSpan::class.java
                )
        if (spans.isNotEmpty()) {
            mTouchSpan = spans[0]
        } else {
            val linkSpans = spannable.getSpans(off, off, MyLinkClickSpan::class.java)
            if (linkSpans != null && linkSpans.isNotEmpty()) {
                mTouchSpan = linkSpans[0]
            }
        }
        return mTouchSpan
    }
}

    
    
    
    
    

注意:如果是有链接需求要主动调用该方法,否则链接的触摸交互无效。

/**
 * 使用者主动调用
 * 如果有显示链接需求一定要调用此方法
 */
fun setMovementMethodDefault() {
    movementMethod = MyLinkMovementMethod.instance
    highlightColor = Color.TRANSPARENT
}

另外,这里没有对内容连续换行的处理,因为个人觉得列表数据是对主要内容的显示,另外客户端不要做太多的数据处理的耗时操作,应该是由后端的同学或者产品设计时避免这种情况的产生。

四、效果

五、代码地址

点击获取


作者:笑慢
链接:https://juejin.cn/post/7037416456782348295
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


推荐阅读
  • POJ 1046 Color Me Less
    ColorMeLessTimeLimit: 1000MS MemoryLimit: 10000KTotalSubmissions: 31449 Accept ... [详细]
  • PS网页设计教程VIII——在Photoshop中设计不同寻常布局
    作为编码者,美工基础是偏弱的。我们可以参考一些成熟的网页PS教程,提高自身的设计能力。套用一句话,“熟读唐诗三百首,不会作诗也会吟”。本系列的教程来源于网上的PS教程,都是国外的,全英文的。本人尝 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • 求解连通树的最小长度及优化
    本文介绍了求解连通树的最小长度的方法,并通过四边形不等式进行了优化。具体方法为使用状态转移方程求解树的最小长度,并通过四边形不等式进行优化。 ... [详细]
  • Flutter 布局(四) Baseline、FractionallySizedBox、IntrinsicHeight、IntrinsicWidth详解
    本文主要介绍Flutter布局中的Baseline、FractionallySizedBox、IntrinsicHeight、IntrinsicWidth四种控件,详细介绍了其布局 ... [详细]
  • Highcharts翻译系列之二十:曲线图例子(二)
    Highcharts翻译系列之二十:曲线图例子(二)代码 ... [详细]
  • 20210304力扣 根据前序遍历和中序遍历确定二叉树 快速排序思想
    力扣根据前序遍历和中序遍历确定二叉树基本思路前序遍历确定根节点是哪个(第一个就是根节点)中序遍历根据已知根节点确定左右子树的元素组成根节点左左子树根节点右右子树再根据前序遍历确定左 ... [详细]
  • 记录一些 Latex 的技巧
    Latex一些技巧:1.如何创建不浮动的的figure和table\makeatletter\newcommand{\figcaption}{\def\captyp ... [详细]
  • 我正在使用ChemDoodleWebComponents在网页上显示分子。基本上,我可以在我的页面中插入以下脚本,它将创建一个HTML5canvas元素来显示分子。vartrans ... [详细]
author-avatar
手机用户2502892083
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有