org.wikipedia.page.shareafact.SnippetImage.java Source code

Java tutorial

Introduction

Here is the source code for org.wikipedia.page.shareafact.SnippetImage.java

Source

package org.wikipedia.page.shareafact;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.text.Layout;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;

import org.wikipedia.R;
import org.wikipedia.gallery.ImageLicense;
import org.wikipedia.util.L10nUtil;
import org.wikipedia.util.StringUtil;

import static android.text.Layout.Alignment.ALIGN_NORMAL;
import static android.text.Layout.Alignment.ALIGN_OPPOSITE;

/**
 * Creator and holder of a Bitmap which is comprised of an optional lead image, a title,
 * optional description, text, the Wikipedia wordmark, and some license icons.
 *
 * Creates a device-independent bitmap object; all dimension values are in px, not dp.
 */
public final class SnippetImage {
    private static final int WIDTH = 640;
    private static final int HEIGHT = 360;
    private static final int BOTTOM_PADDING = 25;
    private static final int HORIZONTAL_PADDING = 30;
    private static final int TOP_PADDING = 22;
    private static final int TEXT_WIDTH = WIDTH - 2 * HORIZONTAL_PADDING;
    private static final int DESCRIPTION_WIDTH = 360;
    private static final int ICONS_WIDTH = 16;
    private static final int ICONS_HEIGHT = 16;
    private static final float SPACING_MULTIPLIER = 1.0f;
    private static final Typeface SERIF = Typeface.create("serif", Typeface.NORMAL);
    private static final int QUARTER = 4;

    /**
     * Creates a card image usable for sharing and the preview of the same.
     * If we have a leadImageBitmap the use that as the background. If not then
     * just use a black background.
     */
    public static Bitmap getSnippetImage(@NonNull Context context, @Nullable Bitmap leadImageBitmap,
            @NonNull String title, @Nullable String description, @NonNull CharSequence textSnippet,
            @NonNull ImageLicense license) {
        Bitmap resultBitmap = drawBackground(leadImageBitmap, license);
        Canvas canvas = new Canvas(resultBitmap);
        if (leadImageBitmap != null) {
            drawGradient(canvas);
        }

        Layout textLayout = drawTextSnippet(canvas, textSnippet);
        boolean isArticleRTL = textLayout.getParagraphDirection(0) == Layout.DIR_RIGHT_TO_LEFT;

        drawLicenseIcons(context, leadImageBitmap, license, canvas, isArticleRTL);
        int top = drawDescription(canvas, description, HEIGHT - BOTTOM_PADDING - ICONS_HEIGHT, isArticleRTL);
        drawTitle(canvas, title, top, isArticleRTL);
        if (L10nUtil.canLangUseImageForWikipediaWordmark(context)) {
            drawWordmarkFromStaticImage(context, canvas, isArticleRTL);
        } else {
            drawWordmarkFromText(context, canvas, isArticleRTL);
        }

        return resultBitmap;
    }

    @NonNull
    private static Bitmap drawBackground(@Nullable Bitmap leadImageBitmap, @NonNull ImageLicense license) {
        Bitmap resultBitmap;
        if (leadImageBitmap != null && license.hasLicenseInfo()) {
            // use lead image
            resultBitmap = scaleCropToFit(leadImageBitmap, WIDTH, HEIGHT);
        } else {
            resultBitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
            final int backgroundColor = 0xff242438;
            resultBitmap.eraseColor(backgroundColor);
        }
        return resultBitmap;
    }

    private static void drawGradient(@NonNull Canvas canvas) {
        // draw a dark gradient over the image, so that the white text
        // will stand out better against it.
        final int gradientStartColor = 0x60000000;
        final int gradientStopColor = 0xA0000000;
        Shader shader = new LinearGradient(0, 0, 0, canvas.getHeight(), gradientStartColor, gradientStopColor,
                Shader.TileMode.CLAMP);
        Paint paint = new Paint();
        paint.setShader(shader);
        canvas.drawRect(new Rect(0, 0, canvas.getWidth(), canvas.getHeight()), paint);
    }

