Android 自定义可展开收回能够@xxx和#话题的TextView(仿小红书效果)

前段时间接到一个需求,需要完成以下效果。


image.png

大致功能和小红书效果类似 可以 展开 和收起 也可以@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);
image.png

有了这些东西经过简单的计算我们就可以获取到我们需要截取的内容长度。对原内容进行截取再拼接上“展开”或“收回”即可!

/**
    * 计算原内容被裁剪的长度
    *
    * @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 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 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 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 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 convert = new HashMap<>();
       //对自定义的进行正则匹配
       if (mNeedSelf) {
           List 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 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 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 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
}



  ExpandableTextViewLibrary
  收起
  展开
  网页链接
  
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      

      
      
      
      
      
      
  

你可能感兴趣的:(Android 自定义可展开收回能够@xxx和#话题的TextView(仿小红书效果))