diff --git a/app/src/main/java/com/gh/common/util/Extensions.kt b/app/src/main/java/com/gh/common/util/Extensions.kt index 2b1afa872c..c1c3e39ecd 100644 --- a/app/src/main/java/com/gh/common/util/Extensions.kt +++ b/app/src/main/java/com/gh/common/util/Extensions.kt @@ -27,6 +27,7 @@ import com.gh.common.constant.Config import com.gh.common.constant.Constants import com.gh.common.view.CenterImageSpan import com.gh.common.view.CustomLinkMovementMethod +import com.gh.common.view.ExpandTextView import com.gh.gamecenter.BuildConfig import com.gh.gamecenter.R import com.gh.gamecenter.WebActivity @@ -199,6 +200,18 @@ fun String.insert(index: Int, string: String): String { return this.substring(0, index) + string + this.substring(index, this.length) } +/** + * TextView 内部处理 ul li ol 得跟 Android 版本走,这里换成专属的标签手动处理 + */ +fun String.replaceUnsupportedHtmlTag(): String { + return this.replace("", "") + .replace("", "") + .replace("", "") +} + /** * 在限定 interval 里只触发一次 action */ @@ -459,21 +472,33 @@ fun TextView.setTextChangedListener(action: (s: CharSequence, start: Int, before /** * 拦截 TextView 中的 Url Span,用应用内页面的形式打开链接 - * @param text 文字 - * @param handleDefaultHighlighter 是否转换使用 ### ### 格式的包裹高亮文字 - * @param copyHighlighterOnClick 点击时是否复制使用 ### ### 格式的包裹高亮文字 - * + * @param shrankText 未展开时的文字 + * @param expandedText 展开后的文字 */ -fun TextView.setTextWithInterceptingInternalUrl(text: CharSequence, - handleDefaultHighlighter: Boolean = false, - copyHighlighterOnClick: Boolean = false) { - var ssb = SpannableStringBuilder.valueOf(text).apply { +fun ExpandTextView.setTextWithInterceptingInternalUrl(shrankText: CharSequence, expandedText: CharSequence) { + var shrankSsb = shrankText.interceptUrlSpanAndRoundImageSpan() + var expandedSsb = expandedText.interceptUrlSpanAndRoundImageSpan() + + // 去掉多余的 p 换行 + if (expandedSsb.endsWith("\n", true)) { + expandedSsb = SpannableStringBuilder((expandedSsb.subSequence(0, expandedSsb.length - 2))) + } + + movementMethod = CustomLinkMovementMethod.getInstance() + + shrankSsb = TextHelper.updateSpannableStringWithHighlightedSpan(context, shrankSsb, Constants.DEFAULT_TEXT_WRAPPER, R.color.theme_font) + expandedSsb = TextHelper.updateSpannableStringWithHighlightedSpan(context, expandedSsb, Constants.DEFAULT_TEXT_WRAPPER, R.color.theme_font) + setShrankTextAndExpandedText(shrankSsb, expandedSsb) +} + +fun CharSequence.interceptUrlSpanAndRoundImageSpan(): SpannableStringBuilder { + return SpannableStringBuilder.valueOf(this).apply { getSpans(0, length, URLSpan::class.java).forEach { setSpan( object : ClickableSpan() { override fun updateDrawState(ds: TextPaint) { super.updateDrawState(ds) - ds.color = ContextCompat.getColor(context, R.color.theme_font) + ds.color = ContextCompat.getColor(HaloApp.getInstance().application, R.color.theme_font) ds.isUnderlineText = false } @@ -500,25 +525,6 @@ fun TextView.setTextWithInterceptingInternalUrl(text: CharSequence, removeSpan(it) } } - - movementMethod = CustomLinkMovementMethod.getInstance() - if (handleDefaultHighlighter) { - ssb = TextHelper.updateSpannableStringWithHighlightedSpan( - context, - ssb, - Constants.DEFAULT_TEXT_WRAPPER, - R.color.theme_font, - object : SimpleCallback { - override fun onCallback(arg: String) { - if (copyHighlighterOnClick) { - arg.copyTextAndToast("已复制:$arg") - } - } - }) - setText(ssb) - } else { - setText(ssb) - } } fun Int.toColor(): Int { diff --git a/app/src/main/java/com/gh/common/util/ExtraTagHandler.kt b/app/src/main/java/com/gh/common/util/ExtraTagHandler.kt new file mode 100644 index 0000000000..9221da1856 --- /dev/null +++ b/app/src/main/java/com/gh/common/util/ExtraTagHandler.kt @@ -0,0 +1,183 @@ +package com.gh.common.util + +import android.text.Editable +import android.text.Html.TagHandler +import android.text.Spanned +import android.text.style.BulletSpan +import android.text.style.LeadingMarginSpan +import android.util.Log +import org.xml.sax.XMLReader +import java.util.* + +/** + * Implements support for ordered (`
    `) and unordered (`
      `) lists in to Android TextView. + * + * + * This can be used as follows:

      + * `textView.setText(Html.fromHtml("
      • item 1
      • item 2
      ", null, new HtmlListTagHandler()));` + * + * + * Implementation based on code by Juha Kuitunen (https://bitbucket.org/Kuitsi/android-textview-html-list), + * released under Apache License v2.0. Refactored & improved by Matthias Stevens (InThePocket.mobi). + * + * + * **Known issues:** + * * The indentation on nested `
        `s isn't quite right (TODO fix this) + * * the `start` attribute of `
          ` is not supported. Doing so is tricky because + * [Html.TagHandler.handleTag] does not expose tag attributes. + * The only way to do it would be to use reflection to access the attribute information kept by the XMLReader + * (see: http://stackoverflow.com/a/24534689/1084488). + * + * https://bitbucket.org/Kuitsi/android-textview-html-list/src/master/app/src/main/java/fi/iki/kuitsi/listtest/MyTagHandler.java + * + */ +class ExtraTagHandler : TagHandler { + /** + * Keeps track of lists (ol, ul). On bottom of Stack is the outermost list + * and on top of Stack is the most nested list + */ + private val lists = Stack() + + /** + * @see android.text.Html.TagHandler.handleTag + */ + override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) { + if (UL_TAG.equals(tag, ignoreCase = true)) { + if (opening) { // handle
            + lists.push(Ul()) + } else { // handle
          + lists.pop() + } + } else if (OL_TAG.equals(tag, ignoreCase = true)) { + if (opening) { // handle
            + lists.push(Ol()) // use default start index of 1 + } else { // handle
          + lists.pop() + } + } else if (LI_TAG.equals(tag, ignoreCase = true)) { + if (opening) { // handle
        1. + lists.peek().openItem(output) + } else { // handle
        2. + lists.peek().closeItem(output, lists.size) + } + } else { + Log.d("TagHandler", "Found an unsupported tag $tag") + } + } + + /** + * Abstract super class for [Ul] and [Ol]. + */ + private abstract class ListTag { + /** + * Opens a new list item. + * + * @param text + */ + open fun openItem(text: Editable) { + if (text.length > 0 && text[text.length - 1] != '\n') { + text.append("\n") + } + val len = text.length + text.setSpan(this, len, len, Spanned.SPAN_MARK_MARK) + } + + /** + * Closes a list item. + * + * @param text + * @param indentation + */ + fun closeItem(text: Editable, indentation: Int) { + if (text.length > 0 && text[text.length - 1] != '\n') { + text.append("\n") + } + val replaces = getReplaces(text, indentation) + val len = text.length + val listTag = getLast(text) + val where = text.getSpanStart(listTag) + text.removeSpan(listTag) + if (where != len) { + for (replace in replaces) { + text.setSpan(replace, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + } + + protected abstract fun getReplaces(text: Editable?, indentation: Int): Array + + /** + * Note: This knows that the last returned object from getSpans() will be the most recently added. + * + * @see Html + */ + private fun getLast(text: Spanned): ListTag? { + val listTags = text.getSpans(0, text.length, ListTag::class.java) + return if (listTags.size == 0) { + null + } else listTags[listTags.size - 1] + } + } + + /** + * Class representing the unordered list (`
            `) HTML tag. + */ + private class Ul : ListTag() { + override fun getReplaces(text: Editable?, indentation: Int): Array { + // Nested BulletSpans increases distance between BULLET_SPAN and text, so we must prevent it. + var bulletMargin = INDENT_PX + if (indentation > 1) { + bulletMargin = INDENT_PX - BULLET_SPAN.getLeadingMargin(true) + if (indentation > 2) { + // This get's more complicated when we add a LeadingMarginSpan into the same line: + // we have also counter it's effect to BulletSpan + bulletMargin -= (indentation - 2) * LIST_ITEM_INDENT_PX + } + } + return arrayOf( + LeadingMarginSpan.Standard(LIST_ITEM_INDENT_PX * (indentation - 1)), + BulletSpan(bulletMargin) + ) + } + } + + /** + * Class representing the ordered list (`
              `) HTML tag. + */ + private class Ol + /** + * Creates a new `
                ` with start index of 1. + */ @JvmOverloads constructor(private var nextIdx: Int = 1) : ListTag() { + override fun openItem(text: Editable) { + super.openItem(text) + text.append(Integer.toString(nextIdx++)).append(". ") + } + + override fun getReplaces(text: Editable?, indentation: Int): Array { + var numberMargin = LIST_ITEM_INDENT_PX * (indentation - 1) + if (indentation > 2) { + // Same as in ordered lists: counter the effect of nested Spans + numberMargin -= (indentation - 2) * LIST_ITEM_INDENT_PX + } + return arrayOf(LeadingMarginSpan.Standard(numberMargin)) + } + /** + * Creates a new `
                  ` with given start index. + * + * @param nextIdx + */ + } + + companion object { + private const val OL_TAG = "hol" + private const val UL_TAG = "hul" + private const val LI_TAG = "hli" + + /** + * List indentation in pixels. Nested lists use multiple of this. + */ + private const val INDENT_PX = 10 + private const val LIST_ITEM_INDENT_PX = INDENT_PX * 2 + private val BULLET_SPAN = BulletSpan(INDENT_PX) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/common/view/ExpandTextView.java b/app/src/main/java/com/gh/common/view/ExpandTextView.java index 791ad05143..ed5fc678e5 100644 --- a/app/src/main/java/com/gh/common/view/ExpandTextView.java +++ b/app/src/main/java/com/gh/common/view/ExpandTextView.java @@ -8,6 +8,7 @@ import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; +import android.text.TextUtils; import android.text.style.ClickableSpan; import android.util.AttributeSet; import android.view.View; @@ -23,9 +24,11 @@ public class ExpandTextView extends AppCompatTextView { private CharSequence mSnapshotText; private String mEndText = "..."; + private CharSequence mShrankText = ""; private String mExpandText = mEndText + "全文"; + private CharSequence mExpandedText = ""; private boolean mUseGradientAlphaEndText = false; - private boolean mShrinkOnAnchor = false; // 以锚点所在的位置进行收起 (即 maxLines = 锚点所在的行) + private boolean mShowExpandTextRegardlessOfMaxLines = false; private int mMaxLines = 3; // 由于sdk版本限制(getMaxLines) 这里设置默认值 @@ -39,7 +42,6 @@ public class ExpandTextView extends AppCompatTextView { private Rect mLastActualLineRect; private static int DEFAULT_ADDITIONAL_END_TEXT_COUNT = 2; - public static final String SHRINK_ANCHOR = "☼"; public ExpandTextView(Context context) { super(context); @@ -55,45 +57,33 @@ public class ExpandTextView extends AppCompatTextView { mLastActualLineRect = new Rect(); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandTextView); - mShrinkOnAnchor = ta.getBoolean(R.styleable.ExpandTextView_shrinkOnAnchor, false); mUseGradientAlphaEndText = ta.getBoolean(R.styleable.ExpandTextView_useGradientAlphaEndText, false); mEndText = ta.getString(R.styleable.ExpandTextView_endText) == null ? mEndText : ta.getString(R.styleable.ExpandTextView_endText); - mExpandText = ta.getString(R.styleable.ExpandTextView_expandText) == null ? mExpandText : ta - .getString(R.styleable.ExpandTextView_expandText); + mExpandText = ta.getString(R.styleable.ExpandTextView_expandText) == null ? mExpandText : ta.getString(R.styleable.ExpandTextView_expandText); ta.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mShrinkOnAnchor && getText().toString().contains(SHRINK_ANCHOR)) { - updateMaxLinesByShrinkAnchor(); + // TODO 复用有问题 + if (mShowExpandTextRegardlessOfMaxLines && !mIsExpanded) { + updateMaxLines(); } setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight() - getExtraBottomPadding()); } - private void updateMaxLinesByShrinkAnchor() { - int lineCount = getLineCount(); - for (int line = 0; line < lineCount; line++) { - CharSequence substring = getText().subSequence(getLayout().getLineStart(line), getLayout().getLineEnd(line)); - if (substring.toString().contains(SHRINK_ANCHOR)) { - if (mMaxLines != line + 1) { - if (!mIsExpanded) { - setExpandMaxLines(line + 1); - mMaxLinesCalculatedCallback.onMaxLinesCalculated(line + 1); - } - setText(findAndDeleteAnchor(getText())); - break; - } - } - } + private void updateMaxLines() { + mMaxLines = getLineCount() - 1; + mMaxLinesCalculatedCallback.onMaxLinesCalculated(getLineCount() - 1); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (mInitLayout && !mIsExpanded && getLineCount() > mMaxLines && mMaxLines > 0) { - mSnapshotText = findAndDeleteAnchor(getText()); + if ((mShowExpandTextRegardlessOfMaxLines && !mIsExpanded) + || (mInitLayout && !mIsExpanded && getLineCount() > mMaxLines && mMaxLines > 0)) { + mSnapshotText = getText(); mInitLayout = false; showExpandButton(); } @@ -107,6 +97,18 @@ public class ExpandTextView extends AppCompatTextView { this.mExpandCallback = callback; } + public void setShrankTextAndExpandedText(CharSequence shrankText, CharSequence expandedText) { + mShrankText = shrankText; + mExpandedText = expandedText; + mShowExpandTextRegardlessOfMaxLines = !TextUtils.isEmpty(shrankText); + + if (!mIsExpanded && mShowExpandTextRegardlessOfMaxLines) { + setText(mShrankText); + } else { + setText(mExpandedText); + } + } + @Override public void setText(CharSequence text, BufferType type) { mInitLayout = true; @@ -139,7 +141,7 @@ public class ExpandTextView extends AppCompatTextView { if (viewWidth - lastLineRight > expandTextWidth) { if (mUseGradientAlphaEndText) { finalEndText = content.toString() - .substring(content.length() - additionalEndTextCount, content.length()) + mEndText; + .substring(content.length() - additionalEndTextCount, content.length()) + mEndText; finalEndText = finalEndText.replace("\n", ""); content = content.subSequence(0, content.length() - additionalEndTextCount) + finalEndText + mExpandText; @@ -198,7 +200,11 @@ public class ExpandTextView extends AppCompatTextView { public void onClick(@NonNull View widget) { mIsExpanded = true; setMaxLines(Integer.MAX_VALUE); - setText(mSnapshotText); + if (mShowExpandTextRegardlessOfMaxLines) { + setText(mExpandedText); + } else { + setText(mSnapshotText); + } if (mExpandCallback != null) { mExpandCallback.onExpand(); @@ -229,7 +235,7 @@ public class ExpandTextView extends AppCompatTextView { if (getMeasuredHeight() == getLayout().getHeight() - heightBetweenLastVisibleLineRectAndLastActualLineRect) { result = mLastVisibleLineRect.bottom - (lastVisibleLineBaseline + layout.getPaint() - .getFontMetricsInt().descent + getPaddingBottom()); + .getFontMetricsInt().descent + getPaddingBottom()); if (getLineSpacingExtra() > result) { result = 0; } else { @@ -240,18 +246,6 @@ public class ExpandTextView extends AppCompatTextView { return result; } - // 去掉锚点 - private CharSequence findAndDeleteAnchor(CharSequence charSequence) { - int anchorIndex = charSequence.toString().indexOf(SHRINK_ANCHOR); - if (anchorIndex >= 0) { - SpannableStringBuilder sb = new SpannableStringBuilder(getText()); - sb.delete(anchorIndex, anchorIndex + 1); - return sb; - } else { - return charSequence; - } - } - /** * 此方法仅更改标记,不做实际展开的操作 */ diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescAdapter.kt index c152fce060..7bfd33fc81 100644 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescAdapter.kt @@ -636,8 +636,9 @@ class DescAdapter(context: Context, titleHintTv.paint?.isUnderlineText = true contentTv.setExpandMaxLines(maxDesLines) contentTv.setIsExpanded(Int.MAX_VALUE == maxDesLines) - val spanned = HtmlCompat.fromHtml(customColumn.des ?: "", HtmlCompat.FROM_HTML_MODE_LEGACY, PicassoImageGetter(contentTv), null) - contentTv.setTextWithInterceptingInternalUrl(text = spanned, handleDefaultHighlighter = true, copyHighlighterOnClick = false) + val shrankSpanned = HtmlCompat.fromHtml(customColumn.desBrief ?: "", HtmlCompat.FROM_HTML_MODE_COMPACT, PicassoImageGetter(contentTv), ExtraTagHandler()) + val expandedSpanned = HtmlCompat.fromHtml(customColumn.des ?: "", HtmlCompat.FROM_HTML_MODE_COMPACT, PicassoImageGetter(contentTv), ExtraTagHandler()) + contentTv.setTextWithInterceptingInternalUrl(shrankText= shrankSpanned, expandedText = expandedSpanned) contentTv.setSelfCalculateMaxLinesCallback { mExpandableTextViewMaxLinesSparseIntArray.put(position, it) } recyclerView.isNestedScrollingEnabled = false recyclerView.layoutManager = layoutManager diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescViewModel.kt index 8cb10ea82c..b3a32e0dc9 100644 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/desc/DescViewModel.kt @@ -12,7 +12,6 @@ import com.facebook.common.util.UriUtil import com.gh.common.constant.Constants import com.gh.common.repository.RemenkapaiRepository import com.gh.common.util.* -import com.gh.common.view.ExpandTextView import com.gh.gamecenter.R import com.gh.gamecenter.entity.ErrorEntity import com.gh.gamecenter.entity.GameEntity @@ -270,14 +269,8 @@ class DescViewModel(application: Application, // 手动为自定义栏目的说明添加展开分隔符 if (!TextUtils.isEmpty(rawItem.customColumn?.desBrief)) { - val index = rawItem.customColumn?.desBrief?.length ?: 0 - if (index >= 0) { - rawItem.customColumn?.desFull = rawItem.customColumn?.desFull?.insert(index, ExpandTextView.SHRINK_ANCHOR) - } - } - - if (rawItem.customColumn?.name != "游戏简介") { - rawItem.customColumn?.showDesRowNum = Int.MAX_VALUE - 1 + rawItem.customColumn?.desFull = rawItem.customColumn?.desFull?.replaceUnsupportedHtmlTag() + rawItem.customColumn?.desBrief = rawItem.customColumn?.desBrief?.replaceUnsupportedHtmlTag() } rawItem.customColumn?.des = rawItem.customColumn?.desFull ?: rawItem.customColumn?.des diff --git a/app/src/main/res/layout/gamedetail_item_custom_column.xml b/app/src/main/res/layout/gamedetail_item_custom_column.xml index 885f5a5c16..6677d79d3f 100644 --- a/app/src/main/res/layout/gamedetail_item_custom_column.xml +++ b/app/src/main/res/layout/gamedetail_item_custom_column.xml @@ -232,7 +232,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/contentHintContainer" app:layout_goneMarginTop="10dp" - app:shrinkOnAnchor="true" app:useGradientAlphaEndText="true" tools:text="公告文章、权重大于0的自定义栏目和介绍文案,这三类版块内容可组合拼接为一个整体部分,即拼接内容可为其中两种(如公告文章+介绍文案),也可为全部三种(包括多个自定义栏目内容),其中公告文章和自定义栏目之间、公告文章和介绍文案之间、自定义栏目和介绍文案之间加上分割线隔开" tools:visibility="visible" /> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 58ffb40ae7..5f9f29b281 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -106,7 +106,6 @@ -