    @NonNull
    private static Layout drawTextSnippet(@NonNull Canvas canvas, @NonNull CharSequence textSnippet) {
        final int top = TOP_PADDING;
        final int maxHeight = 225;
        final int maxLines = 5;
        final float maxFontSize = 195.0f;
        final float minFontSize = 32.0f;

        TextPaint textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(maxFontSize);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setTypeface(Typeface.DEFAULT_BOLD);
        textPaint.setShadowLayer(1.0f, 1.0f, 1.0f, Color.GRAY);

        StaticLayout textLayout = optimizeTextSize(
                new TextLayoutParams(textSnippet, textPaint, TEXT_WIDTH, SPACING_MULTIPLIER), maxHeight, maxLines,
                maxFontSize, minFontSize);

        canvas.save();
        int horizontalCenterOffset = top + (maxHeight - textLayout.getHeight()) / QUARTER;
        canvas.translate(HORIZONTAL_PADDING, horizontalCenterOffset);
        textLayout.draw(canvas);
        canvas.restore();

        return textLayout;
    }

    private static int drawDescription(@NonNull Canvas canvas, @Nullable String description, int top,
            boolean isArticleRTL) {
        final int marginBottom = 5;
        final int maxHeight = 23;
        final int maxLines = 2;
        final float maxFontSize = 15.0f;
        final float minFontSize = 10.0f;

        if (TextUtils.isEmpty(description)) {
            return top - marginBottom;
        }
        TextPaint textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(maxFontSize);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setShadowLayer(1.0f, 0.0f, 0.0f, Color.GRAY);

        StaticLayout textLayout = optimizeTextSize(
                new TextLayoutParams(description, textPaint, DESCRIPTION_WIDTH, SPACING_MULTIPLIER), maxHeight,
                maxLines, maxFontSize, minFontSize);
        int left = HORIZONTAL_PADDING;
        if (isArticleRTL) {
            left = WIDTH - HORIZONTAL_PADDING - textLayout.getWidth();
        }

        top = top - marginBottom - textLayout.getHeight();
        canvas.save();
        canvas.translate(left, top);
        textLayout.draw(canvas);
        canvas.restore();

        return top;
    }

    private static void drawTitle(@NonNull Canvas canvas, @NonNull String title, int top, boolean isArticleRTL) {
        final int marginBottom = 0;
        final int maxHeight = 70;
        final int maxLines = 2;
        final float maxFontSize = 30.0f;
        final float spacingMultiplier = 0.7f;

        TextPaint textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(maxFontSize);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setTypeface(SERIF);
        textPaint.setShadowLayer(1.0f, 0.0f, 1.0f, Color.GRAY);

        StaticLayout textLayout = optimizeTextSize(
                new TextLayoutParams(title, textPaint, DESCRIPTION_WIDTH, spacingMultiplier), maxHeight, maxLines,
                maxFontSize, maxFontSize);
        int left = HORIZONTAL_PADDING;
        if (isArticleRTL) {
            left = WIDTH - HORIZONTAL_PADDING - textLayout.getWidth();
        }
        int marginBottomTotal = marginBottom;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            // versions < 5.0 don't compensate for bottom margin correctly when line
            // spacing is less than 1.0, so we'll compensate ourselves
            final int marginBoost = 10;
            marginBottomTotal += marginBoost;
        }

