前段时间接到一个需求,必要完成以下结果。
大抵功能和小红书结果类似 可以 睁开 和收起 也可以@xxx 还能加#话题
- 1、内容超过指定行数必要折叠起来;
- 2、内容中含有@+“内容”,必要携带“内容”跳转指定页面。
- 3、有大概会在“睁开”大概“收回”前面附加表现其他内容
实现思绪:
可以自界说View继续TextView,在自界说View里面行止理全部的逻辑,如许方便后期维护扩展。
具体实现
在开始写代码之前,我们必要思量几个点
- 怎么包管“睁开”大概“收回”放在笔墨的末了面
- 如何辨认笔墨中的@用户和#话题
- 处理惩罚@用户,链接和“睁开”大概“收回”三者的高亮表现和点击事件
题目处理惩罚
一、怎么包管“睁开”大概“收回”放在笔墨的末了面
这个确实挺难处理惩罚的!在此之前也是让我头疼的一个题目,不外厥后我遇到了DynamicLayout,使用它我们可以获取行的末了位置,行的开始位置,行的行宽以及指定内容的所占的行数。
//用来盘算内容的巨细 DynamicLayout mDynamicLayout = new DynamicLayout(mFormatData.formatedContent, mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, 1.2f, 0.0f, true); //获取行数 int mLineCount = mDynamicLayout.getLineCount(); int index = currentLines - 1; //获取指定行的末了位置 int endPosition = mDynamicLayout.getLineEnd(index); //获取指定行的开始位置 int startPosition = mDynamicLayout.getLineStart(index); //获取指定行的行宽 float lineWidth = mDynamicLayout.getLineWidth(index);有了这些东西颠末简单的盘算我们就可以获取到我们必要截取的内容长度。对原内容举行截取再拼接上“睁开”或“收回”即可!
/** * 盘算原内容被裁剪的长度 * * @param endPosition * @param startPosition * @param lineWidth * @param endStringWith * @param offset * @return */ private int getFitPosition(int endPosition, int startPosition, float lineWidth, float endStringWith, float offset, String aimContent) { //末了一行必要添加的笔墨的字数 int position = (int) ((lineWidth - (endStringWith + offset)) * (endPosition - startPosition)/ lineWidth); if (position < 0) return endPosition; //盘算末了一行必要表现的正文的长度 float measureText = mPaint.measureText( (aimContent.substring(startPosition, startPosition + position))); //如果末了一行必要表现的正文的长度比末了一行的长减去“睁开”笔墨的长度要短就可以了 否则加个空格继续算 if (measureText <= lineWidth - endStringWith) { return startPosition + position; } else { return getFitPosition(endPosition, startPosition, lineWidth, endStringWith, offset + mPaint.measureText(" ")); } }二、如何辨认笔墨中的@用户
//对@用户 举行正则匹配 Pattern pattern = Pattern.compile(regexp_mention, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(newResult.toString()); List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { //将匹配到的内容举行统计处理惩罚 datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), LinkType.MENTION_TYPE)); }三、处理惩罚@用户,链接和“睁开”大概“收回”三者的高亮表现和点击事件
对于@用户,链接和“睁开”大概“收回”三者的实现,终极都是使用SpannableStringBuilder来处理惩罚。之前我们在对原内容举行解析的时间,将匹配到的链接大概@用户举行了存储,而且存储了他们地点的位置(start,end)以及范例。
//界说范例的摆列范例 public enum LinkType { //寻常链接 LINK_TYPE, //@用户 MENTION_TYPE }复制代码有了这些数据的聚集,我们只必要遍历这些数据,并分别对这些数据举行setSpan处理惩罚,而且在setSpan的过程中设置字体颜色,以及点击事件的回调即可。
//处理惩罚链接大概@用户 private void dealLinksOrMention(FormatData formatData,SpannableStringBuilder ssb) { List<FormatData.PositionData> positionDatas = formatData.getPositionDatas(); HH: for (FormatData.PositionData data : positionDatas) { if (data.getType().equals(LinkType.LINK_TYPE)) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { SelfImageSpan imageSpan = new SelfImageSpan(mLinkDrawable, ImageSpan.ALIGN_BASELINE); //设置链接图标 ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE); //设置链接笔墨样式 int endPosition = data.getEnd(); if (fitPosition > data.getStart() + 1 && fitPosition < data.getEnd()) { endPosition = fitPosition; } if (data.getStart() + 1 < fitPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onLinkClickListener(LinkType.LINK_TYPE, data.getUrl()); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mLinkTextColor); ds.setUnderlineText(false); } }, data.getStart() + 1, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } } else { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (fitPosition < data.getEnd()) { endPosition = fitPosition; } ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onLinkClickListener(LinkType.MENTION_TYPE, data.getUrl()); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mLinkTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } } } /** * 设置 "睁开" * @param ssb * @param formatData */ private void setExpandSpan(SpannableStringBuilder ssb,FormatData formatData){ int index = currentLines - 1; int endPosition = mDynamicLayout.getLineEnd(index); int startPosition = mDynamicLayout.getLineStart(index); float lineWidth = mDynamicLayout.getLineWidth(index); String endString = getHideEndContent(); //盘算原内容被截取的位置下标 int fitPosition = getFitPosition(endPosition, startPosition, lineWidth, mPaint.measureText(endString), 0); ssb.append(formatData.formatedContent.substring(0, fitPosition)); //在被截断的笔墨背面添加 睁开 笔墨 ssb.append(endString); int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length(); ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { action(); } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(mExpandTextColor); ds.setUnderlineText(false); } }, ssb.length() - TEXT_EXPEND.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); }复制代码在处理惩罚这一块的时间有个细节必要注意,那就是如果在笔墨切割后的末端恰恰有个一个链接,而这个地方又要表现“睁开”大概“收回”,这个地方要特别注意链接setSpan的范围,一不注意就大概连同把背面的“睁开”大概“收回”也一起设置了,导致事件不对。处理惩罚“收回”是差不多的,就不贴代码了。末了另有一个附加功能就是在末了添加时间串的功能,其实也就是在“睁开”和“收回”前面加一个串,做好这方面的判断就好了,代码里面已经做了处理惩罚。
下面是全部源码实现:
public class CustomExpandableTextView extends AppCompatTextView { private static final int DEF_MAX_LINE = 4; public static String TEXT_CONTRACT = "收起"; public static String TEXT_EXPEND = "睁开"; public static final String Space = " "; public static String TEXT_TARGET = "网页链接"; public static final String IMAGE_TARGET = "图"; public static final String TARGET = IMAGE_TARGET + TEXT_TARGET; public static final String DEFAULT_CONTENT = " "; private static int retryTime = 0; public static final String regexp_mention = "@[^\\n\\s]{1,80}\\s{1}"; public static final String regexp_topic = "#[^\\n\\s]{1,80}\\s{1}"; //匹配自界说链接的正则表达式 public static final String self_regex = "\\[([^\\[]*)\\]\\(([^\\(]*)\\)"; private TextPaint mPaint; boolean linkHit; private Context mContext; /** * 纪录当前的model */ private ExpandableStatusFix mModel; /** * 盘算的layout */ private DynamicLayout mDynamicLayout; //hide状态下,展示多少行开始省略 private int mLimitLines; private int currentLines; private int mWidth; private Drawable mLinkDrawable = null; /** * 链接和@用户的事件点击 */ private OnLinkClickListener linkClickListener; /** * 点击睁开大概收回按钮的时间 是否真的实行操纵 */ private boolean needRealExpandOrContract = true; /** * 睁开大概收回事件监听 */ private OnExpandOrContractClickListener expandOrContractClickListener; /** * 是否必要收起 */ private boolean mNeedContract = true; private FormatData mFormatData; /** * 是否必要睁开功能 */ private boolean mNeedExpend = true; /** * 是否必要转换url成网页链接四个字 */ private boolean mNeedConvertUrl = true; /** * 是否必要@用户的功能 */ private boolean mNeedMention = true; /** * 是否必要#用户的功能 */ private boolean mNeedTopic = true; /** * 是否必要对链接举行处理惩罚 */ private boolean mNeedLink = true; /** * 是否必要对自界说情况举行处理惩罚 */ private boolean mNeedSelf = false; /** * 是否必要永久将睁开或收回表现在最右边 */ private boolean mNeedAlwaysShowRight = false; /** * 是否必要动画 默认开启动画 */ private boolean mNeedAnimation = true; private int mLineCount; private CharSequence mContent; /** * 睁开笔墨的颜色 */ private int mExpandTextColor; /** * 睁开笔墨的颜色 */ private int mMentionTextColor; private int mTopicTextColor; /** * 链接的字体颜色 */ private int mLinkTextColor; /** * 自界说规则的字体颜色 */ private int mSelfTextColor; /** * 收起的笔墨的颜色 */ private int mContractTextColor; /** * 睁开的文案 */ private String mExpandString; /** * 收起的文案 */ private String mContractString; /** * 在收回和睁开前面添加的内容 */ private String mEndExpandContent; /** * 在收回和睁开前面添加的内容的字体颜色 */ private int mEndExpandTextColor; //是否AttachedToWindow private boolean isAttached; public ExpandableTextView(Context context) { this(context, null); } public ExpandableTextView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, -1); } public ExpandableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); setMovementMethod(LocalLinkMovementMethod.getInstance()); addOnAttachStateChangeListener(new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { if (isAttached == false) doSetContent(); isAttached = true; } @Override public void onViewDetachedFromWindow(View v) { } }); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { //适配英文版 TEXT_CONTRACT = context.getString(R.string.social_contract); TEXT_EXPEND = context.getString(R.string.social_expend); TEXT_TARGET = context.getString(R.string.social_text_target); if (attrs != null) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandableTextView, defStyleAttr, 0); mLimitLines = a.getInt(R.styleable.ExpandableTextView_ep_max_line, DEF_MAX_LINE); mNeedExpend = a.getBoolean(R.styleable.ExpandableTextView_ep_need_expand, true); mNeedContract = a.getBoolean(R.styleable.ExpandableTextView_ep_need_contract, false); mNeedAnimation = a.getBoolean(R.styleable.ExpandableTextView_ep_need_animation, true); mNeedSelf = a.getBoolean(R.styleable.ExpandableTextView_ep_need_self, false); mNeedMention = a.getBoolean(R.styleable.ExpandableTextView_ep_need_mention, true); mNeedTopic = a.getBoolean(R.styleable.ExpandableTextView_ep_need_mention, true); mNeedLink = a.getBoolean(R.styleable.ExpandableTextView_ep_need_link, true); mNeedAlwaysShowRight = a.getBoolean(R.styleable.ExpandableTextView_ep_need_always_showright, false); mNeedConvertUrl = a.getBoolean(R.styleable.ExpandableTextView_ep_need_convert_url, true); mContractString = a.getString(R.styleable.ExpandableTextView_ep_contract_text); mExpandString = a.getString(R.styleable.ExpandableTextView_ep_expand_text); if (TextUtils.isEmpty(mExpandString)) { mExpandString = TEXT_EXPEND; } if (TextUtils.isEmpty(mContractString)) { mContractString = TEXT_CONTRACT; } mExpandTextColor = a.getColor(R.styleable.ExpandableTextView_ep_expand_color, Color.parseColor("#999999")); mEndExpandTextColor = a.getColor(R.styleable.ExpandableTextView_ep_expand_color, Color.parseColor("#999999")); mContractTextColor = a.getColor(R.styleable.ExpandableTextView_ep_contract_color, Color.parseColor("#999999")); mLinkTextColor = a.getColor(R.styleable.ExpandableTextView_ep_link_color, Color.parseColor("#FF6200")); mSelfTextColor = a.getColor(R.styleable.ExpandableTextView_ep_self_color, Color.parseColor("#FF6200")); mMentionTextColor = a.getColor(R.styleable.ExpandableTextView_ep_mention_color, Color.parseColor("#FF6200")); mTopicTextColor = a.getColor(R.styleable.ExpandableTextView_ep_topic_color, Color.parseColor("#FF6200")); int resId = a.getResourceId(R.styleable.ExpandableTextView_ep_link_res, R.mipmap.link); mLinkDrawable = getResources().getDrawable(resId); currentLines = mLimitLines; a.recycle(); } else { mLinkDrawable = context.getResources().getDrawable(R.mipmap.link); } mContext = context; mPaint = getPaint(); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); //初始化link的图片 mLinkDrawable.setBounds(0, 0, 30, 30); //必须设置图片巨细,否则不表现 } private SpannableStringBuilder setRealContent(CharSequence content,boolean isHide) { //处理惩罚给定的数据 mFormatData = formatData(content); //用来盘算内容的巨细 mDynamicLayout = new DynamicLayout(mFormatData.getFormatedContent(), mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, 1.2f, 0.0f, true); //获取行数 mLineCount = mDynamicLayout.getLineCount(); if (onGetLineCountListener != null) { onGetLineCountListener.onGetLineCount(mLineCount, mLineCount > mLimitLines); } if (!mNeedExpend || mLineCount <= mLimitLines) { //不必要睁开功能 直接处理惩罚链接模块 return dealLink(mFormatData, false,false); } else { return dealLink(mFormatData, true,isHide); } } /** * 设置追加的内容 * * @param endExpendContent */ public void setEndExpendContent(String endExpendContent) { this.mEndExpandContent = endExpendContent; } /** * 设置内容 * * @param content */ public void setContent(final String content) { mContent = content; if (isAttached) doSetContent(); } /** * 现实设置内容的 */ private void doSetContent() { if (mContent == null) { return; } currentLines = mLimitLines; if (mWidth <= 0) { if (getWidth() > 0) mWidth = getWidth() - getPaddingLeft() - getPaddingRight(); } if (mWidth <= 0) { if (retryTime > 10) { setText(DEFAULT_CONTENT); } this.post(new Runnable() { @Override public void run() { retryTime++; setContent(mContent.toString()); } }); } else { setRealContent(mContent.toString(),false); } } /** * 设置末了的收起文案 * * @return */ private String getExpandEndContent() { if (TextUtils.isEmpty(mEndExpandContent)) { return String.format(Locale.getDefault(), " %s", mContractString); } else { return String.format(Locale.getDefault(), " %s %s", mEndExpandContent, mContractString); } } /** * 设置睁开的文案 * * @return */ private String getHideEndContent() { if (TextUtils.isEmpty(mEndExpandContent)) { return String.format(Locale.getDefault(), mNeedAlwaysShowRight ? " %s" : "... %s", mExpandString); } else { return String.format(Locale.getDefault(), mNeedAlwaysShowRight ? " %s %s" : "... %s %s", mEndExpandContent, mExpandString); } } /** * 处理惩罚笔墨中的链接题目 * * @param formatData * @param ignoreMore */ private SpannableStringBuilder dealLink(FormatData formatData, boolean ignoreMore,boolean mIsHide) { SpannableStringBuilder ssb = new SpannableStringBuilder(); //获取存储的状态 if (mModel != null && mModel.getStatus() != null) { boolean isHide = false; if (mModel.getStatus() != null) { if (mModel.getStatus().equals(StatusType.STATUS_CONTRACT)) { //收起 isHide = true; } else { //睁开 isHide = false; } } if (isHide) { currentLines = mLimitLines + ((mLineCount - mLimitLines)); } else { if (mNeedContract) currentLines = mLimitLines; } mIsHide = isHide; } //处理惩罚折叠操纵 if (ignoreMore) { if (currentLines < mLineCount) { int index = currentLines - 1; int endPosition = mDynamicLayout.getLineEnd(index); int startPosition = mDynamicLayout.getLineStart(index); float lineWidth = mDynamicLayout.getLineWidth(index); String endString = getHideEndContent(); //盘算原内容被截取的位置下标 int fitPosition = getFitPosition(endString, endPosition, startPosition, lineWidth, mPaint.measureText(endString), 0); String substring = formatData.getFormatedContent().substring(0, fitPosition); if (substring.endsWith("\n")) { substring = substring.substring(0, substring.length() - "\n".length()); } ssb.append(substring); if (mNeedAlwaysShowRight) { //盘算一下末了一行有没有布满 float lastLineWidth = 0; for (int i = 0; i < index; i++) { lastLineWidth += mDynamicLayout.getLineWidth(i); } lastLineWidth = lastLineWidth / (index); float emptyWidth = lastLineWidth - lineWidth - mPaint.measureText(endString); if (emptyWidth > 0) { float measureText = mPaint.measureText(Space); int count = 0; while (measureText * count < emptyWidth) { count++; } count = count - 1; for (int i = 0; i < count; i++) { ssb.append(Space); } } } //在被截断的笔墨背面添加 睁开 笔墨 ssb.append(endString); int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length(); ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (needRealExpandOrContract) { if (mModel != null) { mModel.setStatus(StatusType.STATUS_CONTRACT); action(mModel.getStatus()); } else { action(); } } if (expandOrContractClickListener != null) { expandOrContractClickListener.onClick(StatusType.STATUS_EXPAND); } } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(mExpandTextColor); ds.setUnderlineText(false); } }, ssb.length() - mExpandString.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } else { ssb.append(formatData.getFormatedContent()); if (mNeedContract) { String endString = getExpandEndContent(); if (mNeedAlwaysShowRight) { //盘算一下末了一行有没有布满 int index = mDynamicLayout.getLineCount() - 1; float lineWidth = mDynamicLayout.getLineWidth(index); float lastLineWidth = 0; for (int i = 0; i < index; i++) { lastLineWidth += mDynamicLayout.getLineWidth(i); } lastLineWidth = lastLineWidth / (index); float emptyWidth = lastLineWidth - lineWidth - mPaint.measureText(endString); if (emptyWidth > 0) { float measureText = mPaint.measureText(Space); int count = 0; while (measureText * count < emptyWidth) { count++; } count = count - 1; for (int i = 0; i < count; i++) { ssb.append(Space); } } } ssb.append(endString); int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length(); ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (mModel != null) { mModel.setStatus(StatusType.STATUS_EXPAND); action(mModel.getStatus()); } else { action(); } if (expandOrContractClickListener != null) { expandOrContractClickListener.onClick(StatusType.STATUS_CONTRACT); } } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(mContractTextColor); ds.setUnderlineText(false); } }, ssb.length() - mContractString.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } else { if (!TextUtils.isEmpty(mEndExpandContent)) { ssb.append(mEndExpandContent); ssb.setSpan(new ForegroundColorSpan(mEndExpandTextColor), ssb.length() - mEndExpandContent.length(), ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } } } else { ssb.append(formatData.getFormatedContent()); if (!TextUtils.isEmpty(mEndExpandContent)) { ssb.append(mEndExpandContent); ssb.setSpan(new ForegroundColorSpan(mEndExpandTextColor), ssb.length() - mEndExpandContent.length(), ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } //处理惩罚链接大概@用户 List<FormatData.PositionData> positionDatas = formatData.getPositionDatas(); HH: for (FormatData.PositionData data : positionDatas) { if (ssb.length() >= data.getEnd()) { if (data.getType().equals(LinkType.LINK_TYPE)) { if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { SelfImageSpan imageSpan = new SelfImageSpan(mLinkDrawable, ImageSpan.ALIGN_BASELINE); //设置链接图标 ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE); //设置链接笔墨样式 int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition > data.getStart() + 1 && fitPosition < data.getEnd()) { endPosition = fitPosition; } } if (data.getStart() + 1 < fitPosition) { addUrl(ssb, data, endPosition); } } } else { SelfImageSpan imageSpan = new SelfImageSpan(mLinkDrawable, ImageSpan.ALIGN_BASELINE); //设置链接图标 ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE); addUrl(ssb, data, data.getEnd()); } } else if (data.getType().equals(LinkType.MENTION_TYPE)) { //如果必要睁开 if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition < data.getEnd()) { endPosition = fitPosition; } } addMention(ssb, data, endPosition); } } else { addMention(ssb, data, data.getEnd()); } } else if (data.getType().equals(LinkType.TOPIC_TYPE)) { //如果必要睁开 if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() -(mIsHide?getExpandEndContent().length():getHideEndContent().length()) ; if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition < data.getEnd()) { endPosition = fitPosition; } } addTopic(ssb, data, endPosition); } } else { addTopic(ssb, data, data.getEnd()); } }else if (data.getType().equals(LinkType.SELF)) { //自界说 //如果必要睁开 if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition < data.getEnd()) { endPosition = fitPosition; } } addSelf(ssb, data, endPosition); } } else { addSelf(ssb, data, data.getEnd()); } } } } //扫除链接点击时配景结果 setHighlightColor(Color.TRANSPARENT); //将内容设置到控件中 setText(ssb); return ssb; } /** * 获取必要插入的空格 * * @param emptyWidth * @param endStringWidth * @return */ private int getFitSpaceCount(float emptyWidth, float endStringWidth) { float measureText = mPaint.measureText(Space); int count = 0; while (endStringWidth + measureText * count < emptyWidth) { count++; } return --count; } /** * 添加自界说规则 * * @param ssb * @param data * @param endPosition */ private void addSelf(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onLinkClickListener(LinkType.SELF, data.getSelfAim(), data.getSelfContent()); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mSelfTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } /** * 添加@用户的Span * * @param ssb * @param data * @param endPosition */ private void addMention(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onLinkClickListener(LinkType.MENTION_TYPE, data.getUrl(), null); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mMentionTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } /** * 添加@用户的Span * * @param ssb * @param data * @param endPosition */ private void addTopic(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onLinkClickListener(LinkType.TOPIC_TYPE, data.getUrl(), null); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mTopicTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } /** * 添加链接的span * * @param ssb * @param data * @param endPosition */ private void addUrl(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) { linkClickListener.onLinkClickListener(LinkType.LINK_TYPE, data.getUrl(), null); } else { //如果没有设置监听 则调用默认的打开欣赏器表现毗连 Intent intent = new Intent(); intent.setAction("android.intent.action.VIEW"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri url = Uri.parse(data.getUrl()); intent.setData(url); mContext.startActivity(intent); } } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mLinkTextColor); ds.setUnderlineText(false); } }, data.getStart() + 1, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } /** * 设置当前的状态 * * @param type */ public void setCurrStatus(StatusType type) { action(type); } private void action() { action(null); } /** * 实行睁开和收回的动作 */ private void action(StatusType type) { boolean isHide = currentLines < mLineCount; if (type != null) { mNeedAnimation = false; } if (mNeedAnimation) { ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); final boolean finalIsHide = isHide; valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Float value = (Float) animation.getAnimatedValue(); if (finalIsHide) { currentLines = mLimitLines + (int) ((mLineCount - mLimitLines) * value); } else { if (mNeedContract) currentLines = mLimitLines + (int) ((mLineCount - mLimitLines) * (1 - value)); } setText(setRealContent(mContent,finalIsHide)); } }); valueAnimator.setDuration(100); valueAnimator.start(); } else { if (isHide) { currentLines = mLimitLines + ((mLineCount - mLimitLines)); } else { if (mNeedContract) currentLines = mLimitLines; } setText(setRealContent(mContent,isHide)); } } /** * 盘算原内容被裁剪的长度 * * @param endString * @param endPosition 指定行末了笔墨的位置 * @param startPosition 指定行笔墨开始的位置 * @param lineWidth 指定行笔墨的宽度 * @param endStringWith 末了添加的笔墨的宽度 * @param offset 偏移量 * @return */ private int getFitPosition(String endString, int endPosition, int startPosition, float lineWidth, float endStringWith, float offset) { //末了一行必要添加的笔墨的字数 int position = (int) ((lineWidth - (endStringWith + offset)) * (endPosition - startPosition) / lineWidth); if (position <= endString.length()) return endPosition; //盘算末了一行必要表现的正文的长度 float measureText = mPaint.measureText( (mFormatData.getFormatedContent().substring(startPosition, startPosition + position))); //如果末了一行必要表现的正文的长度比末了一行的长减去“睁开”笔墨的长度要短就可以了 否则加个空格继续算 if (measureText <= lineWidth - endStringWith) { return startPosition + position; } else { return getFitPosition(endString, endPosition, startPosition, lineWidth, endStringWith, offset + mPaint.measureText(Space)); } } /** * 对传入的数据举行正则匹配并处理惩罚 * * @param content * @return */ private FormatData formatData(CharSequence content) { FormatData formatData = new FormatData(); List<FormatData.PositionData> datas = new ArrayList<>(); //对链接举行正则匹配// Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE); Pattern pattern = Pattern.compile(self_regex, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(content); StringBuffer newResult = new StringBuffer(); int start = 0; int end = 0; int temp = 0; Map<String, String> convert = new HashMap<>(); //对自界说的举行正则匹配 if (mNeedSelf) { List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { start = matcher.start(); end = matcher.end(); newResult.append(content.toString().substring(temp, start)); //将匹配到的内容举行统计处理惩罚 String result = matcher.group(); if (!TextUtils.isEmpty(result)) { //解析数据 String aimSrt = result.substring(result.indexOf("[") + 1, result.indexOf("]")); String contentSrt = result.substring(result.indexOf("(") + 1, result.indexOf(")")); String key = UUIDUtils.getUuid(aimSrt.length()); datasMention.add(new FormatData.PositionData(newResult.length() + 1, newResult.length() + 2 + aimSrt.length(), aimSrt, contentSrt, LinkType.SELF)); convert.put(key, aimSrt); newResult.append(" " + key + " "); temp = end; } } datas.addAll(datasMention); } //重置状态 newResult.append(content.toString().substring(end, content.toString().length())); content = newResult.toString(); newResult = new StringBuffer(); start = 0; end = 0; temp = 0; if (mNeedLink) { pattern = AUTOLINK_WEB_URL; matcher = pattern.matcher(content); while (matcher.find()) { start = matcher.start(); end = matcher.end(); newResult.append(content.toString().substring(temp, start)); if (mNeedConvertUrl) { //将匹配到的内容举行统计处理惩罚 datas.add(new FormatData.PositionData(newResult.length() + 1, newResult.length() + 2 + TARGET.length(), matcher.group(), LinkType.LINK_TYPE)); newResult.append(" " + TARGET + " "); } else { String result = matcher.group(); String key = UUIDUtils.getUuid(result.length()); datas.add(new FormatData.PositionData(newResult.length(), newResult.length() + 2 + key.length(), result, LinkType.LINK_TYPE)); convert.put(key, result); newResult.append(" " + key + " "); } temp = end; } } newResult.append(content.toString().substring(end, content.toString().length())); //对@用户 举行正则匹配 if (mNeedMention) { pattern = Pattern.compile(regexp_mention, Pattern.CASE_INSENSITIVE); matcher = pattern.matcher(newResult.toString()); List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { //将匹配到的内容举行统计处理惩罚 datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), LinkType.MENTION_TYPE)); } datas.addAll(0, datasMention); } if (mNeedTopic) { pattern = Pattern.compile(regexp_topic, Pattern.CASE_INSENSITIVE); matcher = pattern.matcher(newResult.toString()); List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { //将匹配到的内容举行统计处理惩罚 datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), LinkType.TOPIC_TYPE)); } datas.addAll(0, datasMention); } if (!convert.isEmpty()) { String resultData = newResult.toString(); for (Map.Entry<String, String> entry : convert.entrySet()) { resultData = resultData.replaceAll(entry.getKey(), entry.getValue()); } newResult = new StringBuffer(resultData); } formatData.setFormatedContent(newResult.toString()); formatData.setPositionDatas(datas); return formatData; } /** * 自界说ImageSpan 让Image 在行内居中表现 */ class SelfImageSpan extends ImageSpan { private Drawable drawable; public SelfImageSpan(Drawable d, int verticalAlignment) { super(d, verticalAlignment); this.drawable = d; } @Override public Drawable getDrawable() { return drawable; } @Override public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { // image to draw Drawable b = getDrawable(); // font metrics of text to be replaced Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int transY = (y + fm.descent + y + fm.ascent) / 2 - b.getBounds().bottom / 2; canvas.save(); canvas.translate(x, transY); b.draw(canvas); canvas.restore(); } } /** * 绑定状态 * * @param model */ public void bind(ExpandableStatusFix model) { mModel = model; } public static class LocalLinkMovementMethod extends LinkMovementMethod { static LocalLinkMovementMethod sInstance; public static LocalLinkMovementMethod getInstance() { if (sInstance == null) sInstance = new LocalLinkMovementMethod(); return sInstance; } @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { int x = (int) event.getX(); int y = (int) event.getY(); x -= widget.getTotalPaddingLeft(); y -= widget.getTotalPaddingTop(); x += widget.getScrollX(); y += widget.getScrollY(); Layout layout = widget.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); ClickableSpan[] link = buffer.getSpans( off, off, ClickableSpan.class); if (link.length != 0) { if (action == MotionEvent.ACTION_UP) { link[0].onClick(widget); } else if (action == MotionEvent.ACTION_DOWN) { Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0])); } if (widget instanceof ExpandableTextView) { ((ExpandableTextView) widget).linkHit = true; } return true; } else { Selection.removeSelection(buffer); Touch.onTouchEvent(widget, buffer, event); return false; } } return Touch.onTouchEvent(widget, buffer, event); } } boolean dontConsumeNonUrlClicks = true; @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); linkHit = false; boolean res = super.onTouchEvent(event); if (dontConsumeNonUrlClicks) return linkHit; //防止选择复制的状态不消散 if (action == MotionEvent.ACTION_UP) { this.setTextIsSelectable(false); } return res; } public interface OnLinkClickListener { void onLinkClickListener(LinkType type, String content, String selfContent); } public interface OnGetLineCountListener { /** * lineCount 预估大概占据的行数 * canExpand 是否到达可以睁开的条件 */ void onGetLineCount(int lineCount, boolean canExpand); } private OnGetLineCountListener onGetLineCountListener; public OnGetLineCountListener getOnGetLineCountListener() { return onGetLineCountListener; } public void setOnGetLineCountListener(OnGetLineCountListener onGetLineCountListener) { this.onGetLineCountListener = onGetLineCountListener; } public interface OnExpandOrContractClickListener { void onClick(StatusType type); } public OnLinkClickListener getLinkClickListener() { return linkClickListener; } public void setLinkClickListener(OnLinkClickListener linkClickListener) { this.linkClickListener = linkClickListener; } public boolean ismNeedMention() { return mNeedMention; } public void setNeedMention(boolean mNeedMention) { this.mNeedMention = mNeedMention; } public Drawable getLinkDrawable() { return mLinkDrawable; } public void setLinkDrawable(Drawable mLinkDrawable) { this.mLinkDrawable = mLinkDrawable; } public boolean isNeedContract() { return mNeedContract; } public void setNeedContract(boolean mNeedContract) { this.mNeedContract = mNeedContract; } public boolean isNeedExpend() { return mNeedExpend; } public void setNeedExpend(boolean mNeedExpend) { this.mNeedExpend = mNeedExpend; } public boolean isNeedAnimation() { return mNeedAnimation; } public void setNeedAnimation(boolean mNeedAnimation) { this.mNeedAnimation = mNeedAnimation; } public int getExpandableLineCount() { return mLineCount; } public void setExpandableLineCount(int mLineCount) { this.mLineCount = mLineCount; } public int getExpandTextColor() { return mExpandTextColor; } public void setExpandTextColor(int mExpandTextColor) { this.mExpandTextColor = mExpandTextColor; } public int getExpandableLinkTextColor() { return mLinkTextColor; } public void setExpandableLinkTextColor(int mLinkTextColor) { this.mLinkTextColor = mLinkTextColor; } public int getContractTextColor() { return mContractTextColor; } public void setContractTextColor(int mContractTextColor) { this.mContractTextColor = mContractTextColor; } public String getExpandString() { return mExpandString; } public void setExpandString(String mExpandString) { this.mExpandString = mExpandString; } public String getContractString() { return mContractString; } public void setContractString(String mContractString) { this.mContractString = mContractString; } public int getEndExpandTextColor() { return mEndExpandTextColor; } public void setEndExpandTextColor(int mEndExpandTextColor) { this.mEndExpandTextColor = mEndExpandTextColor; } public boolean isNeedLink() { return mNeedLink; } public void setNeedLink(boolean mNeedLink) { this.mNeedLink = mNeedLink; } public int getSelfTextColor() { return mSelfTextColor; } public void setSelfTextColor(int mSelfTextColor) { this.mSelfTextColor = mSelfTextColor; } public boolean isNeedSelf() { return mNeedSelf; } public void setNeedSelf(boolean mNeedSelf) { this.mNeedSelf = mNeedSelf; } public boolean isNeedAlwaysShowRight() { return mNeedAlwaysShowRight; } public void setNeedAlwaysShowRight(boolean mNeedAlwaysShowRight) { this.mNeedAlwaysShowRight = mNeedAlwaysShowRight; } public OnExpandOrContractClickListener getExpandOrContractClickListener() { return expandOrContractClickListener; } public void setExpandOrContractClickListener(OnExpandOrContractClickListener expandOrContractClickListener) { this.expandOrContractClickListener = expandOrContractClickListener; } public void setExpandOrContractClickListener(OnExpandOrContractClickListener expandOrContractClickListener, boolean needRealExpandOrContract) { this.expandOrContractClickListener = expandOrContractClickListener; this.needRealExpandOrContract = needRealExpandOrContract; }}//界说范例的摆列范例public enum LinkType { //寻常链接 LINK_TYPE, //@用户 MENTION_TYPE, TOPIC_TYPE, //自界说规则 SELF}public enum StatusType { //睁开 STATUS_EXPAND, //收起 STATUS_CONTRACT}<?xml version="1.0" encoding="utf-8"?><resources> <string name="app_name">ExpandableTextViewLibrary</string> <string name="social_contract">收起</string> <string name="social_expend">睁开</string> <string name="social_text_target">网页链接</string> <declare-styleable name="ExpandableTextView"> <!--保存的行数--> <attr format="integer" name="ep_max_line"/> <!--是否必要睁开--> <attr format="boolean" name="ep_need_expand"/> <!--是否必要收起 这个是建立在开启睁开的基础上的--> <attr format="boolean" name="ep_need_contract"/> <!--是否必要@用户 --> <attr format="boolean" name="ep_need_mention"/> <!--是否必要对链接举行处理惩罚 --> <attr format="boolean" name="ep_need_link"/> <!--是否必要动画--> <attr format="boolean" name="ep_need_animation"/> <!--是否必要永久将睁开大概收回放置在末了边--> <attr format="boolean" name="ep_need_always_showright"/> <!--是否必要将毗连转换成网页链接表现 默以为true--> <attr format="boolean" name="ep_need_convert_url"/> <!--是否必要自界说规则--> <attr format="boolean" name="ep_need_self"/> <!--收起的文案--> <attr format="string" name="ep_contract_text"/> <!--睁开的文案--> <attr format="string" name="ep_expand_text"/> <!--睁开的笔墨的颜色--> <attr format="color" name="ep_expand_color"/> <!--收起的笔墨的颜色--> <attr format="color" name="ep_contract_color"/> <!--在收回和睁开前面添加的内容的字体颜色--> <attr format="color" name="ep_end_color"/> <!--链接的笔墨的颜色--> <attr format="color" name="ep_link_color"/> <!--@用户的笔墨的颜色--> <attr format="color" name="ep_mention_color"/> <!--@用户的笔墨的颜色--> <attr format="color" name="ep_topic_color"/> <!--自界说规则的笔墨的颜色--> <attr format="color" name="ep_self_color"/> <!--链接的图标--> <attr format="reference" name="ep_link_res"/> </declare-styleable></resources> |