feature: MarqueeTypeView

This commit is contained in:
cgspine
2021-01-21 14:02:23 +08:00
parent 1744053908
commit be066fcbc3
12 changed files with 635 additions and 283 deletions
@@ -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<QMUIQQFaceCompiler.Element> 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() {
@@ -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("这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" +
"这是一段很长很长[微笑][微笑][微笑][微笑]的文本,但是最多只能显示三行;" +
@@ -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<TypeModel.EffectRemover> 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);
}
}
@@ -46,14 +46,35 @@
android:orientation="vertical"
android:padding="25dp">
<com.qmuiteam.qmuidemo.fragment.components.qqface.QMUIQQFaceView2
android:id="@+id/test_view"
<TextView
style="@style/QDCommonTitle"
android:text="单行滚动"/>
<com.qmuiteam.qmui.type.view.MarqueeTypeView
android:id="@+id/marquee1"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.qmuiteam.qmui.type.view.MarqueeTypeView
android:id="@+id/marquee2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"/>
<TextView
style="@style/QDCommonTitle"
android:text="新排版, 两端对齐"
android:layout_marginTop="20dp"/>
<com.qmuiteam.qmui.type.view.LineTypeView
android:id="@+id/line_type_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
style="@style/QDCommonTitle"
android:text="单行表情,末尾省略"/>
android:text="单行表情,末尾省略"
android:layout_marginTop="20dp"/>
<com.qmuiteam.qmui.qqface.QMUIQQFaceView
android:id="@+id/qqface1"
android:layout_width="wrap_content"
@@ -22,102 +22,64 @@ import android.text.TextUtils.TruncateAt
import com.qmuiteam.qmui.type.element.*
import java.util.*
class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
private var mMaxLines = Int.MAX_VALUE
private var mEllipsize: TruncateAt? = null
private var mCalculateWholeLines = true
private val mLines: MutableList<Line> = 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<Line> = 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<Int> = 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<Element> = 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<Line> = 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() {
@@ -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
}
}
@@ -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<Int> = 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<Int> = 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<Int> = ArrayList()
types.add(TypeEnvironment.TYPE_BG_COLOR)
@@ -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) {
@@ -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
@@ -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.")
}
}
}
@@ -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)
}
}
@@ -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)
}
}
}
}