        top = top - marginBottomTotal - textLayout.getHeight();
        canvas.save();
        canvas.translate(left, top);
        textLayout.draw(canvas);
        canvas.restore();
    }

    private static void drawLicenseIcons(@NonNull Context context, @Nullable Bitmap leadImageBitmap,
            @NonNull ImageLicense license, @NonNull Canvas canvas, boolean isArticleRTL) {
        final int bottom = SnippetImage.HEIGHT - SnippetImage.BOTTOM_PADDING;
        final int top = bottom - SnippetImage.ICONS_HEIGHT;
        int left = SnippetImage.HORIZONTAL_PADDING;
        int right = left + SnippetImage.ICONS_WIDTH;

        if (isArticleRTL) {
            right = SnippetImage.WIDTH - SnippetImage.HORIZONTAL_PADDING;
            left = right - SnippetImage.ICONS_WIDTH;
        }

        Drawable d = ContextCompat.getDrawable(context,
                shouldDefaultToCCLicense(leadImageBitmap, license) ? R.drawable.ic_license_cc
                        : license.getLicenseIcon());
        d.setBounds(left, top, right, bottom);
        d.draw(canvas);
    }

    /**
     * Default to showing Creative Commons license icon for card as a whole if lead image is not present
     * or will not be used due to a lack of licensing data.
     */
    private static boolean shouldDefaultToCCLicense(@Nullable Bitmap leadImageBitmap,
            @NonNull ImageLicense license) {
        return leadImageBitmap == null || !license.hasLicenseInfo();
    }

    private static void drawWordmarkFromStaticImage(@NonNull Context context, @NonNull Canvas canvas,
            boolean isArticleRTL) {
        // scaling it a bit down from original 317x54px size
        final int width = 130;
        final int height = 22;
        final int bottom = HEIGHT - BOTTOM_PADDING;
        final int top = bottom - height;

        Drawable d = ContextCompat.getDrawable(context, R.drawable.wp_wordmark);
        DrawableCompat.setTint(d, Color.LTGRAY);

        int left = WIDTH - HORIZONTAL_PADDING - width;
        if (isArticleRTL) {
            left = HORIZONTAL_PADDING;
        }
        int right = left + width;

        d.setBounds(left, top, right, bottom);
        d.draw(canvas);
    }

    private static void drawWordmarkFromText(@NonNull Context context, @NonNull Canvas canvas,
            boolean isArticleRTL) {
        final int maxWidth = WIDTH - DESCRIPTION_WIDTH - 2 * HORIZONTAL_PADDING;
        final float fontSize = 20.0f;
        final float scaleX = 1.06f;

        TextPaint textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.LTGRAY);
        textPaint.setTextSize(fontSize);
        textPaint.setTypeface(SERIF);
        textPaint.setTextScaleX(scaleX);

        Spanned wikipedia = StringUtil.fromHtml(context.getString(R.string.wp_stylized));
        Layout.Alignment align = L10nUtil.isDeviceRTL() ? ALIGN_OPPOSITE : ALIGN_NORMAL;
        StaticLayout wordmarkLayout = buildLayout(
                new TextLayoutParams(wikipedia, textPaint, maxWidth, 1.0f, align));
        final int width = (int) wordmarkLayout.getLineWidth(0);
        final int height = wordmarkLayout.getHeight();

        final int bottom = HEIGHT - BOTTOM_PADDING;
        final int top = bottom - height;

        int left = WIDTH - HORIZONTAL_PADDING - width;
        if (isArticleRTL) {
            left = HORIZONTAL_PADDING;
        }

        canvas.save(); // --
        canvas.translate(left, top);
        wordmarkLayout.draw(canvas);
        canvas.restore(); // --
    }

    /**
     * If the title or text is too long we first reduce the font size.
     * If that is not enough it gets ellipsized.
     */
    private static StaticLayout optimizeTextSize(TextLayoutParams params, int maxHeight, int maxLines,
            float maxFontSize, float minFontSize) {
        final float threshold1 = 60.0f;
        final float threshold2 = 40.0f;
        final float extraStep1 = 3.0f;
        final float extraStep2 = 1.0f;
        boolean fits = false;
        StaticLayout textLayout = null;

        // Try decreasing font size first
        for (float fontSize = maxFontSize; fontSize >= minFontSize; fontSize -= 1.0f) {
            params.textPaint.setTextSize(fontSize);
            textLayout = buildLayout(params);
            if (textLayout.getHeight() <= maxHeight) {
                fits = true;
                break;
            }

            // make it go faster at the beginning...
            if (fontSize > threshold1) {
                fontSize -= extraStep1;
            } else if (fontSize > threshold2) {
                fontSize -= extraStep2;
            }
        }

        // Then do own ellipsize: cut text off after last fitting space and add "..."
        // Didn't want to cut off randomly in the middle of a line or word.
        if (!fits) {
            final String textStr = params.text.toString();
            final int ellipsisLength = 3;
            final int ellipsisStart = textLayout != null ? textLayout.getLineStart(maxLines) - ellipsisLength
                    : textStr.length();
            final int end = textStr.lastIndexOf(' ', ellipsisStart) + 1;
            if (end > 0) {
                textLayout = buildLayout(new TextLayoutParams(params, textStr.substring(0, end) + "..."));
                if (textLayout.getLineCount() <= maxLines) {
                    fits = true;
                }
            }
        }

        // last resort: use TextUtils.ellipsize()
        if (!fits) {
            final float textRatio = .87f;
            final float maxWidth = textRatio * maxLines * params.lineWidth;
            textLayout = buildLayout(new TextLayoutParams(params,
                    TextUtils.ellipsize(params.text, params.textPaint, maxWidth, TextUtils.TruncateAt.END)));
        }

        return textLayout;
    }

    private static StaticLayout buildLayout(TextLayoutParams params) {
        return new StaticLayout(params.text, params.textPaint, params.lineWidth, params.align,
                params.spacingMultiplier, 0.0f, false);
    }

    // Borrowed from http://stackoverflow.com/questions/5226922/crop-to-fit-image-in-android
    // Modified to allow for face detection adjustment, startY
    @NonNull
    private static Bitmap scaleCropToFit(@NonNull Bitmap original, int targetWidth, int targetHeight) {
        // Need to scale the image, keeping the aspect ratio first
        int width = original.getWidth();
        int height = original.getHeight();

        float widthScale = (float) targetWidth / (float) width;
        float heightScale = (float) targetHeight / (float) height;
        float scaledWidth;
        float scaledHeight;

        int startX = 0;
        int startY = 0;

        if (widthScale > heightScale) {
            scaledWidth = targetWidth;
            scaledHeight = height * widthScale;
            startY = (int) (scaledHeight - targetHeight) / 2;
            if (startY < 0) {
                startY = 0;
            } else if (startY + targetHeight > scaledHeight) {
                startY = (int) (scaledHeight - targetHeight);
            }
        } else {
            scaledHeight = targetHeight;
            scaledWidth = width * heightScale;
        }

        Bitmap scaledBitmap = Bitmap.createScaledBitmap(original, (int) scaledWidth, (int) scaledHeight, true);
        Bitmap bitmap = Bitmap.createBitmap(scaledBitmap, startX, startY, targetWidth, targetHeight);
        scaledBitmap.recycle();
        return bitmap;
    }

    /**
     * Parameter object for #buildLayout and #optimizeTextSize.
     */
    private static class TextLayoutParams {
        private final CharSequence text;
        private final TextPaint textPaint;
        private final int lineWidth;
        private final float spacingMultiplier;
        private final Layout.Alignment align;

        /** Copy constructor with updated text */
        TextLayoutParams(TextLayoutParams other, CharSequence text) {
            this.text = text;
            this.textPaint = other.textPaint;
            this.lineWidth = other.lineWidth;
            this.spacingMultiplier = other.spacingMultiplier;
            this.align = other.align;
        }

        TextLayoutParams(CharSequence text, TextPaint textPaint, int lineWidth, float spacingMultiplier,
                Layout.Alignment align) {
            this.text = text;
            this.textPaint = textPaint;
            this.lineWidth = lineWidth;
            this.spacingMultiplier = spacingMultiplier;
            this.align = align;
        }

        private TextLayoutParams(CharSequence text, TextPaint textPaint, int lineWidth, float spacingMultiplier) {
            this(text, textPaint, lineWidth, spacingMultiplier, ALIGN_NORMAL);
        }
    }

    private SnippetImage() {
    }
}