diff --git a/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java b/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java index 6fd862d8..46921ab7 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/qqface/QMUIQQFaceView.java @@ -29,7 +29,6 @@ import android.os.Build; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; @@ -883,9 +882,6 @@ public class QMUIQQFaceView extends View { break; } setMeasuredDimension(width, height); - Log.v(TAG, "mLines = " + mLines + " ; width = " + width + " ; height = " - + height + " ; maxLine = " + maxLine + "; measure time = " - + (System.currentTimeMillis() - start)); } @Override @@ -894,15 +890,12 @@ public class QMUIQQFaceView extends View { return; } pickTextPaintColor(); - - long start = System.currentTimeMillis(); List elements = mElementList.getElements(); mCurrentDrawBaseLine = getPaddingTop() + mFirstBaseLine; mCurrentDrawLine = 1; setStartDrawUsedWidth(getPaddingLeft(), getWidth() - getPaddingLeft() - getPaddingRight()); mIsExecutedMiddleEllipsize = false; drawElements(canvas, elements, getWidth() - getPaddingLeft() - getPaddingRight()); - Log.v(TAG, "onDraw spend time = " + (System.currentTimeMillis() - start)); } private void pickTextPaintColor() { diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.java index f48e5192..24806b25 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.java +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QDQQFaceUsageFragment.java @@ -17,26 +17,34 @@ package com.qmuiteam.qmuidemo.fragment.components.qqface; import android.graphics.Color; +import android.os.Build; import android.text.SpannableString; import android.text.Spanned; +import android.text.TextUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; +import androidx.core.content.ContextCompat; + import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord; import com.qmuiteam.qmui.qqface.QMUIQQFaceView; import com.qmuiteam.qmui.span.QMUITouchableSpan; +import com.qmuiteam.qmui.type.LineLayout; +import com.qmuiteam.qmui.type.parser.EmojiTextParser; +import com.qmuiteam.qmui.type.parser.TextParser; +import com.qmuiteam.qmui.type.view.LineTypeView; +import com.qmuiteam.qmui.type.view.MarqueeTypeView; import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmui.widget.QMUITopBar; import com.qmuiteam.qmui.widget.QMUITopBarLayout; -import com.qmuiteam.qmuidemo.manager.QDDataManager; -import com.qmuiteam.qmuidemo.base.BaseFragment; +import com.qmuiteam.qmuidemo.QDQQFaceManager; import com.qmuiteam.qmuidemo.R; +import com.qmuiteam.qmuidemo.base.BaseFragment; import com.qmuiteam.qmuidemo.lib.Group; import com.qmuiteam.qmuidemo.lib.annotation.Widget; +import com.qmuiteam.qmuidemo.manager.QDDataManager; -import androidx.core.content.ContextCompat; import butterknife.BindView; import butterknife.ButterKnife; @@ -49,6 +57,9 @@ import butterknife.ButterKnife; @LatestVisitRecord public class QDQQFaceUsageFragment extends BaseFragment { @BindView(R.id.topbar) QMUITopBarLayout mTopBar; + @BindView(R.id.marquee1) MarqueeTypeView mMarqueeTypeView1; + @BindView(R.id.marquee2) MarqueeTypeView mMarqueeTypeView2; + @BindView(R.id.line_type_1) LineTypeView mLineType1; @BindView(R.id.qqface1) QMUIQQFaceView mQQFace1; @BindView(R.id.qqface2) QMUIQQFaceView mQQFace2; @BindView(R.id.qqface3) QMUIQQFaceView mQQFace3; @@ -89,6 +100,31 @@ public class QDQQFaceUsageFragment extends BaseFragment { } private void initData() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ + TextParser textParser = new EmojiTextParser(QDQQFaceManager.getInstance()); + mMarqueeTypeView1.setFadeWidth(QMUIDisplayHelper.dp2px(getContext(), 40)); + mMarqueeTypeView1.setTextParser(textParser); + mMarqueeTypeView1.setText("飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀飘呀这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示"); + mMarqueeTypeView2.setFadeWidth(QMUIDisplayHelper.dp2px(getContext(), 40)); + mMarqueeTypeView2.setTextParser(textParser); + mMarqueeTypeView2.setText("[大哭]我太短了,实在是飘不动了"); + + mLineType1.setTextParser(textParser); + LineLayout lineLayout = mLineType1.getLineLayout(); + lineLayout.setMaxLines(6); + lineLayout.setEllipsize(TextUtils.TruncateAt.END); + lineLayout.setMoreText("更多"); + lineLayout.setMoreUnderlineHeight(QMUIDisplayHelper.dp2px(getContext(), 2)); + lineLayout.setMoreTextColor(Color.RED); + lineLayout.setMoreUnderlineColor(Color.BLUE); + mLineType1.setLineSpace(QMUIDisplayHelper.dp2px(getContext(), 5)); + mLineType1.setTextColor(Color.BLACK); + mLineType1.setTextSize(QMUIDisplayHelper.sp2px(getContext(), 15)); + mLineType1.setTextParser(textParser); + mLineType1.setText("QMUI Android 的设计[微笑]目的是用于辅助快速搭建一个具备基本设计还原[微笑]效果的 Android 项目," + + "同时利用自身[微笑]提供的丰富控件及兼容处理,让开[微笑]发者能专注于业务需求而无需耗费[微笑]精力在基础代[微笑]码的设计上。" + + "不管是新项目的创建,或是已有项[微笑]目的维护,均可使开[微笑]发效率和项目[微笑]质量得到大幅度提升。"); + } mQQFace1.setText("这是一行很长很长[微笑][微笑][微笑][微笑]的文本,但是[微笑][微笑][微笑][微笑]只能单行显示"); mQQFace2.setText("这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + "这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" + diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QMUIQQFaceView2.java b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QMUIQQFaceView2.java deleted file mode 100644 index adbd1157..00000000 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/components/qqface/QMUIQQFaceView2.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making QMUI_Android available. - * - * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. - * - * Licensed under the MIT License (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * - * http://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is - * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, - * either express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.qmuiteam.qmuidemo.fragment.components.qqface; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Typeface; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; - -import androidx.annotation.Nullable; - -import com.qmuiteam.qmui.type.LineLayout; -import com.qmuiteam.qmui.type.TypeEnvironment; -import com.qmuiteam.qmui.type.TypeModel; -import com.qmuiteam.qmui.type.parser.EmojiTextParser; -import com.qmuiteam.qmui.type.parser.TextParser; -import com.qmuiteam.qmui.util.QMUIDisplayHelper; -import com.qmuiteam.qmuidemo.QDQQFaceManager; - -import java.util.ArrayList; -import java.util.List; - -public class QMUIQQFaceView2 extends View { - - private TypeEnvironment mTypeEnvironment = new TypeEnvironment(); - private LineLayout mLineLayout = new LineLayout(mTypeEnvironment) - .setCalculateWholeLines(true) - .setEllipsize(TextUtils.TruncateAt.MIDDLE) - .setMoreText("更多", Color.RED, Typeface.DEFAULT_BOLD) - .setMaxLines(10); - private TextParser mTextParser = new EmojiTextParser(QDQQFaceManager.getInstance()); - private List mEffectRemovers = new ArrayList<>(); - - public QMUIQQFaceView2(Context context) { - this(context, null); - } - - public QMUIQQFaceView2(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - mTypeEnvironment.setTextSize(QMUIDisplayHelper.dp2px(context, 16)); - mTypeEnvironment.setLineSpace(QMUIDisplayHelper.dp2px(context, 5)); - mTypeEnvironment.setParagraphSpace(QMUIDisplayHelper.dp2px(context, 15)); - mTypeEnvironment.setAlignment(TypeEnvironment.Alignment.JUSTIFY); - TypeModel typeModel = mTextParser.parse("QMUI 换肤最原始是为了适配 Dark Mode。" + - "但作为框架的实现者,就需要考虑到更通用的使用形式,并且要尽可能保证 API 的简" + - "洁性。因而 QMUI 是支持多套肤色的切换,而 [微笑]Dark Mode 只是其中的一种。\n" + - "在无需重启 Activity 的前提下,我们做[流泪]换肤框架的实现思路其实是很简单的:" + - "就是当触发换肤时,遍历 View 树 来更新 View 的肤色相关的属性。基于这一" + - "思路,组件的意义就在于利用数据结构、设[色]计模式、系统 API等来简化封装出一套" + - "足够方便的使用接口,避免业务使用时为了完成功[大哭]能而堆砌一堆 if else 代码。" + - "写组件也不是一个高大尚的事情,在业务开发过程中,我们应该尽可能多思考,构建一" + - "些好用的组件,持续锻炼,才能逐渐 Hold 住越来越强大的组件。\n此外,在业务开发之" + - "余,我们需要多读一些源码,如果你一直[大哭]走业务线,可能不会发[大哭]觉阅读源码的作用,而" + - "如果你有尝试封装组件,那[大哭]么这些优秀的库往[大哭]往会给你思路的启迪。如果是两年前的我" + - "来写换肤框架,我写出来的框[大哭]架可能比现在差得很远。如果你阅[大哭]读本文,也期望能给你以" + - "启迪。"); - - mLineLayout.setTypeModel(typeModel); - - setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if(!mEffectRemovers.isEmpty()){ - for(TypeModel.EffectRemover remover: mEffectRemovers){ - remover.remove(); - } - mEffectRemovers.clear(); - invalidate(); - }else{ - addBgEffect(); - addTextColorEffect(); - addUnderLineEffect(); - } - } - }); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - mTypeEnvironment.setMeasureLimit(widthSize, heightSize); - mLineLayout.measureAndLayout(); - setMeasuredDimension(widthSize, mLineLayout.getContentHeight()); - } - - public void addBgEffect(){ - TypeModel typeModel = mLineLayout.getTypeModel(); - if(typeModel != null){ - TypeModel.EffectRemover remover = typeModel.addBgEffect(10, 40, Color.YELLOW); - mEffectRemovers.add(remover); - invalidate(); - } - } - - public void addTextColorEffect(){ - TypeModel typeModel = mLineLayout.getTypeModel(); - if(typeModel != null){ - TypeModel.EffectRemover remover = typeModel.addTextColorEffect(20, 50, Color.RED); - mEffectRemovers.add(remover); - invalidate(); - } - } - - public void addUnderLineEffect(){ - TypeModel typeModel = mLineLayout.getTypeModel(); - if(typeModel != null){ - TypeModel.EffectRemover remover = typeModel.addUnderLineEffect(25, 60, Color.BLUE, QMUIDisplayHelper.dp2px(getContext(), 3)); - mEffectRemovers.add(remover); - invalidate(); - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - mLineLayout.draw(canvas); - } -} diff --git a/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml b/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml index 4cc1e177..9b2e7eca 100644 --- a/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml +++ b/qmuidemo/src/main/res/layout/fragment_qqface_layout.xml @@ -46,14 +46,35 @@ android:orientation="vertical" android:padding="25dp"> - + + + + + + + + + android:text="单行表情,末尾省略" + android:layout_marginTop="20dp"/> = ArrayList() - private var mDropLastIfSpace = true - private var mMoreText: String? = null - private var mMoreTextColor = 0 - private var mMoreTextTypeface: Typeface? = null - private var mMoreUnderlineColor = Color.TRANSPARENT - private var mMoreBgColor = 0 - private var mMoreUnderlineHeight = 0 - private var mTotalLineCount = 0 - +class LineLayout { + var maxLines = Int.MAX_VALUE + var ellipsize: TruncateAt? = null + var calculateWholeLines = false + var dropLastIfSpace = true + var moreText: String? = null + var moreTextColor = 0 + var moreTextTypeface: Typeface? = null + var moreUnderlineColor = Color.TRANSPARENT + var moreBgColor = 0 + var moreUnderlineHeight = 0 var typeModel: TypeModel? = null + + + private val mLines: MutableList = ArrayList() + + + var totalLineCount = 0 private set - fun setMaxLines(maxLines: Int): LineLayout { - mMaxLines = maxLines - return this - } - fun setEllipsize(ellipsize: TruncateAt?): LineLayout { - mEllipsize = ellipsize - return this - } - fun setCalculateWholeLines(calculateWholeLines: Boolean): LineLayout { - mCalculateWholeLines = calculateWholeLines - return this - } - - fun setDropLastIfSpace(dropLastIfSpace: Boolean): LineLayout { - mDropLastIfSpace = dropLastIfSpace - return this - } - - fun setMoreText(text: String?, color: Int, typeface: Typeface?): LineLayout { - mMoreText = text - mMoreTextColor = color - mMoreTextTypeface = typeface - return this - } - - fun setMoreBackgroundColor(color: Int): LineLayout { - mMoreBgColor = color - return this - } - - fun setUnderline(height: Int, color: Int): LineLayout { - mMoreUnderlineHeight = height - mMoreUnderlineColor = color - return this - } - - fun setTypeModel(typeModel: TypeModel?): LineLayout { - this.typeModel = typeModel - return this - } - - fun measureAndLayout() { - mTypeEnvironment.clear() + fun measureAndLayout(env: TypeEnvironment) { + env.clear() release() if (typeModel == null) { return } - var element: Element? = typeModel!!.firstElement() ?: return + var element: Element? = typeModel!!.firstElement() var line = Line.acquire() var y = 0 - line.init(0, y, mTypeEnvironment.widthLimit) + line.init(0, y, env.widthLimit) while (element != null) { - element.measure(mTypeEnvironment) + element.measure(env) if (element is NextParagraphElement) { line.add(element) - line.layout(mTypeEnvironment, mDropLastIfSpace, false) + line.layout(env, dropLastIfSpace, false) mLines.add(line) if (canInterrupt()) { return } - y += line.contentHeight + mTypeEnvironment.paragraphSpace - line = createNewLine(y) - } else if (line.contentWidth + element.measureWidth > mTypeEnvironment.widthLimit) { + y += line.contentHeight + env.paragraphSpace + line = createNewLine(env, y) + } else if (line.contentWidth + element.measureWidth > env.widthLimit) { if (mLines.size == 0 && line.size == 0) { // the width is too small. line.release() return } - val back = line.handleWordBreak(mTypeEnvironment) - line.layout(mTypeEnvironment, mDropLastIfSpace, false) + val back = line.handleWordBreak(env) + line.layout(env, dropLastIfSpace, false) mLines.add(line) if (canInterrupt()) { - handleEllipse(true) + handleEllipse(env,true) return } - y += line.contentHeight + mTypeEnvironment.lineSpace - line = createNewLine(y) + y += line.contentHeight + env.lineSpace + line = createNewLine(env, y) if (back != null && !back.isEmpty()) { for (el in back) { line.add(el) @@ -130,37 +92,39 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { element = element.next } if (line.size > 0) { - line.layout(mTypeEnvironment, mDropLastIfSpace, true) + line.layout(env, dropLastIfSpace, true) mLines.add(line) } else { line.release() } - mTotalLineCount = mLines.size - handleEllipse(false) + totalLineCount = mLines.size + handleEllipse(env,false) } - private fun createNewLine(y: Int): Line { + private fun createNewLine(env: TypeEnvironment, y: Int): Line { val line = Line.acquire() - line.init(0, y, mTypeEnvironment.widthLimit) + line.init(0, y, env.widthLimit) return line } - private fun handleEllipse(fromInterrupt: Boolean) { - if (mLines.isEmpty() || mLines.size < mMaxLines || mLines.size == mMaxLines && !fromInterrupt) { + private fun handleEllipse(env: TypeEnvironment, fromInterrupt: Boolean) { + if (mLines.isEmpty() || mLines.size < maxLines || mLines.size == maxLines && !fromInterrupt) { return } - if (mEllipsize == TruncateAt.END) { - handleEllipseEnd() - } else if (mEllipsize == TruncateAt.START) { - handleEllipseStart() - } else if (mEllipsize == TruncateAt.MIDDLE) { - handleEllipseMiddle() + if (ellipsize == TruncateAt.END) { + handleEllipseEnd(env) + } else if (ellipsize == TruncateAt.START) { + handleEllipseStart(env) + } else if (ellipsize == TruncateAt.MIDDLE) { + handleEllipseMiddle(env) } } - private fun handleEllipseEnd() { - for (i in mLines.size - 1 downTo mMaxLines) { - mLines.remove(mLines[i]) + private fun handleEllipseEnd(env: TypeEnvironment) { + for (i in mLines.size - 1 downTo maxLines) { + val line = mLines[i] + mLines.remove(line) + line.release() } val lastLine = mLines[mLines.size - 1] var limitWidth = lastLine.widthLimit @@ -170,11 +134,11 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { env.clear() } }) - ellipseElement.measure(mTypeEnvironment) + ellipseElement.measure(env) limitWidth = (limitWidth - ellipseElement.measureWidth).toInt() var moreElement: Element? = null - if (mMoreText != null && !mMoreText!!.isEmpty()) { - moreElement = TextElement(mMoreText!!, -1, -1) + if (moreText != null && !moreText!!.isEmpty()) { + moreElement = TextElement(moreText!!, -1, -1) val changeTypes: MutableList = ArrayList() changeTypes.add(TypeEnvironment.TYPE_TEXT_COLOR) changeTypes.add(TypeEnvironment.TYPE_BG_COLOR) @@ -183,21 +147,21 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { changeTypes.add(TypeEnvironment.TYPE_BORDER_BOTTOM_WIDTH) moreElement.addSingleEnvironmentUpdater(changeTypes, object : EnvironmentUpdater { override fun update(env: TypeEnvironment) { - if (mMoreTextColor != 0) { - env.textColor = mMoreTextColor + if (moreTextColor != 0) { + env.textColor = moreTextColor } - if (mMoreBgColor != 0) { - env.backgroundColor = mMoreBgColor + if (moreBgColor != 0) { + env.backgroundColor = moreBgColor } - if (mMoreTextTypeface != null) { - env.typeface = mMoreTextTypeface + if (moreTextTypeface != null) { + env.typeface = moreTextTypeface } - if (mMoreUnderlineHeight > 0) { - env.setBorderBottom(mMoreUnderlineHeight, mMoreUnderlineColor) + if (moreUnderlineHeight > 0) { + env.setBorderBottom(moreUnderlineHeight, moreUnderlineColor) } } }) - moreElement.measure(mTypeEnvironment) + moreElement.measure(env) limitWidth = (limitWidth - moreElement.measureWidth).toInt() } val contentWidth = lastLine.contentWidth @@ -208,7 +172,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { for (el in elements) { if (el.measureWidth <= limitWidth) { lastLine.add(el) - limitWidth -= (limitWidth - el.measureWidth).toInt() + limitWidth = (limitWidth - el.measureWidth).toInt() } else { break } @@ -218,12 +182,12 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { if (moreElement != null) { lastLine.add(moreElement) } - lastLine.layout(mTypeEnvironment, mDropLastIfSpace, true) + lastLine.layout(env, dropLastIfSpace, true) } - private fun handleEllipseStart() { - mTypeEnvironment.clear() - for (i in mLines.size - 1 downTo mMaxLines) { + private fun handleEllipseStart(env: TypeEnvironment) { + env.clear() + for (i in mLines.size - 1 downTo maxLines) { mLines.remove(mLines[i]) } val ellipseElement: Element = TextElement("...", -1, -1) @@ -232,7 +196,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { env.clear() } }) - ellipseElement.measure(mTypeEnvironment) + ellipseElement.measure(env) val elements: Queue = LinkedList() elements.add(ellipseElement) for (i in mLines.indices) { @@ -245,7 +209,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { if (el is NextParagraphElement) { elements.poll() line.add(el) - el.move(mTypeEnvironment) + el.move(env) break } if (el is BreakWordLineElement) { @@ -255,7 +219,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { if (line.contentWidth + el.measureWidth <= limitWidth) { elements.poll() line.add(el) - el.move(mTypeEnvironment) + el.move(env) } else { break } @@ -263,21 +227,21 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { elements.poll() } } - line.handleWordBreak(mTypeEnvironment) - line.layout(mTypeEnvironment, mDropLastIfSpace, false) + line.handleWordBreak(env) + line.layout(env, dropLastIfSpace, false) if (elements.isEmpty()) { return } } } - private fun handleEllipseMiddle() { - mTypeEnvironment.clear() + private fun handleEllipseMiddle(env: TypeEnvironment) { + env.clear() val lines: List = ArrayList(mLines) mLines.clear() val ellipseElement: Element = TextElement("...", -1, -1) - ellipseElement.measure(mTypeEnvironment) - val ellipseLine = if (mMaxLines % 2 == 0) mMaxLines / 2 else (mMaxLines + 1) / 2 + ellipseElement.measure(env) + val ellipseLine = if (maxLines % 2 == 0) maxLines / 2 else (maxLines + 1) / 2 for (i in 0 until ellipseLine) { mLines.add(lines[i]) } @@ -290,7 +254,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { if (handleLine.contentWidth + el.measureWidth <= limitWidth / 2f - ellipseElement.measureWidth / 2) { unHandled.poll() handleLine.add(el) - el.move(mTypeEnvironment) + el.move(env) } else { break } @@ -298,9 +262,9 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { unHandled.poll() } } - ellipseElement.measure(mTypeEnvironment) + ellipseElement.measure(env) handleLine.add(ellipseElement) - val nextFullShowLine = lines.size - mMaxLines + ellipseLine + val nextFullShowLine = lines.size - maxLines + ellipseLine var startLine = lines.size - 1 // find the latest paragraph end line. for (i in lines.size - 2 downTo nextFullShowLine + 1) { @@ -372,29 +336,29 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { } if (ellipseEffect.size > 0) { val ignoreEffectElement = IgnoreEffectElement(ellipseEffect) - ignoreEffectElement.move(mTypeEnvironment) + ignoreEffectElement.move(env) handleLine.add(ignoreEffectElement) } } for (el in toAdd) { - el.move(mTypeEnvironment) + el.move(env) handleLine.add(el) } - handleLine.handleWordBreak(mTypeEnvironment) - handleLine.layout(mTypeEnvironment, mDropLastIfSpace, ellipseLine == lines.size) + handleLine.handleWordBreak(env) + handleLine.layout(env, dropLastIfSpace, ellipseLine == lines.size) var lastEnd = handleLine.y + handleLine.contentHeight for (i in nextFullShowLine until lines.size) { val line = lines[i] val prev = lines[i - 1] if (prev.isMiddleParagraphEndLine) { - line.y = lastEnd + mTypeEnvironment.paragraphSpace + line.y = lastEnd + env.paragraphSpace } else { - line.y = lastEnd + mTypeEnvironment.lineSpace + line.y = lastEnd + env.lineSpace } lastEnd = line.y + line.contentHeight - line.move(mTypeEnvironment) - line.handleWordBreak(mTypeEnvironment) - line.layout(mTypeEnvironment, mDropLastIfSpace, i == lines.size - 1) + line.move(env) + line.handleWordBreak(env) + line.layout(env, dropLastIfSpace, i == lines.size - 1) mLines.add(line) } } @@ -416,16 +380,16 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) { return last.y + last.contentHeight } - fun draw(canvas: Canvas) { - mTypeEnvironment.clear() + fun draw(canvas: Canvas, env: TypeEnvironment) { + env.clear() for (line in mLines) { - line.draw(mTypeEnvironment, canvas) + line.draw(env, canvas) } } private fun canInterrupt(): Boolean { - return mLines.size == mMaxLines && !mCalculateWholeLines && - (mEllipsize == null || mEllipsize == TruncateAt.END) + return mLines.size == maxLines && !calculateWholeLines && + (ellipsize == null || ellipsize == TruncateAt.END) } fun release() { diff --git a/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.kt b/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.kt index 3159b0b6..611a5c16 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.kt +++ b/type/src/main/java/com/qmuiteam/qmui/type/TypeEnvironment.kt @@ -62,6 +62,7 @@ class TypeEnvironment { val paint = Paint().apply { isAntiAlias = true + textSize = Resources.getSystem().displayMetrics.scaledDensity * 14f } val bgPaint = Paint().apply { isAntiAlias = true @@ -81,7 +82,7 @@ class TypeEnvironment { paint.typeface = value } - var textSize: Float = 0f + var textSize: Float = paint.textSize set(value) { field = value paint.textSize = value @@ -98,7 +99,7 @@ class TypeEnvironment { } fun setCustomProp(type: Int, value: Any?) { - mCustomProp!!.put(type, value) + mCustomProp.put(type, value) } fun setBorderTop(width: Int, color: Int) { @@ -257,4 +258,14 @@ class TypeEnvironment { } } } + + fun isRunning(): Boolean{ + for (i in 0 until mStack.size()) { + val stack = mStack.valueAt(i) + if(stack.size > 0){ + return true + } + } + return false + } } \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.kt b/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.kt index ebe7acc5..8e2419a3 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.kt +++ b/type/src/main/java/com/qmuiteam/qmui/type/TypeModel.kt @@ -15,6 +15,7 @@ */ package com.qmuiteam.qmui.type +import android.graphics.Typeface import com.qmuiteam.qmui.type.element.Element import java.util.* @@ -26,6 +27,26 @@ class TypeModel( var firstEffect: Element? = null + fun addTypefaceEffect(start: Int, end: Int, typeface: Typeface): EffectRemover? { + val types: MutableList = ArrayList() + types.add(TypeEnvironment.TYPE_TYPEFACE) + return unsafeAddEffect(start, end, types, object : EnvironmentUpdater { + override fun update(env: TypeEnvironment) { + env.typeface = typeface + } + }) + } + + fun addTextSizeEffect(start: Int, end: Int, textSize: Float): EffectRemover? { + val types: MutableList = ArrayList() + types.add(TypeEnvironment.TYPE_TEXT_SIZE) + return unsafeAddEffect(start, end, types, object : EnvironmentUpdater { + override fun update(env: TypeEnvironment) { + env.textSize = textSize + } + }) + } + fun addBgEffect(start: Int, end: Int, bgColor: Int): EffectRemover? { val types: MutableList = ArrayList() types.add(TypeEnvironment.TYPE_BG_COLOR) diff --git a/type/src/main/java/com/qmuiteam/qmui/type/element/Element.kt b/type/src/main/java/com/qmuiteam/qmui/type/element/Element.kt index 13730fca..a66dacd0 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/element/Element.kt +++ b/type/src/main/java/com/qmuiteam/qmui/type/element/Element.kt @@ -260,9 +260,9 @@ abstract class Element(val text: CharSequence, val index: Int, val start: Int) { saveTypeList?.forEach { env.save(it) } -// environmentUpdaterList?.forEach { -// it.update(env) -// } + environmentUpdaterList?.forEach { + it.update(env) + } } fun restoreEnv(env: TypeEnvironment) { diff --git a/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.kt b/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.kt index 66233929..14b800cb 100644 --- a/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.kt +++ b/type/src/main/java/com/qmuiteam/qmui/type/parser/PlainTextParser.kt @@ -23,6 +23,13 @@ import com.qmuiteam.qmui.type.parser.ParserHelper.handleWordPart import java.util.* class PlainTextParser : TextParser { + + companion object { + val instance by lazy { + PlainTextParser() + } + } + override fun parse(text: CharSequence?): TypeModel? { if (text == null || text.isEmpty()) { return null diff --git a/type/src/main/java/com/qmuiteam/qmui/type/view/BaseTypeView.kt b/type/src/main/java/com/qmuiteam/qmui/type/view/BaseTypeView.kt new file mode 100644 index 00000000..1c608e9f --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/view/BaseTypeView.kt @@ -0,0 +1,86 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.qmuiteam.qmui.type.view + +import android.content.Context +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.View +import com.qmuiteam.qmui.type.TypeEnvironment + +open class BaseTypeView(context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0): View(context, attrs, defStyleAttr) { + val environment = TypeEnvironment() + + var textSize: Float + get() = environment.textSize + set(value){ + throwIfRunning("setTextSize") + if(environment.textSize != value){ + environment.textSize = value + requestLayout() + } + } + + var textColor: Int + get() = environment.textColor + set(value){ + throwIfRunning("setTextColor") + if(environment.textColor != value){ + environment.textColor = value + invalidate() + } + } + + var typeface: Typeface? + get() = environment.typeface + set(value){ + throwIfRunning("setTypeface") + if(environment.typeface != value){ + environment.typeface = value + requestLayout() + } + } + + var lineSpace: Int + get() = environment.lineSpace + set(value){ + throwIfRunning("setLineSpace") + if(environment.lineSpace != value){ + environment.lineSpace = value + requestLayout() + } + } + + var paragraphSpace: Int + get() = environment.paragraphSpace + set(value){ + throwIfRunning("setParagraphSpace") + if(environment.paragraphSpace != value){ + environment.paragraphSpace = value + requestLayout() + } + } + + + fun throwIfRunning(action: String) { + if(environment.isRunning()){ + throw RuntimeException("can not perform $action when running.") + } + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/view/LineTypeView.kt b/type/src/main/java/com/qmuiteam/qmui/type/view/LineTypeView.kt new file mode 100644 index 00000000..8ab0b32c --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/view/LineTypeView.kt @@ -0,0 +1,121 @@ +/* + * Tencent is pleased to support the open source community by making QMUI_Android available. + * + * Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.qmuiteam.qmui.type.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Typeface +import android.text.TextUtils +import android.util.AttributeSet +import androidx.annotation.ColorInt +import com.qmuiteam.qmui.type.LineLayout +import com.qmuiteam.qmui.type.TypeModel +import com.qmuiteam.qmui.type.parser.PlainTextParser +import com.qmuiteam.qmui.type.parser.TextParser + +open class LineTypeView : BaseTypeView { + + val lineLayout = LineLayout() + + var textParser: TextParser = PlainTextParser.instance + set(value) { + if (field != value) { + field = value + lineLayout.typeModel = value.parse(text) + requestLayout() + } + } + + constructor(context: Context): super(context) + constructor(context: Context, attrs: AttributeSet?): super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + environment.setMeasureLimit(widthSize, heightSize) + lineLayout.measureAndLayout(environment) + val usedWidth = if (widthMode == MeasureSpec.AT_MOST) { + lineLayout.maxLayoutWidth + } else widthSize + val usedHeight = if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + lineLayout.contentHeight + } else heightSize + setMeasuredDimension(usedWidth, usedHeight) + } + + var text: CharSequence? = null + set(value) { + if (field != value) { + field = value + lineLayout.typeModel = textParser.parse(value) + requestLayout() + } + } + + var ellipsized: TextUtils.TruncateAt? + get() = lineLayout.ellipsize + set(value) { + if (lineLayout.ellipsize != value) { + lineLayout.ellipsize = value + requestLayout() + } + } + + + fun addBgEffect(start: Int, end: Int, @ColorInt color: Int): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addBgEffect(start, end, color) ?: return null + invalidate() + return remover + } + + fun addTextColorEffect(start: Int, end: Int, @ColorInt color: Int): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addTextColorEffect(start, end, color) ?: return null + invalidate() + return remover + } + + fun addUnderLineEffect(start: Int, end: Int, @ColorInt color: Int, height: Int): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addUnderLineEffect(start, end, color, height) ?: return null + invalidate() + return remover + } + + fun addTypefaceEffect(start: Int, end: Int, typeface: Typeface): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addTypefaceEffect(start, end, typeface) ?: return null + requestLayout() + return remover + } + + fun addTextSizeEffect(start: Int, end: Int, textSize: Float): TypeModel.EffectRemover? { + val typeModel = lineLayout.typeModel ?: return null + val remover = typeModel.addTextSizeEffect(start, end, textSize) ?: return null + requestLayout() + return remover + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + lineLayout.draw(canvas, environment) + } +} \ No newline at end of file diff --git a/type/src/main/java/com/qmuiteam/qmui/type/view/MarqueeTypeView.kt b/type/src/main/java/com/qmuiteam/qmui/type/view/MarqueeTypeView.kt new file mode 100644 index 00000000..7fb40b36 --- /dev/null +++ b/type/src/main/java/com/qmuiteam/qmui/type/view/MarqueeTypeView.kt @@ -0,0 +1,228 @@ +package com.qmuiteam.qmui.type.view + +import android.content.Context +import android.graphics.* +import android.os.Build +import android.os.SystemClock +import android.util.AttributeSet +import androidx.annotation.RequiresApi +import com.qmuiteam.qmui.type.TypeModel +import com.qmuiteam.qmui.type.parser.PlainTextParser +import com.qmuiteam.qmui.type.parser.TextParser + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +class MarqueeTypeView : BaseTypeView { + + constructor(context: Context): super(context) + constructor(context: Context, attrs: AttributeSet?): super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) + + var typeModel: TypeModel? = null + var textParser: TextParser = PlainTextParser.instance + set(value) { + if (field != value) { + field = value + typeModel = value.parse(text) + requestLayout() + if (isAttachedToWindow) { + start() + } + } + } + + var text: CharSequence? = null + set(value) { + if (field != value) { + field = value + reset() + typeModel = textParser.parse(value) + requestLayout() + if (isAttachedToWindow) { + start() + } + } + } + + var gap: Float = resources.displayMetrics.density * 50 + var moveSpeedPerMs: Float = resources.displayMetrics.density / 32 + var lastDrawTime = -2L + + private var elementMaxHeight: Float = 0f + private var contentWidth: Float = 0f + private var fadeHelper: FadeHelper? = null + private var startX: Float = 0f + + + var fadeWidth: Float + get() = fadeHelper?.fadeWidth ?: 0f + set(value) { + (fadeHelper ?: FadeHelper().also { fadeHelper = it }).fadeWidth = value + invalidate() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + environment.setMeasureLimit(widthSize, heightSize) + measureAndLayoutModel() + val usedWidth = if (widthMode == MeasureSpec.AT_MOST) { + contentWidth.toInt() + } else widthSize + val usedHeight = if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + elementMaxHeight.toInt() + } else heightSize + + var current = typeModel?.firstElement() + while (current != null) { + current.y = (usedHeight - current.measureHeight) / 2 + current = current.next + } + setMeasuredDimension(usedWidth, usedHeight) + } + + + private fun measureAndLayoutModel() { + elementMaxHeight = 0f + var current = typeModel?.firstElement() + var x = 0f + while (current != null) { + current.measure(environment) + current.x = x + elementMaxHeight = elementMaxHeight.coerceAtLeast(current.measureHeight) + x += current.measureWidth + current = current.next + } + contentWidth = x + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (!text.isNullOrBlank()) { + start() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stop() + } + + fun start() { + lastDrawTime = -1 + } + + fun stop() { + lastDrawTime = -2 + } + + fun reset() { + startX = 0f + } + + override fun onDraw(canvas: Canvas) { + if(fadeHelper == null){ + drawContent(canvas) + }else{ + fadeHelper!!.drawFade(canvas){ + drawContent(canvas) + } + } + } + + + private fun drawContent(canvas: Canvas){ + if (contentWidth <= width) { + lastDrawTime = -2 + startX = 0f + } + canvas.save() + canvas.translate(startX, 0f) + var current = typeModel?.firstElement() + while (current != null) { + if (current.x + startX > width) { + break + } else if (current.x + startX + current.measureWidth > 0) { + current.draw(environment, canvas) + } + current = current.next + } + canvas.restore() + val gapRight = startX + contentWidth + gap + if (startX < 0 && gapRight < width) { + canvas.save() + canvas.translate(gapRight, 0f) + current = typeModel?.firstElement() + while (current != null) { + if (current.x + gapRight > width) { + break + } else { + current.draw(environment, canvas) + } + current = current.next + } + canvas.restore() + } + if (lastDrawTime == -2L) { + return + } + if (lastDrawTime == -1L) { + lastDrawTime = SystemClock.elapsedRealtime() + } else { + val newTime = SystemClock.elapsedRealtime() + startX -= moveSpeedPerMs * (newTime - lastDrawTime) + lastDrawTime = newTime + if (startX + contentWidth < 0) { + startX += contentWidth + gap + } + // if drop many many frames. this condition can be matched, so recover to normal state. + if (startX + contentWidth < 0) { + startX = 0f + } + } + postInvalidateOnAnimation() + } + + private inner class FadeHelper{ + + var fadeWidth = 0f + + private val paint = Paint().apply { + style = Paint.Style.FILL + xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) + } + + private val leftFadeShader by lazy(LazyThreadSafetyMode.NONE) { + LinearGradient( + 0f, 0f, fadeWidth, 0f, + intArrayOf(Color.TRANSPARENT, Color.BLACK), null, Shader.TileMode.CLAMP + ) + } + + private val rightFadeShader by lazy(LazyThreadSafetyMode.NONE) { + LinearGradient( + 0f, 0f, fadeWidth, 0f, + intArrayOf(Color.BLACK, Color.TRANSPARENT), null, Shader.TileMode.CLAMP + ) + } + + inline fun drawFade(canvas: Canvas, action: (Canvas) -> Unit) = canvas.apply { + if(fadeWidth <= 0 || contentWidth < width){ + action(this) + }else{ + val layerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null) + action(this) + if(startX < 0){ + paint.shader = leftFadeShader + drawRect(0f, 0f, fadeWidth, height.toFloat(), paint) + } + + translate((width - fadeWidth).coerceAtLeast(0f), 0f) + paint.shader = rightFadeShader + drawRect(0f, 0f, fadeWidth, height.toFloat(), paint) + restoreToCount(layerId) + } + } + } +} \ No newline at end of file