package com.quran.labs.androidquran.widgets; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.FontMetrics; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.SparseArray; import android.widget.ImageView; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.common.AyahBounds; import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.ui.helpers.HighlightType; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; public class HighlightingImageView extends ImageView { private static final SparseArray<Paint> SPARSE_PAINT_ARRAY = new SparseArray<>(); private static int overlayTextColor = -1; private static int headerFooterSize; private static int headerFooterFontSize; private static int scrollableHeaderFooterSize; private static int scrollableHeaderFooterFontSize; // Sorted map so we use highest priority highlighting when iterating private SortedMap<HighlightType, Set<String>> currentHighlights = new TreeMap<>(); private boolean isNightMode; private boolean isColorFilterOn; private int nightModeTextBrightness = Constants.DEFAULT_NIGHT_MODE_TEXT_BRIGHTNESS; // cached objects for onDraw private final RectF scaledRect = new RectF(); private final Set<String> alreadyHighlighted = new HashSet<>(); // Params for drawing text private int fontSize; private OverlayParams overlayParams = null; private RectF pageBounds = null; private boolean didDraw = false; private Map<String, List<AyahBounds>> coordinatesData; public HighlightingImageView(Context context) { this(context, null); } public HighlightingImageView(Context context, AttributeSet attrs) { super(context, attrs); if (overlayTextColor == -1) { final Resources res = context.getResources(); overlayTextColor = ContextCompat.getColor(context, R.color.overlay_text_color); headerFooterSize = res.getDimensionPixelSize(R.dimen.page_overlay_size); scrollableHeaderFooterSize = res.getDimensionPixelSize(R.dimen.page_overlay_size_scrollable); headerFooterFontSize = res.getDimensionPixelSize(R.dimen.page_overlay_font_size); scrollableHeaderFooterFontSize = res.getDimensionPixelSize(R.dimen.page_overlay_font_size_scrollable); } } public void setIsScrollable(boolean scrollable) { int topBottom = scrollable ? scrollableHeaderFooterSize : headerFooterSize; setPadding(getPaddingLeft(), topBottom, getPaddingRight(), topBottom); fontSize = scrollable ? scrollableHeaderFooterFontSize : headerFooterFontSize; } public void unHighlight(int sura, int ayah, HighlightType type) { Set<String> highlights = currentHighlights.get(type); if (highlights != null && highlights.remove(sura + ":" + ayah)) { invalidate(); } } public void highlightAyat(Set<String> ayahKeys, HighlightType type) { Set<String> highlights = currentHighlights.get(type); if (highlights == null) { highlights = new HashSet<>(); currentHighlights.put(type, highlights); } highlights.addAll(ayahKeys); } public void unHighlight(HighlightType type) { if (!currentHighlights.isEmpty()) { currentHighlights.remove(type); invalidate(); } } public void setCoordinateData(Map<String, List<AyahBounds>> data) { coordinatesData = data; } public void setNightMode(boolean isNightMode, int textBrightness) { this.isNightMode = isNightMode; if (isNightMode) { nightModeTextBrightness = textBrightness; // we need a new color filter now isColorFilterOn = false; } adjustNightMode(); } public void highlightAyah(int sura, int ayah, HighlightType type) { Set<String> highlights = currentHighlights.get(type); if (highlights == null) { highlights = new HashSet<>(); currentHighlights.put(type, highlights); } else if (!type.isMultipleHighlightsAllowed()) { // If multiple highlighting not allowed (e.g. audio) // clear all others of this type first highlights.clear(); } highlights.add(sura + ":" + ayah); } @Override public void setImageDrawable(Drawable bitmap) { // clear the color filter before setting the image clearColorFilter(); // this allows the filter to be enabled again if needed isColorFilterOn = false; super.setImageDrawable(bitmap); if (bitmap != null) { adjustNightMode(); } } public void adjustNightMode() { if (isNightMode && !isColorFilterOn) { float[] matrix = { -1, 0, 0, 0, nightModeTextBrightness, 0, -1, 0, 0, nightModeTextBrightness, 0, 0, -1, 0, nightModeTextBrightness, 0, 0, 0, 1, 0 }; setColorFilter(new ColorMatrixColorFilter(matrix)); isColorFilterOn = true; } else if (!isNightMode) { clearColorFilter(); isColorFilterOn = false; } invalidate(); } private static class OverlayParams { boolean init = false; Paint paint = null; float offsetX; float topBaseline; float bottomBaseline; String suraText = null; String juzText = null; String pageText = null; String rub3Text = null; } public void setOverlayText(String suraText, String juzText, String pageText, String rub3Text) { // Calculate page bounding rect from ayahinfo db if (pageBounds == null) { return; } overlayParams = new OverlayParams(); overlayParams.suraText = suraText; overlayParams.juzText = juzText; overlayParams.pageText = pageText; overlayParams.rub3Text = rub3Text; overlayParams.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); overlayParams.paint.setTextSize(fontSize); if (!didDraw) { invalidate(); } } public void setPageBounds(RectF rect) { pageBounds = rect; } private boolean initOverlayParams(Matrix matrix) { if (overlayParams == null || pageBounds == null) { return false; } // Overlay params previously initiated; skip if (overlayParams.init) { return true; } int overlayColor = overlayTextColor; if (isNightMode) { overlayColor = Color.rgb(nightModeTextBrightness, nightModeTextBrightness, nightModeTextBrightness); } overlayParams.paint.setColor(overlayColor); // Use font metrics to calculate the maximum possible height of the text FontMetrics fm = overlayParams.paint.getFontMetrics(); final RectF mappedRect = new RectF(); matrix.mapRect(mappedRect, pageBounds); // Calculate where the text's baseline should be // (for top text and bottom text) // (p.s. parts of the glyphs will be below the baseline such as a // 'y' or 'ي') overlayParams.topBaseline = -fm.top; overlayParams.bottomBaseline = getHeight() - fm.bottom; // Calculate the horizontal margins off the edge of screen overlayParams.offsetX = Math.min( mappedRect.left, getWidth() - mappedRect.right); overlayParams.init = true; return true; } private void overlayText(Canvas canvas, Matrix matrix) { if (overlayParams == null || !initOverlayParams(matrix)) { return; } overlayParams.paint.setTextAlign(Align.LEFT); canvas.drawText(overlayParams.suraText, overlayParams.offsetX, overlayParams.topBaseline, overlayParams.paint); overlayParams.paint.setTextAlign(Align.CENTER); canvas.drawText(overlayParams.pageText, getWidth() / 2.0f, overlayParams.bottomBaseline, overlayParams.paint); // Merge the current rub3 text with the juz' text overlayParams.paint.setTextAlign(Align.RIGHT); canvas.drawText(overlayParams.juzText + overlayParams.rub3Text, getWidth() - overlayParams.offsetX, overlayParams.topBaseline, overlayParams.paint); didDraw = true; } private Paint getPaintForHighlightType(HighlightType type) { int color = type.getColor(getContext()); Paint paint = SPARSE_PAINT_ARRAY.get(color); if (paint == null) { paint = new Paint(); paint.setColor(color); SPARSE_PAINT_ARRAY.put(color, paint); } return paint; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (overlayParams != null) { overlayParams.init = false; } } @Override protected void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); final Drawable d = getDrawable(); if (d == null) { // no image, forget it. return; } final Matrix matrix = getImageMatrix(); // Draw overlay text didDraw = false; if (overlayParams != null) { overlayText(canvas, matrix); } // Draw each ayah highlight if (coordinatesData != null && !currentHighlights.isEmpty()) { alreadyHighlighted.clear(); for (Map.Entry<HighlightType, Set<String>> entry : currentHighlights.entrySet()) { Paint paint = getPaintForHighlightType(entry.getKey()); for (String ayah : entry.getValue()) { if (alreadyHighlighted.contains(ayah)) continue; List<AyahBounds> rangesToDraw = coordinatesData.get(ayah); if (rangesToDraw != null && !rangesToDraw.isEmpty()) { for (AyahBounds b : rangesToDraw) { matrix.mapRect(scaledRect, b.getBounds()); scaledRect.offset(0, getPaddingTop()); canvas.drawRect(scaledRect, paint); } alreadyHighlighted.add(ayah); } } } } } }