private void reflow(CharSequence s, int where, int before, int after) { if (s != mBase) return; CharSequence text = mDisplay; int len = text.length(); // seek back to the start of the paragraph int find = TextUtils.lastIndexOf(text, '\n', where - 1); if (find < 0) find = 0; else find = find + 1; { int diff = where - find; before += diff; after += diff; where -= diff; } // seek forward to the end of the paragraph int look = TextUtils.indexOf(text, '\n', where + after); if (look < 0) look = len; else look++; // we want the index after the \n int change = look - (where + after); before += change; after += change; // seek further out to cover anything that is forced to wrap together if (text instanceof Spanned) { Spanned sp = (Spanned) text; boolean again; do { again = false; Object[] force = sp.getSpans(where, where + after, WrapTogetherSpan.class); for (int i = 0; i < force.length; i++) { int st = sp.getSpanStart(force[i]); int en = sp.getSpanEnd(force[i]); if (st < where) { again = true; int diff = where - st; before += diff; after += diff; where -= diff; } if (en > where + after) { again = true; int diff = en - (where + after); before += diff; after += diff; } } } while (again); } // find affected region of old layout int startline = getLineForOffset(where); int startv = getLineTop(startline); int endline = getLineForOffset(where + before); if (where + after == len) endline = getLineCount(); int endv = getLineTop(endline); boolean islast = (endline == getLineCount()); // generate new layout for affected text StaticLayout reflowed; synchronized (sLock) { reflowed = sStaticLayout; sStaticLayout = null; } if (reflowed == null) { reflowed = new StaticLayout(null); } else { reflowed.prepare(); } reflowed.generate( text, where, where + after, getPaint(), getWidth(), getTextDirectionHeuristic(), getSpacingMultiplier(), getSpacingAdd(), false, true, mEllipsizedWidth, mEllipsizeAt); int n = reflowed.getLineCount(); // If the new layout has a blank line at the end, but it is not // the very end of the buffer, then we already have a line that // starts there, so disregard the blank line. if (where + after != len && reflowed.getLineStart(n - 1) == where + after) n--; // remove affected lines from old layout mInts.deleteAt(startline, endline - startline); mObjects.deleteAt(startline, endline - startline); // adjust offsets in layout for new height and offsets int ht = reflowed.getLineTop(n); int toppad = 0, botpad = 0; if (mIncludePad && startline == 0) { toppad = reflowed.getTopPadding(); mTopPadding = toppad; ht -= toppad; } if (mIncludePad && islast) { botpad = reflowed.getBottomPadding(); mBottomPadding = botpad; ht += botpad; } mInts.adjustValuesBelow(startline, START, after - before); mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); // insert new layout int[] ints; if (mEllipsize) { ints = new int[COLUMNS_ELLIPSIZE]; ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; } else { ints = new int[COLUMNS_NORMAL]; } Directions[] objects = new Directions[1]; for (int i = 0; i < n; i++) { ints[START] = reflowed.getLineStart(i) | (reflowed.getParagraphDirection(i) << DIR_SHIFT) | (reflowed.getLineContainsTab(i) ? TAB_MASK : 0); int top = reflowed.getLineTop(i) + startv; if (i > 0) top -= toppad; ints[TOP] = top; int desc = reflowed.getLineDescent(i); if (i == n - 1) desc += botpad; ints[DESCENT] = desc; objects[0] = reflowed.getLineDirections(i); if (mEllipsize) { ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); } mInts.insertAt(startline + i, ints); mObjects.insertAt(startline + i, objects); } synchronized (sLock) { sStaticLayout = reflowed; reflowed.finish(); } }
public void build(MessageWireframe wireframe, int desiredWidth, StelsApplication application) { Logger.d(TAG, "Build layout start"); checkResources(application); senderPaint = initTextPaint(); senderPaint.setTypeface(FontController.loadTypeface(application, "regular")); senderPaint.setTextSize(bodyPaint.getTextSize()); senderPaint.setColor(0xff000000); forwardingPaint = initTextPaint(); forwardingPaint.setTypeface(FontController.loadTypeface(application, "light")); forwardingPaint.setTextSize(bodyPaint.getTextSize()); forwardingPaint.setColor(0xff000000); this.layoutDesiredWidth = desiredWidth; this.isOut = wireframe.message.isOut(); this.showState = isOut; this.isGroup = wireframe.message.getPeerType() == PeerType.PEER_CHAT && !isOut; if (isGroup) { User user = wireframe.senderUser; this.senderName = user.getDisplayName(); if (!wireframe.message.isForwarded()) { senderPaint.setColor( Placeholders.USER_PLACEHOLDERS_COLOR[ wireframe.message.getSenderId() % Placeholders.USER_PLACEHOLDERS_COLOR.length]); forwardingPaint.setColor( Placeholders.USER_PLACEHOLDERS_COLOR[ wireframe.message.getSenderId() % Placeholders.USER_PLACEHOLDERS_COLOR.length]); } } if (wireframe.message.isForwarded()) { isForwarded = true; this.forwarderName = wireframe.forwardUser.getDisplayName(); if (isOut) { forwardingPaint.setColor(0xff739f53); senderPaint.setColor(0xff739f53); } else { forwardingPaint.setColor(0xff4884cf); senderPaint.setColor(0xff4884cf); } } else { isForwarded = false; } if (isGroup) { User user = application.getEngine().getUser(wireframe.message.getSenderId()); this.senderName = user.getDisplayName(); } if (wireframe.message.isForwarded()) { isForwarded = true; this.forwarderName = wireframe.forwardUser.getDisplayName(); } else { isForwarded = false; } layoutDesiredWidth = desiredWidth; long start = SystemClock.uptimeMillis(); this.spannable = application .getEmojiProcessor() .processEmojiCompatMutable( wireframe.message.getMessage(), EmojiProcessor.CONFIGURATION_BUBBLES); // spannable = new SpannableString(wireframe.message.getMessage()); Logger.d(TAG, "Emoji processed in " + (SystemClock.uptimeMillis() - start) + " ms"); start = SystemClock.uptimeMillis(); Linkify.addLinks( this.spannable, Linkify.WEB_URLS | Linkify.PHONE_NUMBERS | Linkify.EMAIL_ADDRESSES); fixLinks(spannable); Logger.d(TAG, "Added links in " + (SystemClock.uptimeMillis() - start) + " ms"); start = SystemClock.uptimeMillis(); layout = new StaticLayout( spannable, bodyPaint, desiredWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); Logger.d(TAG, "Built base layout in " + (SystemClock.uptimeMillis() - start) + " ms"); if (layout.getLineCount() < 20) { int layoutTextWidth = 0; for (int i = 0; i < layout.getLineCount(); i++) { layoutTextWidth = (int) Math.max(layout.getLineWidth(i), layoutTextWidth); } if (layoutTextWidth < layout.getWidth() - px(10)) { layout = new StaticLayout( spannable, bodyPaint, layoutTextWidth + px(2), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); } } layoutRealWidth = layout.getWidth(); timeWidth = (int) clockOutPaint.measureText(wireframe.date) + px((showState ? 23 : 0) + 6); if (layout.getLineCount() == 1) { boolean isLastRtl = layout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT; if (!isLastRtl && desiredWidth - layoutRealWidth > timeWidth) { layoutRealWidth += timeWidth; layoutHeight = layout.getHeight() + px(3); } else if (isLastRtl && desiredWidth - layout.getWidth() > timeWidth) { layoutRealWidth = layout.getWidth() + timeWidth; layoutHeight = layout.getHeight() + px(3); } else { if (isLastRtl) { layoutRealWidth = layout.getWidth(); } layoutHeight = layout.getHeight() + px(17); } } else { boolean isLastRtl = layout.getParagraphDirection(layout.getLineCount() - 1) == Layout.DIR_RIGHT_TO_LEFT; if (!isLastRtl && (desiredWidth - layout.getLineWidth(layout.getLineCount() - 1) > timeWidth)) { layoutRealWidth = (int) Math.max( layoutRealWidth, layout.getLineWidth(layout.getLineCount() - 1) + timeWidth); layoutHeight = layout.getHeight() + px(3); } else if (isLastRtl && (desiredWidth - layout.getWidth() > timeWidth)) { layoutRealWidth = (int) Math.max(layoutRealWidth, layout.getWidth() + timeWidth); layoutHeight = layout.getHeight() + px(3); } else { layoutHeight = layout.getHeight() + px(17); } } if (layoutRealWidth < timeWidth) { layoutRealWidth = timeWidth; } if (isForwarded) { layoutHeight += px(19) * 2; forwardOffset = (int) forwardingPaintBase.measureText("From "); layoutRealWidth = (int) Math.max(layoutRealWidth, forwardingPaintBase.measureText("Forwarded message")); forwarderNameMeasured = TextUtils.ellipsize( forwarderName, senderPaintBase, desiredWidth - forwardOffset, TextUtils.TruncateAt.END) .toString(); layoutRealWidth = (int) Math.max( layoutRealWidth, forwardOffset + senderPaintBase.measureText(forwarderNameMeasured)); } if (isGroup && !isOut && !isForwarded) { layoutHeight += px(19); senderNameMeasured = TextUtils.ellipsize(senderName, senderPaintBase, desiredWidth, TextUtils.TruncateAt.END) .toString(); int width = (int) senderPaintBase.measureText(senderNameMeasured); layoutRealWidth = Math.max(layoutRealWidth, width); } Logger.d(TAG, "Build layout end"); }
private void build(int width) { positions.clear(); width = width - getPaddingLeft() - getPaddingRight(); if (width <= 0 || TextUtils.isEmpty(text)) { truncatedLayout = expandedLayout = null; return; } for (BubbleSpan span : spans) { span.resetWidth(width); } try { truncatedLayout = expandedLayout = new StaticLayout( text, textPaint, width, Layout.Alignment.ALIGN_NORMAL, lineSpacing, 1, false); if (maxLines > 0 && truncatedLayout.getLineCount() > maxLines) { int lineEnd = truncatedLayout.getLineEnd(maxLines - 1); int offset = -1; StaticLayout sl = new StaticLayout( moreText, textPaint, width, Layout.Alignment.ALIGN_NORMAL, lineSpacing, 1, false); sl.getWidth(); while (truncatedLayout.getLineCount() > maxLines && lineEnd > 0) { if (offset == -1 && truncatedLayout.getLineWidth(maxLines - 1) + sl.getLineWidth(0) > width) { offset = truncatedLayout.getOffsetForHorizontal(maxLines - 1, width - sl.getLineWidth(0)); lineEnd = offset; } else if (offset > 0) { lineEnd--; } SpannableStringBuilder textTruncated = new SpannableStringBuilder(text.subSequence(0, lineEnd)); textTruncated.append(moreText); truncatedLayout = new StaticLayout( textTruncated, textPaint, width, Layout.Alignment.ALIGN_NORMAL, lineSpacing, 1, false); } } } catch (java.lang.ArrayIndexOutOfBoundsException e) { return; } if (truncated) { recomputeSpans((Spannable) truncatedLayout.getText()); } else { recomputeSpans((Spannable) expandedLayout.getText()); } for (BubbleSpan span : spans) { positions.put(span, span.rect(this)); } }
public List<Integer> getPageOffsets(CharSequence text, boolean includePageNumbers) { if (text == null) { return new ArrayList<Integer>(); } List<Integer> pageOffsets = new ArrayList<Integer>(); TextPaint textPaint = bookView.getInnerView().getPaint(); int boundedWidth = bookView.getInnerView().getWidth(); LOG.debug("Page width: " + boundedWidth); StaticLayout layout = layoutFactory.create(text, textPaint, boundedWidth, bookView.getLineSpacing()); LOG.debug("Layout height: " + layout.getHeight()); layout.draw(new Canvas()); // Subtract the height of the top margin int pageHeight = bookView.getHeight() - bookView.getVerticalMargin(); if (includePageNumbers) { String bottomSpace = "0\n"; StaticLayout numLayout = layoutFactory.create(bottomSpace, textPaint, boundedWidth, bookView.getLineSpacing()); numLayout.draw(new Canvas()); // Subtract the height needed to show page numbers, or the // height of the margin, whichever is more pageHeight = pageHeight - Math.max(numLayout.getHeight(), bookView.getVerticalMargin()); } else { // Just subtract the bottom margin pageHeight = pageHeight - bookView.getVerticalMargin(); } LOG.debug("Got pageHeight " + pageHeight); int totalLines = layout.getLineCount(); int topLineNextPage = -1; int pageStartOffset = 0; while (topLineNextPage < totalLines - 1) { LOG.debug("Processing line " + topLineNextPage + " / " + totalLines); int topLine = layout.getLineForOffset(pageStartOffset); topLineNextPage = layout.getLineForVertical(layout.getLineTop(topLine) + pageHeight); LOG.debug("topLine " + topLine + " / " + topLineNextPage); if (topLineNextPage == topLine) { // If lines are bigger than can fit on a page topLineNextPage = topLine + 1; } int pageEnd = layout.getLineEnd(topLineNextPage - 1); LOG.debug("pageStartOffset=" + pageStartOffset + ", pageEnd=" + pageEnd); if (pageEnd > pageStartOffset) { if (text.subSequence(pageStartOffset, pageEnd).toString().trim().length() > 0) { pageOffsets.add(pageStartOffset); } pageStartOffset = layout.getLineStart(topLineNextPage); } } return pageOffsets; }