package com.novoda.dropcap; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Typeface; import android.support.annotation.ColorInt; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; public class DropCapView extends View { private static final float SPACING_MULTIPLIER = 1.0f; private final TextPaint dropCapPaint = new TextPaint(); private final TextPaint copyTextPaint = new TextPaint(); private final Rect dropCapBounds = new Rect(); private final Rect characterBounds = new Rect(); private final TypefaceFactory typefaceFactory; private final float spacer; private Layout copyStaticLayout; private Layout dropCapStaticLayout; private String dropCapText; private String copyText; private int lineSpacingExtra; private int numberOfLinesToSpan; private int dropCapWidth; private int dropCapLineHeight; private float dropCapBaseline; private float distanceFromViewPortTop; public DropCapView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DropCapView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); typefaceFactory = new TypefaceFactory(); spacer = getResources().getDimensionPixelSize(R.dimen.drop_cap_space_right); applyCustomAttributes(context, attrs); } private void applyCustomAttributes(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DropCapView); if (typedArray == null) { return; } try { String dropCapFontPath = typedArray.getString(R.styleable.DropCapView_dropCapFontPath); setDropCapFontType(dropCapFontPath); String copyFontPath = typedArray.getString(R.styleable.DropCapView_copyFontPath); setCopyFontType(copyFontPath); int defaultLineSpacingExtra = 0; lineSpacingExtra = typedArray.getDimensionPixelSize(R.styleable.DropCapView_lineSpacingExtra, defaultLineSpacingExtra); int dropCapDefaultTextSize = getResources().getDimensionPixelSize(R.dimen.drop_cap_default_text_size); int dropCapTextSize = typedArray.getDimensionPixelSize(R.styleable.DropCapView_dropCapTextSize, dropCapDefaultTextSize); setDropCapTextSize(TypedValue.COMPLEX_UNIT_PX, dropCapTextSize); int dropCapDefaultTextColor = getResources().getColor(R.color.drop_cap_default_text); int dropCapTextColor = typedArray.getColor(R.styleable.DropCapView_dropCapTextColor, dropCapDefaultTextColor); setDropCapTextColor(dropCapTextColor); int copyDefaultTextSize = getResources().getDimensionPixelSize(R.dimen.drop_cap_copy_default_text_size); int copyTextSize = typedArray.getDimensionPixelSize(R.styleable.DropCapView_copyTextSize, copyDefaultTextSize); setCopyTextSize(TypedValue.COMPLEX_UNIT_PX, copyTextSize); int copyDefaultTextColor = getResources().getColor(R.color.drop_cap_copy_default_text); int copyTextColor = typedArray.getColor(R.styleable.DropCapView_copyTextColor, copyDefaultTextColor); setCopyTextColor(copyTextColor); String text = typedArray.getString(R.styleable.DropCapView_android_text); setText(text); } finally { typedArray.recycle(); } } public void setDropCapFontType(String fontPath) { Typeface typeface = typefaceFactory.createFrom(getContext(), fontPath); if (dropCapPaint.getTypeface() == typeface) { return; } dropCapPaint.setTypeface(typeface); dropCapPaint.setAntiAlias(true); dropCapPaint.setSubpixelText(true); remeasureAndRedraw(); } private void remeasureAndRedraw() { if (dropCapStaticLayout != null || copyStaticLayout != null) { copyStaticLayout = null; dropCapStaticLayout = null; requestLayout(); invalidate(); } } public void setCopyFontType(String fontPath) { Typeface typeface = typefaceFactory.createFrom(getContext(), fontPath); if (copyTextPaint.getTypeface() == typeface) { return; } copyTextPaint.setTypeface(typeface); copyTextPaint.setAntiAlias(true); copyTextPaint.setSubpixelText(true); remeasureAndRedraw(); } public void setDropCapTextSize(float textSizeSp) { setDropCapTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp); } public void setDropCapTextSize(int unit, float size) { float sizeForDisplay = TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics()); setRawDropCapTextSize(sizeForDisplay); } private void setRawDropCapTextSize(float size) { if (size == dropCapPaint.getTextSize()) { return; } dropCapPaint.setTextSize(size); remeasureAndRedraw(); } public float getDropCapTextSize() { return dropCapPaint.getTextSize(); } public void setDropCapTextColor(@ColorInt int color) { if (color == dropCapPaint.getColor()) { return; } dropCapPaint.setColor(color); invalidate(); } @ColorInt public int getDropCapTextColor() { return copyTextPaint.getColor(); } public void setCopyTextSize(float textSizeSp) { setCopyTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp); } public void setCopyTextSize(int unit, float size) { float sizeForDisplay = TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics()); setRawCopyTextSize(sizeForDisplay); } private void setRawCopyTextSize(float size) { if (size == copyTextPaint.getTextSize()) { return; } copyTextPaint.setTextSize(size); remeasureAndRedraw(); } public float getCopyTextSize() { return copyTextPaint.getTextSize(); } public void setCopyTextColor(@ColorInt int color) { if (color == copyTextPaint.getColor()) { return; } copyTextPaint.setColor(color); invalidate(); } @ColorInt public int getCopyTextColor() { return copyTextPaint.getColor(); } public void setText(String text) { if (isSameText(text)) { return; } if (enoughTextForDropCap(text)) { dropCapText = String.valueOf(text.charAt(0)); copyText = String.valueOf(text.subSequence(1, text.length())); } else { dropCapText = String.valueOf('\0'); copyText = (text == null) ? "" : text; } remeasureAndRedraw(); } private boolean isSameText(String text) { return text != null && text.equals(dropCapText + copyText); } private boolean enoughTextForDropCap(CharSequence text) { return text != null && text.length() > 1; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int totalWidth = MeasureSpec.getSize(widthMeasureSpec); int horizontalPadding = getPaddingLeft() + getPaddingRight(); int widthWithoutPadding = totalWidth - horizontalPadding; measureDropCapFor(widthWithoutPadding); if (enoughLinesForDropCap()) { measureRemainingCopyFor(totalWidth); } else { measureWholeTextFor(totalWidth); } int desiredHeight = dropCapLineHeight + copyStaticLayout.getHeight() + getPaddingTop() + getPaddingBottom(); int desiredHeightMeasureSpec = MeasureSpec.makeMeasureSpec(desiredHeight, MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, desiredHeightMeasureSpec); } private void measureDropCapFor(int width) { dropCapWidth = (int) (dropCapPaint.measureText(dropCapText, 0, 1) + spacer); dropCapPaint.getTextBounds(dropCapText, 0, 1, dropCapBounds); int copyWidthForDropCap = width - dropCapWidth; if (dropCapStaticLayout == null || dropCapStaticLayout.getWidth() != copyWidthForDropCap) { dropCapStaticLayout = new StaticLayout( dropCapText + copyText, copyTextPaint, copyWidthForDropCap, Layout.Alignment.ALIGN_NORMAL, SPACING_MULTIPLIER, lineSpacingExtra, true ); calculateLinesToSpan(); if (enoughLinesForDropCap()) { float baseline = dropCapBounds.height() + getPaddingTop(); dropCapBaseline = baseline - dropCapBounds.bottom; } } } private void calculateLinesToSpan() { int currentLineTop = 0; numberOfLinesToSpan = 0; for (int i = 0; i < dropCapStaticLayout.getLineCount(); i++) { currentLineTop = dropCapStaticLayout.getLineTop(i); if (currentLineTop >= dropCapBounds.height()) { numberOfLinesToSpan = i; i = dropCapStaticLayout.getLineCount(); } } dropCapLineHeight = currentLineTop; } private void measureRemainingCopyFor(int totalWidth) { int lineStart = dropCapStaticLayout.getLineEnd(numberOfLinesToSpan - 1); int lineEnd = dropCapStaticLayout.getText().length(); String remainingText = String.valueOf(dropCapStaticLayout.getText().subSequence(lineStart, lineEnd)); if (copyStaticLayout == null || copyStaticLayout.getWidth() != totalWidth || !remainingText.equals(copyStaticLayout.getText())) { copyStaticLayout = new StaticLayout( remainingText, copyTextPaint, totalWidth, Layout.Alignment.ALIGN_NORMAL, SPACING_MULTIPLIER, lineSpacingExtra, true ); distanceFromViewPortTop = calculateCopyDistanceFromViewPortTop(); } } private void measureWholeTextFor(int totalWidth) { if (copyStaticLayout == null || copyStaticLayout.getWidth() != totalWidth) { copyStaticLayout = new StaticLayout( dropCapText + copyText, copyTextPaint, totalWidth, Layout.Alignment.ALIGN_NORMAL, SPACING_MULTIPLIER, lineSpacingExtra, true ); } } private boolean enoughLinesForDropCap() { return dropCapStaticLayout.getLineCount() > numberOfLinesToSpan && numberOfLinesToSpan > 0; } private float calculateCopyDistanceFromViewPortTop() { copyTextPaint.getTextBounds("d", 0, 1, characterBounds); float dHeight = characterBounds.height(); float lineBaseline = copyStaticLayout.getLineBaseline(0); return lineBaseline - dHeight; } @Override protected void onDraw(Canvas canvas) { if (enoughLinesForDropCap()) { drawDropCap(canvas); drawCopyForDropCap(canvas); drawRemainingCopy(canvas); } else { drawCopyWithoutDropCap(canvas); } } private void drawCopyWithoutDropCap(Canvas canvas) { for (int i = 0; i < copyStaticLayout.getLineCount(); i++) { int lineStart = copyStaticLayout.getLineStart(i); int lineEnd = copyStaticLayout.getLineEnd(i); CharSequence charSequence = copyStaticLayout.getText().subSequence(lineStart, lineEnd); int baseline = copyStaticLayout.getLineBaseline(i) + getPaddingTop(); canvas.drawText( charSequence, 0, charSequence.length(), getPaddingLeft(), baseline, copyStaticLayout.getPaint() ); } } private void drawDropCap(Canvas canvas) { float dropCapBaselineFromCopyTop = dropCapBaseline + distanceFromViewPortTop; canvas.drawText(dropCapStaticLayout.getText(), 0, 1, getPaddingLeft(), dropCapBaselineFromCopyTop, dropCapPaint); } private void drawCopyForDropCap(Canvas canvas) { for (int i = 0; i < numberOfLinesToSpan; i++) { int lineStart = dropCapStaticLayout.getLineStart(i); int lineEnd = dropCapStaticLayout.getLineEnd(i); int baseline = dropCapStaticLayout.getLineBaseline(i) + getPaddingTop(); if (i == 0) { lineStart = lineStart + 1; } canvas.drawText( dropCapStaticLayout.getText(), lineStart, lineEnd, getPaddingLeft() + dropCapWidth, baseline, dropCapStaticLayout.getPaint() ); } } private void drawRemainingCopy(Canvas canvas) { int ascentPadding = Math.abs(dropCapStaticLayout.getTopPadding()); int baseline = dropCapStaticLayout.getLineBottom(numberOfLinesToSpan - 1) - ascentPadding + getPaddingTop(); canvas.translate(getPaddingLeft(), baseline); copyStaticLayout.draw(canvas); } }