private FixingResult addSpacesAroundSpansUntilFixed(
      SpannableStringBuilder builder, int widthMeasureSpec, int heightMeasureSpec) {

    Object[] spans = builder.getSpans(0, builder.length(), Object.class);
    List<Object> spansWithSpacesBefore = new ArrayList<Object>(spans.length);
    List<Object> spansWithSpacesAfter = new ArrayList<Object>(spans.length);

    for (Object span : spans) {
      int spanStart = builder.getSpanStart(span);
      if (isNotSpace(builder, spanStart - 1)) {
        builder.insert(spanStart, " ");
        spansWithSpacesBefore.add(span);
      }

      int spanEnd = builder.getSpanEnd(span);
      if (isNotSpace(builder, spanEnd)) {
        builder.insert(spanEnd, " ");
        spansWithSpacesAfter.add(span);
      }

      try {
        setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
        return FixingResult.fixed(spansWithSpacesBefore, spansWithSpacesAfter);
      } catch (IndexOutOfBoundsException notFixed) {
      }
    }
    if (HtmlTextView.DEBUG) {
      Log.d(HtmlTextView.TAG, "Could not fix the Spanned by adding spaces around spans");
    }
    return FixingResult.notFixed();
  }
 private void resetLinkSpan(SpannableStringBuilder ssb, RichTextConfig config, URLSpan urlSpan) {
   int start = ssb.getSpanStart(urlSpan);
   int end = ssb.getSpanEnd(urlSpan);
   ssb.removeSpan(urlSpan);
   LinkHolder linkHolder = new LinkHolder(urlSpan.getURL());
   if (config.linkFixCallback != null) {
     config.linkFixCallback.fix(linkHolder);
   }
   LongClickableURLSpan longClickableURLSpan =
       new LongClickableURLSpan(
           linkHolder, config.onUrlClickListener, config.onUrlLongClickListener);
   ssb.setSpan(longClickableURLSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
 }
  /**
   * Set the necessary spans for each spoiler.
   *
   * <p>The algorithm works in the same way as <code>setCodeFont</code>.
   *
   * @param sequence
   * @return
   */
  private CharSequence setSpoilerStyle(SpannableStringBuilder sequence) {
    int start = 0;
    int end = 0;
    for (int i = 0; i < sequence.length(); i++) {
      if (sequence.charAt(i) == '[' && i < sequence.length() - 3) {
        if (sequence.charAt(i + 1) == '['
            && sequence.charAt(i + 2) == 's'
            && sequence.charAt(i + 3) == '[') {
          start = i;
        }
      } else if (sequence.charAt(i) == ']' && i < sequence.length() - 3) {
        if (sequence.charAt(i + 1) == 's'
            && sequence.charAt(i + 2) == ']'
            && sequence.charAt(i + 3) == ']') {
          end = i;
        }
      }

      if (end > start) {
        sequence.delete(end, end + 4);
        sequence.delete(start, start + 4);
        BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(Color.BLACK);
        ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.BLACK);

        URLSpan urlSpan = sequence.getSpans(start, start, URLSpan.class)[0];
        sequence.setSpan(
            urlSpan, sequence.getSpanStart(urlSpan), start - 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

        // spoiler text has a space at the front
        sequence.setSpan(
            backgroundColorSpan, start + 1, end - 4, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
        sequence.setSpan(foregroundColorSpan, start, end - 4, Spannable.SPAN_INCLUSIVE_INCLUSIVE);

        storedSpoilerSpans.add(foregroundColorSpan);
        storedSpoilerSpans.add(backgroundColorSpan);
        // Shift 1 to account for remove of beginning "<"
        storedSpoilerStarts.add(start - 1);
        storedSpoilerStarts.add(start - 1);
        storedSpoilerEnds.add(end - 5);
        storedSpoilerEnds.add(end - 5);

        sequence.delete(start - 2, start - 1); // remove the trailing <
        start = 0;
        end = 0;
        i = i - 5; // move back to compensate for removal of [[s[
      }
    }

    return sequence;
  }
 /**
  * @desc
  *     <pre>表情解析,转成unicode字符</pre>
  *
  * @author Weiliang Hu
  * @date 2013-12-17
  * @param cs
  * @param mContext
  * @return
  */
 public static String convertToMsg(CharSequence cs, Context mContext) {
   SpannableStringBuilder ssb = new SpannableStringBuilder(cs);
   ImageSpan[] spans = ssb.getSpans(0, cs.length(), ImageSpan.class);
   for (int i = 0; i < spans.length; i++) {
     ImageSpan span = spans[i];
     String c = span.getSource();
     int a = ssb.getSpanStart(span);
     int b = ssb.getSpanEnd(span);
     if (c.contains("[")) {
       ssb.replace(a, b, convertUnicode(c));
     }
   }
   ssb.clearSpans();
   return ssb.toString();
 }
  protected void makeLinkClickable(
      SpannableStringBuilder strBuilder, final URLSpan span, final Activity c) {
    int start = strBuilder.getSpanStart(span);
    int end = strBuilder.getSpanEnd(span);
    int flags = strBuilder.getSpanFlags(span);
    final ClickableSpan clickable =
        new ClickableSpan() {

          public void onClick(View view) {
            // TODO make clickable ContentOpen.openingText(url, true, c);

          }
        };

    strBuilder.setSpan(clickable, start, end, flags);
    strBuilder.removeSpan(span);
  }
  public static void setTextWithIcon(Context context, TextView textView, String text) {
    Spanned html = Html.fromHtml(text);
    SpannableStringBuilder builder = new SpannableStringBuilder(html);

    ImageSpan[] images = builder.getSpans(0, builder.length(), ImageSpan.class);
    if (images != null && images.length > 0) {
      ImageSpan imagePlaceholder = images[0];
      int start = builder.getSpanStart(imagePlaceholder);
      int end = builder.getSpanEnd(imagePlaceholder);
      int flags = builder.getSpanFlags(imagePlaceholder);

      ImageSpan imageSpan = new ImageSpan(context, R.drawable.lock_icon, ImageSpan.ALIGN_BASELINE);
      builder.setSpan(imageSpan, start, end, flags);
      builder.removeSpan(imagePlaceholder);
    }

    textView.setText(builder);
  }
  @SuppressLint("WrongCall")
  private void removeUnneededSpaces(
      int widthMeasureSpec,
      int heightMeasureSpec,
      SpannableStringBuilder builder,
      FixingResult result) {

    for (Object span : result.spansWithSpacesAfter) {
      int spanEnd = builder.getSpanEnd(span);
      builder.delete(spanEnd, spanEnd + 1);
      try {
        setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
      } catch (IndexOutOfBoundsException ignored) {
        builder.insert(spanEnd, " ");
      }
    }

    boolean needReset = true;
    for (Object span : result.spansWithSpacesBefore) {
      int spanStart = builder.getSpanStart(span);
      builder.delete(spanStart - 1, spanStart);
      try {
        setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
        needReset = false;
      } catch (IndexOutOfBoundsException ignored) {
        needReset = true;
        int newSpanStart = spanStart - 1;
        builder.insert(newSpanStart, " ");
      }
    }

    if (needReset) {
      setText(builder);
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
  }
  private void icUpdateGecko(boolean force) {

    // Skip if receiving a repeated request, or
    // if suppressing compositions during text selection.
    if ((!force && mIcUpdateSeqno == mLastIcUpdateSeqno) || mSuppressCompositions) {
      if (DEBUG) {
        Log.d(LOGTAG, "icUpdateGecko() skipped");
      }
      return;
    }
    mLastIcUpdateSeqno = mIcUpdateSeqno;
    mActionQueue.syncWithGecko();

    if (DEBUG) {
      Log.d(LOGTAG, "icUpdateGecko()");
    }

    final int selStart = mText.getSpanStart(Selection.SELECTION_START);
    final int selEnd = mText.getSpanEnd(Selection.SELECTION_END);
    int composingStart = mText.length();
    int composingEnd = 0;
    Object[] spans = mText.getSpans(0, composingStart, Object.class);

    for (Object span : spans) {
      if ((mText.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
        composingStart = Math.min(composingStart, mText.getSpanStart(span));
        composingEnd = Math.max(composingEnd, mText.getSpanEnd(span));
      }
    }
    if (DEBUG) {
      Log.d(LOGTAG, " range = " + composingStart + "-" + composingEnd);
      Log.d(LOGTAG, " selection = " + selStart + "-" + selEnd);
    }
    if (composingStart >= composingEnd) {
      if (selStart >= 0 && selEnd >= 0) {
        onImeSetSelection(selStart, selEnd);
      } else {
        onImeRemoveComposition();
      }
      return;
    }

    if (selEnd >= composingStart && selEnd <= composingEnd) {
      onImeAddCompositionRange(
          selEnd - composingStart,
          selEnd - composingStart,
          IME_RANGE_CARETPOSITION,
          0,
          0,
          false,
          0,
          0,
          0);
    }
    int rangeStart = composingStart;
    TextPaint tp = new TextPaint();
    TextPaint emptyTp = new TextPaint();
    // set initial foreground color to 0, because we check for tp.getColor() == 0
    // below to decide whether to pass a foreground color to Gecko
    emptyTp.setColor(0);
    do {
      int rangeType, rangeStyles = 0, rangeLineStyle = IME_RANGE_LINE_NONE;
      boolean rangeBoldLine = false;
      int rangeForeColor = 0, rangeBackColor = 0, rangeLineColor = 0;
      int rangeEnd = mText.nextSpanTransition(rangeStart, composingEnd, Object.class);

      if (selStart > rangeStart && selStart < rangeEnd) {
        rangeEnd = selStart;
      } else if (selEnd > rangeStart && selEnd < rangeEnd) {
        rangeEnd = selEnd;
      }
      CharacterStyle[] styleSpans = mText.getSpans(rangeStart, rangeEnd, CharacterStyle.class);

      if (DEBUG) {
        Log.d(LOGTAG, " found " + styleSpans.length + " spans @ " + rangeStart + "-" + rangeEnd);
      }

      if (styleSpans.length == 0) {
        rangeType =
            (selStart == rangeStart && selEnd == rangeEnd)
                ? IME_RANGE_SELECTEDRAWTEXT
                : IME_RANGE_RAWINPUT;
      } else {
        rangeType =
            (selStart == rangeStart && selEnd == rangeEnd)
                ? IME_RANGE_SELECTEDCONVERTEDTEXT
                : IME_RANGE_CONVERTEDTEXT;
        tp.set(emptyTp);
        for (CharacterStyle span : styleSpans) {
          span.updateDrawState(tp);
        }
        int tpUnderlineColor = 0;
        float tpUnderlineThickness = 0.0f;

        // These TextPaint fields only exist on Android ICS+ and are not in the SDK.
        if (Versions.feature14Plus) {
          tpUnderlineColor = (Integer) getField(tp, "underlineColor", 0);
          tpUnderlineThickness = (Float) getField(tp, "underlineThickness", 0.0f);
        }
        if (tpUnderlineColor != 0) {
          rangeStyles |= IME_RANGE_UNDERLINE | IME_RANGE_LINECOLOR;
          rangeLineColor = tpUnderlineColor;
          // Approximately translate underline thickness to what Gecko understands
          if (tpUnderlineThickness <= 0.5f) {
            rangeLineStyle = IME_RANGE_LINE_DOTTED;
          } else {
            rangeLineStyle = IME_RANGE_LINE_SOLID;
            if (tpUnderlineThickness >= 2.0f) {
              rangeBoldLine = true;
            }
          }
        } else if (tp.isUnderlineText()) {
          rangeStyles |= IME_RANGE_UNDERLINE;
          rangeLineStyle = IME_RANGE_LINE_SOLID;
        }
        if (tp.getColor() != 0) {
          rangeStyles |= IME_RANGE_FORECOLOR;
          rangeForeColor = tp.getColor();
        }
        if (tp.bgColor != 0) {
          rangeStyles |= IME_RANGE_BACKCOLOR;
          rangeBackColor = tp.bgColor;
        }
      }
      onImeAddCompositionRange(
          rangeStart - composingStart,
          rangeEnd - composingStart,
          rangeType,
          rangeStyles,
          rangeLineStyle,
          rangeBoldLine,
          rangeForeColor,
          rangeBackColor,
          rangeLineColor);
      rangeStart = rangeEnd;

      if (DEBUG) {
        Log.d(
            LOGTAG,
            " added "
                + rangeType
                + " : "
                + Integer.toHexString(rangeStyles)
                + " : "
                + Integer.toHexString(rangeForeColor)
                + " : "
                + Integer.toHexString(rangeBackColor));
      }
    } while (rangeStart < composingEnd);

    onImeUpdateComposition(composingStart, composingEnd);
  }
  private int handleImage(
      SpannableStringBuilder ssb,
      ImageGetterWrapper imageGetterWrapper,
      RichTextConfig config,
      boolean cached) {
    if (cached) {
      ClickableImageSpan[] cis = ssb.getSpans(0, ssb.length(), ClickableImageSpan.class);
      if (cis != null && cis.length > 0) {
        for (ClickableImageSpan ci : cis) {
          int start = ssb.getSpanStart(ci);
          int end = ssb.getSpanEnd(ci);
          ssb.removeSpan(ci);
          OnImageClickListener onImageClickListener = null;
          OnImageLongClickListener onImageLongClickListener = null;
          if (config.clickable > 0) {
            onImageClickListener = config.onImageClickListener;
            onImageLongClickListener = config.onImageLongClickListener;
          }
          Drawable drawable = imageGetterWrapper.getDrawable(ci.getSource());
          if (drawable == null) {
            drawable = new ColorDrawable(Color.TRANSPARENT);
          }
          ClickableImageSpan nci =
              new ClickableImageSpan(drawable, ci, onImageClickListener, onImageLongClickListener);
          ssb.setSpan(nci, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        return cis.length;
      }
    } else if (!config.noImage) {
      ImageSpan[] iss = ssb.getSpans(0, ssb.length(), ImageSpan.class);
      if (iss != null && iss.length > 0) {
        ArrayList<String> imageUrls = new ArrayList<>(iss.length);
        for (int i = 0; i < iss.length; i++) {
          ImageSpan imageSpan = iss[i];
          String imageUrl = imageSpan.getSource();
          imageUrls.add(imageUrl);

          int start = ssb.getSpanStart(imageSpan);
          int end = ssb.getSpanEnd(imageSpan);
          ClickableSpan[] clickableSpans = ssb.getSpans(start, end, ClickableSpan.class);
          if (clickableSpans != null && clickableSpans.length != 0) {
            for (ClickableSpan cs : clickableSpans) {
              ssb.removeSpan(cs);
            }
          }
          OnImageClickListener onImageClickListener = null;
          OnImageLongClickListener onImageLongClickListener = null;
          if (config.clickable > 0) {
            onImageClickListener = config.onImageClickListener;
            onImageLongClickListener = config.onImageLongClickListener;
          }
          Drawable drawable = imageGetterWrapper.getDrawable(imageUrl);
          if (drawable == null) {
            drawable = new ColorDrawable(Color.TRANSPARENT);
          }
          ClickableImageSpan cacheImageSpan =
              new ClickableImageSpan(
                  drawable, imageUrls, i, onImageClickListener, onImageLongClickListener);
          ssb.removeSpan(imageSpan);
          ssb.setSpan(cacheImageSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        return iss.length;
      }
    }
    return 0;
  }