package com.bluejamesbond.text; /* * Copyright 2015 Mathew Kurian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * DocumentView.java * @author Mathew Kurian * * From TextJustify-Android Library v2.0 * https://github.com/bluejamesbond/TextJustify-Android * * Please report any issues * https://github.com/bluejamesbond/TextJustify-Android/issues * * Date: 1/27/15 3:35 AM */ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.Typeface; import android.os.AsyncTask; import android.os.Build; import android.text.TextPaint; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.widget.ProgressBar; import android.widget.ScrollView; import com.bluejamesbond.text.style.TextAlignment; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; import javax.microedition.khronos.egl.EGLDisplay; @SuppressWarnings("unused") public class DocumentView extends ScrollView { public static final int PLAIN_TEXT = 0; public static final int FORMATTED_TEXT = 1; private static final ITween LINEAR_EASE_IN; private static final Object eglBitmapHeightLock; private static int eglBitmapHeight; protected ILayoutProgressListener layoutProgressListener; static { eglBitmapHeightLock = new Object(); eglBitmapHeight = -1; LINEAR_EASE_IN = new ITween() { @Override public float get(float t, float b, float c, float d) { return c * t / d + b; } }; } private IDocumentLayout layout; private TextPaint paint; private TextPaint cachePaint; private View viewportView; private ITween fadeInTween; private int fadeInDuration = 250; private int fadeInAnimationStepDelay = 35; private volatile MeasureTask measureTask; private volatile MeasureTaskState measureState; private int minimumHeight; private int orientation; private CacheConfig cacheConfig; private CacheBitmap cacheBitmapTop; private CacheBitmap cacheBitmapBottom; private boolean disallowInterceptTouch; public DocumentView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initDocumentView(context, attrs, PLAIN_TEXT); } public DocumentView(Context context, AttributeSet attrs) { super(context, attrs); initDocumentView(context, attrs, PLAIN_TEXT); } public DocumentView(Context context) { super(context); initDocumentView(context, null, PLAIN_TEXT); } public DocumentView(Context context, int type) { super(context); initDocumentView(context, null, type); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public DocumentView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initDocumentView(context, attrs, PLAIN_TEXT); } private static int getMaxTextureSize() { // Code from // http://stackoverflow.com/a/26823209/1100536 // Safe minimum default size final int GL_MAX_TEXTURE_SIZE = 2048; // Get EGL Display EGL10 egl = (EGL10) EGLContext.getEGL(); EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); // Initialise int[] version = new int[2]; egl.eglInitialize(display, version); // Query total number of configurations int[] totalConfigurations = new int[1]; egl.eglGetConfigs(display, null, 0, totalConfigurations); // Query actual list configurations EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]]; egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations); int[] textureSize = new int[1]; int maximumTextureSize = 0; // Iterate through all the configurations to located the maximum texture size for (int i = 0; i < totalConfigurations[0]; i++) { // Only need to check for width since opengl textures are always squared egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize); // Keep track of the maximum texture size if (maximumTextureSize < textureSize[0]) maximumTextureSize = textureSize[0]; } // Release egl.eglTerminate(display); // Return largest texture size found, or default return Math.max(maximumTextureSize, GL_MAX_TEXTURE_SIZE); } public boolean isDisallowInterceptTouch() { return disallowInterceptTouch; } public void setDisallowInterceptTouch(boolean disallowInterceptTouch) { this.disallowInterceptTouch = disallowInterceptTouch; } protected synchronized void drawLayout(Canvas canvas, int startY, int endY, boolean isCache) { if (isCache) { // clear canvas canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); } layout.draw(canvas, startY, endY); // onDraw border around if (getDocumentLayoutParams().isDebugging()) { IDocumentLayout.LayoutParams params = getDocumentLayoutParams(); int lastColor = paint.getColor(); float lastStrokeWidth = paint.getStrokeWidth(); Paint.Style lastStyle = paint.getStyle(); // border paint.setColor(Color.MAGENTA); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(4); float left = params.insetPaddingLeft; float top = params.insetPaddingTop >= startY && params.insetPaddingTop < endY ? params.insetPaddingTop : 0; float right = params.parentWidth - params.insetPaddingRight; float bottom = (bottom = layout.getMeasuredHeight() - params.insetPaddingBottom) >= startY && bottom < endY ? bottom - startY : canvas.getHeight(); canvas.drawRect(left, top, right, bottom, paint); paint.setStrokeWidth(lastStrokeWidth); paint.setColor(lastColor); paint.setStyle(lastStyle); } } public int getFadeInAnimationStepDelay() { return fadeInAnimationStepDelay; } public void setFadeInAnimationStepDelay(int delay) { fadeInAnimationStepDelay = delay; } public int getFadeInDuration() { return fadeInDuration; } public void setFadeInDuration(int duration) { fadeInDuration = duration; } public ITween getFadeInTween() { return fadeInTween; } public void setFadeInTween(ITween tween) { fadeInTween = tween; } private void initDocumentView(Context context, AttributeSet attrs, int type) { synchronized (eglBitmapHeightLock) { if (DocumentView.eglBitmapHeight == -1) { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); DocumentView.eglBitmapHeight = Math.min(Math.max(metrics.heightPixels, metrics.widthPixels) * 7 / 6, getMaxTextureSize()); } } disallowInterceptTouch = false; fadeInTween = LINEAR_EASE_IN; cacheConfig = CacheConfig.AUTO_QUALITY; paint = new TextPaint(); cachePaint = new TextPaint(); viewportView = new View(context); measureState = MeasureTaskState.START; // Initialize paint initPaint(paint); // Set default padding setPadding(0, 0, 0, 0); addView(viewportView); if (attrs != null && !isInEditMode()) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DocumentView); final int N = a.getIndexCount(); // find and onUpdate project layout layout = getDocumentLayoutInstance(a.getInt(R.styleable.DocumentView_documentView_textFormat, DocumentView.PLAIN_TEXT), paint); IDocumentLayout.LayoutParams layoutParams = layout.getLayoutParams(); for (int i = 0; i < N; ++i) { int attr = a.getIndex(i); if (attr == R.styleable.DocumentView_documentView_insetPadding) { Float pad = a.getDimension(attr, 0f); layoutParams.setInsetPaddingLeft(pad); layoutParams.setInsetPaddingBottom(pad); layoutParams.setInsetPaddingRight(pad); layoutParams.setInsetPaddingTop(pad); } else if (attr == R.styleable.DocumentView_documentView_insetPaddingLeft) { layoutParams.setInsetPaddingLeft(a.getDimension(attr, layoutParams.getInsetPaddingLeft())); } else if (attr == R.styleable.DocumentView_documentView_insetPaddingBottom) { layoutParams.setInsetPaddingBottom(a.getDimension(attr, layoutParams.getInsetPaddingBottom())); } else if (attr == R.styleable.DocumentView_documentView_insetPaddingRight) { layoutParams.setInsetPaddingRight(a.getDimension(attr, layoutParams.getInsetPaddingRight())); } else if (attr == R.styleable.DocumentView_documentView_insetPaddingTop) { layoutParams.setInsetPaddingTop(a.getDimension(attr, layoutParams.getInsetPaddingTop())); } else if (attr == R.styleable.DocumentView_documentView_offsetX) { layoutParams.setOffsetX(a.getDimension(attr, layoutParams.getOffsetX())); } else if (attr == R.styleable.DocumentView_documentView_offsetY) { layoutParams.setOffsetY(a.getDimension(attr, layoutParams.getOffsetY())); } else if (attr == R.styleable.DocumentView_documentView_hyphen) { layoutParams.setHyphen(a.getString(attr)); } else if (attr == R.styleable.DocumentView_documentView_maxLines) { layoutParams.setMaxLines(a.getInt(attr, layoutParams.getMaxLines())); } else if (attr == R.styleable.DocumentView_documentView_lineHeightMultiplier) { layoutParams.setLineHeightMultiplier(a.getFloat(attr, layoutParams.getLineHeightMultiplier())); } else if (attr == R.styleable.DocumentView_documentView_textAlignment) { layoutParams.setTextAlignment(TextAlignment.getById(a.getInt(attr, layoutParams.getTextAlignment().getId()))); } else if (attr == R.styleable.DocumentView_documentView_reverse) { layoutParams.setReverse(a.getBoolean(attr, layoutParams.isReverse())); } else if (attr == R.styleable.DocumentView_documentView_wordSpacingMultiplier) { layoutParams.setWordSpacingMultiplier(a.getFloat(attr, layoutParams.getWordSpacingMultiplier())); } else if (attr == R.styleable.DocumentView_documentView_textColor) { layoutParams.setTextColor(a.getColor(attr, layoutParams.getTextColor())); } else if (attr == R.styleable.DocumentView_documentView_textSize) { layoutParams.setRawTextSize(a.getDimension(attr, layoutParams.getTextSize())); } else if (attr == R.styleable.DocumentView_documentView_textStyle) { int style = a.getInt(attr, 0); layoutParams.setTextFakeBold((style & 1) > 0); layoutParams.setTextUnderline(((style >> 1) & 1) > 0); layoutParams.setTextStrikeThru(((style >> 2) & 1) > 0); } else if (attr == R.styleable.DocumentView_documentView_textTypefacePath) { layoutParams.setTextTypeface(Typeface.createFromAsset(getResources().getAssets(), a.getString(attr))); } else if (attr == R.styleable.DocumentView_documentView_antialias) { layoutParams.setAntialias(a.getBoolean(attr, layoutParams.isAntiAlias())); } else if (attr == R.styleable.DocumentView_documentView_textSubPixel) { layoutParams.setTextSubPixel(a.getBoolean(attr, layoutParams.isTextSubPixel())); } else if (attr == R.styleable.DocumentView_documentView_text) { layout.setText(a.getString(attr)); } else if (attr == R.styleable.DocumentView_documentView_cacheConfig) { setCacheConfig(CacheConfig.getById(a.getInt(attr, CacheConfig.AUTO_QUALITY.getId()))); } else if (attr == R.styleable.DocumentView_documentView_progressBar) { setProgressBar(a.getResourceId(R.styleable.DocumentView_documentView_progressBar, 0)); } else if (attr == R.styleable.DocumentView_documentView_fadeInAnimationStepDelay) { setFadeInAnimationStepDelay(a.getInteger(attr, getFadeInAnimationStepDelay())); } else if (attr == R.styleable.DocumentView_documentView_fadeInDuration) { setFadeInDuration(a.getInteger(attr, getFadeInDuration())); } else if (attr == R.styleable.DocumentView_documentView_disallowInterceptTouch) { setDisallowInterceptTouch(a.getBoolean(attr, isDisallowInterceptTouch())); } } a.recycle(); } else { this.layout = getDocumentLayoutInstance(type, paint); } } protected void initPaint(Paint paint) { paint.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL)); paint.setTextSize(34); paint.setAntiAlias(true); } @Override protected void onAttachedToWindow() { orientation = getResources().getConfiguration().orientation; super.onAttachedToWindow(); } public IDocumentLayout getDocumentLayoutInstance(int type, TextPaint paint) { switch (type) { case FORMATTED_TEXT: return new SpannableDocumentLayout(getContext(), paint) { @Override public void onLayoutParamsChange() { invalidateCache(); postInvalidate(); } @Override public void onTextChange() { invalidateCache(); requestLayout(); } }; default: case PLAIN_TEXT: return new StringDocumentLayout(getContext(), paint) { @Override public void onLayoutParamsChange() { invalidateCache(); postInvalidate(); } @Override public void onTextChange() { invalidateCache(); requestLayout(); } }; } } public void invalidateCache() { if (cacheBitmapTop != null) { cacheBitmapTop.setStart(-1); } if (cacheBitmapBottom != null) { cacheBitmapBottom.setStart(-1); } } public View getViewportView(){ return viewportView; } public void setProgressBar(final int progressBarId) { setOnLayoutProgressListener(new DocumentView.ILayoutProgressListener() { private ProgressBar progressBar; @Override public void onCancelled() { progressBar.setProgress(progressBar.getMax()); progressBar = null; } @Override public void onFinish() { progressBar.setProgress(progressBar.getMax()); progressBar = null; } @Override public void onStart() { progressBar = (ProgressBar) ((Activity) getContext()).getWindow().getDecorView().findViewById(progressBarId); progressBar.setProgress(0); } @Override public void onProgressUpdate(float progress) { progressBar.setProgress((int) (progress * (float) progressBar.getMax())); } }); } public void setOnLayoutProgressListener(ILayoutProgressListener listener) { layoutProgressListener = listener; } public CharSequence getText() { return this.layout.getText(); } public void setText(CharSequence text) { this.layout.setText(text); requestLayout(); } public StringDocumentLayout.LayoutParams getDocumentLayoutParams() { return this.layout.getLayoutParams(); } public IDocumentLayout getLayout() { return this.layout; } public CacheConfig getCacheConfig() { return cacheConfig; } public void setCacheConfig(CacheConfig quality) { cacheConfig = quality; } @SuppressWarnings("DrawAllocation") @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { final int width = MeasureSpec.getSize(widthMeasureSpec); switch (measureState) { case FINISH_AWAIT: break; case AWAIT: break; case FINISH: viewportView.setMinimumWidth(width); viewportView.setMinimumHeight(layout.getMeasuredHeight()); measureState = MeasureTaskState.FINISH_AWAIT; if(cacheConfig != CacheConfig.NO_CACHE){ allocateResources(); } break; case START: if (measureTask != null) { measureTask.cancel(true); measureTask = null; } measureTask = new MeasureTask(width); measureTask.execute(); measureState = MeasureTaskState.AWAIT; break; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public boolean onTouchEvent(MotionEvent ev) { requestDisallowInterceptTouchEvent(disallowInterceptTouch); return super.onTouchEvent(ev); } @Override public void requestLayout() { measureState = MeasureTaskState.START; super.requestLayout(); } @Override protected void onDetachedFromWindow() { freeResources(); super.onDetachedFromWindow(); } public void setProgressBar(final ProgressBar progressBar) { setOnLayoutProgressListener(new DocumentView.ILayoutProgressListener() { @Override public void onCancelled() { progressBar.setProgress(progressBar.getMax()); } @Override public void onFinish() { progressBar.setProgress(progressBar.getMax()); } @Override public void onStart() { progressBar.setProgress(0); } @Override public void onProgressUpdate(float progress) { progressBar.setProgress((int) (progress * (float) progressBar.getMax())); } }); } @Override protected void onConfigurationChanged(Configuration newConfig) { if (orientation != newConfig.orientation) { orientation = newConfig.orientation; freeResources(); } super.onConfigurationChanged(newConfig); } @SuppressLint("DrawAllocation") @Override protected final void onDraw(Canvas canvas) { super.onDraw(canvas); // Android studio render if (isInEditMode()) { return; } boolean cacheEnabled = cacheConfig != CacheConfig.NO_CACHE && layout.getMeasuredHeight() > getHeight(); if (cacheEnabled) { allocateResources(); final int scrollTop = getScrollY(); final int scrollBottom = scrollTop + getHeight(); final CacheBitmap top = scrollTop % (eglBitmapHeight * 2) < eglBitmapHeight ? cacheBitmapTop : cacheBitmapBottom; final CacheBitmap bottom = scrollBottom % (eglBitmapHeight * 2) >= eglBitmapHeight ? cacheBitmapBottom : cacheBitmapTop; final int startTop = scrollTop - (scrollTop % (eglBitmapHeight * 2)) + (top == cacheBitmapTop ? 0 : eglBitmapHeight); boolean postInvalidate; if (top == bottom) { if (startTop != top.getStart()) { top.setStart(startTop); top.drawInBackground(new Runnable() { @Override public void run() { drawLayout(new Canvas(bottom.getBitmap()), startTop, startTop + eglBitmapHeight, true); } }); } postInvalidate = drawCacheToView(canvas, cachePaint, top, startTop); } else { final int startBottom = startTop + eglBitmapHeight; if (startTop != top.getStart()) { top.setStart(startTop); top.drawInBackground(new Runnable() { @Override public void run() { drawLayout(new Canvas(top.getBitmap()), startTop, startTop + eglBitmapHeight, true); } }); } if (startBottom != bottom.getStart()) { bottom.setStart(startBottom); bottom.drawInBackground(new Runnable() { @Override public void run() { drawLayout(new Canvas(bottom.getBitmap()), startBottom, startBottom + eglBitmapHeight, true); } }); } postInvalidate = drawCacheToView(canvas, cachePaint, top, startTop) | drawCacheToView(canvas, cachePaint, bottom, startBottom); } if (postInvalidate) { postInvalidateDelayed(fadeInAnimationStepDelay); } } else { drawLayout(canvas, 0, layout.getMeasuredHeight(), false); } } @Override public void setMinimumHeight(int minHeight) { minimumHeight = minHeight; viewportView.setMinimumHeight(minimumHeight); } public void allocateResources() { if (cacheBitmapTop == null) { cacheBitmapTop = new CacheBitmap(getWidth(), eglBitmapHeight, cacheConfig.getConfig()); } if (cacheBitmapBottom == null) { cacheBitmapBottom = new CacheBitmap(getWidth(), eglBitmapHeight, cacheConfig.getConfig()); } } protected boolean drawCacheToView(Canvas canvas, Paint paint, CacheBitmap cache, int y) { // onDraw only if cache is ready if (cache.isReady()) { int lastAlpha = paint.getAlpha(); paint.setAlpha(cache.getAlpha()); canvas.drawBitmap(cache.getBitmap(), 0, y, paint); paint.setAlpha(lastAlpha); return cache.getAlpha() < 255; // return true to invoke postInvalidateDelayed() } return false; } protected void freeResources() { viewportView.setMinimumHeight(minimumHeight); if (measureTask != null) { measureTask.cancel(true); measureTask = null; measureState = MeasureTaskState.START; } destroyCache(); } public void destroyCache() { if (cacheBitmapTop != null) { cacheBitmapTop.recycle(); cacheBitmapTop = null; } if (cacheBitmapBottom != null) { cacheBitmapBottom.recycle(); cacheBitmapBottom = null; } } public static enum CacheConfig { NO_CACHE(null, 0), AUTO_QUALITY(Config.ARGB_4444, 1), LOW_QUALITY(Config.RGB_565, 2), HIGH_QUALITY(Config.ARGB_8888, 3), GRAYSCALE(Config.ALPHA_8, 4); private final Config mConfig; private final int mId; private CacheConfig(Config config, int id) { mConfig = config; mId = id; } public static CacheConfig getById(int id) { switch (id) { default: case 0: return NO_CACHE; case 1: return AUTO_QUALITY; case 2: return LOW_QUALITY; case 3: return HIGH_QUALITY; case 4: return GRAYSCALE; } } private Config getConfig() { return mConfig; } public int getId() { return mId; } } enum MeasureTaskState { AWAIT, FINISH, START, FINISH_AWAIT } public static interface ILayoutProgressListener { public void onCancelled(); public void onFinish(); public void onStart(); public void onProgressUpdate(float progress); } public static interface ITween { public float get(float t, float b, float c, float d); } public class MeasureTask extends AsyncTask<Void, Float, Boolean> { private IDocumentLayout.IProgress<Float> progress; private IDocumentLayout.ICancel<Boolean> cancelled; public MeasureTask(float parentWidth) { layout.getLayoutParams().setParentWidth(parentWidth); progress = new IDocumentLayout.IProgress<Float>() { @Override public void onUpdate(Float progress) { if (layoutProgressListener != null) { layoutProgressListener.onProgressUpdate(progress); } } }; cancelled = new IDocumentLayout.ICancel<Boolean>() { @Override public Boolean isCancelled() { return MeasureTask.this.isCancelled(); } }; } @Override protected Boolean doInBackground(Void... params) { try { return layout.measure(progress, cancelled); } catch (Exception e) { e.printStackTrace(); return false; } } @Override protected void onPreExecute() { if (layoutProgressListener != null) { layoutProgressListener.onStart(); } } @Override protected void onPostExecute(Boolean done) { if (!done || isCancelled()) { if (layoutProgressListener != null) { layoutProgressListener.onCancelled(); } return; } measureTask = null; measureState = MeasureTaskState.FINISH; DocumentView.super.requestLayout(); if (layoutProgressListener != null) { layoutProgressListener.onFinish(); } } @Override protected void onCancelled() { super.onCancelled(); if (layoutProgressListener != null) { layoutProgressListener.onCancelled(); } } } private class CacheBitmap { private long drawFadeInStartTime; private Bitmap bitmap; private int start; private volatile boolean drawCompleted; private volatile CacheDrawTask drawTask; private volatile int alpha; public CacheBitmap(int w, int h, Bitmap.Config config) { bitmap = Bitmap.createBitmap(w, h, config); start = -1; drawCompleted = false; } public int getAlpha() { return (int) Math.min(fadeInTween.get(System.currentTimeMillis() - drawFadeInStartTime, 0, 255f, fadeInDuration), 255f); } public void drawInBackground(Runnable runnable) { if (drawTask != null) { drawTask.cancel(true); drawTask = null; } drawCompleted = false; alpha = 0; drawTask = new CacheDrawTask(runnable); drawTask.execute(); } public Bitmap getBitmap() { return bitmap; } public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; } public int getStart() { return start; } public void setStart(int start) { this.start = start; } public boolean isReady() { return drawCompleted; } public void recycle() { if (drawTask != null) { drawTask.cancel(true); drawTask = null; drawCompleted = false; } if (bitmap != null) { bitmap.recycle(); bitmap = null; } } public class CacheDrawTask extends AsyncTask<Void, Void, Void> { private Runnable drawRunnable; public CacheDrawTask(Runnable runnable) { drawRunnable = runnable; } @Override protected void onPreExecute() { super.onPreExecute(); } @Override protected Void doInBackground(Void... params) { drawRunnable.run(); return null; } @Override protected void onPostExecute(Void aVoid) { drawFadeInStartTime = System.currentTimeMillis(); drawCompleted = true; invalidate(); } } } }