package ar.rulosoft.mimanganu.componentes.readers.continuos; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.support.annotation.NonNull; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import ar.rulosoft.mimanganu.componentes.readers.Reader; import rapid.decoder.BitmapDecoder; /** * Created by Raul on 22/10/2015. */ public abstract class ReaderContinuous extends Reader implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener{ protected int currentPage = 0, lastBestVisible = 0; protected float lastPageBestPercent = 0f; protected int mTextureMax = 1024; protected boolean animatingSeek = false; protected boolean stopAnimationsOnTouch = false, stopAnimationOnVerticalOver = false, stopAnimationOnHorizontalOver = false; protected boolean iniVisibility, endVisibility; protected boolean pagesLoaded = false, viewReady = false, layoutReady = false; protected float xScroll = 0, yScroll = 0; protected ArrayList<Page> pages; protected ScaleGestureDetector mScaleDetector; protected GestureDetector mGestureDetector; protected Rect screen; float mScaleFactor = 1.f; Matrix m = new Matrix(); int screenHeight, screenWidth; int screenHeightSS, screenWidthSS; // Sub scaled Handler mHandler; ArrayList<Page.Segment> toDraw = new ArrayList<>(); boolean drawing = false, preparing = false, waiting = false; float ppi; public ReaderContinuous(Context context) { super(context); init(context); } protected abstract void absoluteScroll(float x, float y); protected abstract void relativeScroll(double distanceX, double distanceY); protected abstract void calculateParticularScale(); protected abstract void calculateParticularScale(Page page); protected abstract void calculateVisibilities(); public abstract void goToPage(int aPage); protected abstract Page getNewPage(); public abstract void reset(); public abstract void seekPage(int index); public abstract void postLayout(); public abstract void reloadImage(int idx); @Override protected int transformPage(int page) { return page + 1; } public void freeMemory() { if (pages != null) for (Page p : pages) { p.freeMemory(); } } public void freePage(int idx) { getPage(idx).freeMemory(); } @Override public int getCurrentPage() { return currentPage + 1; } public String getPath(int idx) { return getPage(idx).getPath(); } public Page getPage(int page) { if (page == 0) { // only happens when one spams // the re-download button on the last page return pages.get(page); } else { return pages.get(page - 1); } } private void init(Context context) { setWillNotDraw(false); mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); mGestureDetector = new GestureDetector(getContext(), this); mHandler = new Handler(); ppi = context.getResources().getDisplayMetrics().density * 160.0f; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { screenHeight = Math.abs(bottom - top); screenWidth = Math.abs(right - left); screenWidthSS = screenWidth; screenHeightSS = screenHeight; if (pages != null) { calculateParticularScale(); calculateVisibilities(); layoutReady = true; generateDrawPool(); } postLayout(); super.onLayout(changed, left, top, right, bottom); } protected void generateDrawPool() { if (!preparing) { preparing = true; new Thread(new Runnable() { @Override public void run() { ArrayList<Page.Segment> _segments = new ArrayList<Page.Segment>(); if (viewReady) { lastBestVisible = -1; iniVisibility = false; endVisibility = false; lastPageBestPercent = 0f; if (pages != null) { int currentPageIdx = currentPage - 1; boolean tested = false; while (!tested) { tested = true; try { for (int i = currentPageIdx - 1; i >= 0; i--) {//pre Page page = pages.get(i); if (page.isVisible()) { iniVisibility = true; _segments.addAll(page.getVisibleSegments()); if (page.getVisiblePercent() >= lastPageBestPercent) { lastPageBestPercent = page.getVisiblePercent(); lastBestVisible = pages.indexOf(page); } } else { break; } } {//actual if (currentPageIdx >= 0 && currentPageIdx < pages.size()) { Page page = pages.get(currentPageIdx); if (page.isVisible()) { iniVisibility = true; _segments.addAll(page.getVisibleSegments()); if (page.getVisiblePercent() >= lastPageBestPercent) { lastPageBestPercent = page.getVisiblePercent(); lastBestVisible = pages.indexOf(page); } } } } for (int i = currentPageIdx + 1; i < pages.size(); i++) {//next Page page = pages.get(i); if (page.isVisible()) { iniVisibility = true; _segments.addAll(page.getVisibleSegments()); if (page.getVisiblePercent() >= lastPageBestPercent) { lastPageBestPercent = page.getVisiblePercent(); lastBestVisible = pages.indexOf(page); } } else { break; } } if (_segments.size() == 0) {//if none in range find... for (int i = 0; i < pages.size(); i++) { Page page = pages.get(i); if (page.isVisible()) { iniVisibility = true; _segments.addAll(page.getVisibleSegments()); if (page.getVisiblePercent() >= lastPageBestPercent) { lastPageBestPercent = page.getVisiblePercent(); lastBestVisible = pages.indexOf(page); } } else { if (iniVisibility) endVisibility = true; } if (iniVisibility && endVisibility) break; } } } catch (Exception e) { tested = false; }//catch errors caused for array concurrent modify } if (currentPage != lastBestVisible) { setPage(lastBestVisible); } } } else if (pagesLoaded) { //TODO if (mViewReadyListener != null) viewReady = true; preparing = false; seekPage(transformPage(currentPage)); } toDraw = _segments; drawing = true; mHandler.post(new Runnable() { @Override public void run() { invalidate(); } }); } }).start(); } } @Override public void invalidate() { generateDrawPool(); super.invalidate(); } @Override protected void onDraw(Canvas canvas) { waiting = false; if (drawing) { if (toDraw.size() > 0) for (Page.Segment s : toDraw) { s.draw(canvas); } else waiting = true; preparing = false; drawing = false; if (waiting) { waiting = false; generateDrawPool(); } } else { if (preparing) waiting = true; else generateDrawPool(); } } public void setPaths(List<String> paths) { pages = new ArrayList<>(); for (int i = 0; i < paths.size(); i++) { pages.add(initValues(paths.get(i))); } if (layoutReady) { calculateParticularScale(); calculateVisibilities(); generateDrawPool(); } } protected Page initValues(String path) { Page dimension = getNewPage(); dimension.path = path; File f = new File(path); if (f.exists()) { try { dimension.path = path; InputStream inputStream = new FileInputStream(path); BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); bitmapOptions.inJustDecodeBounds = true; BitmapFactory.decodeStream(inputStream, null, bitmapOptions); dimension.original_width = bitmapOptions.outWidth; dimension.original_height = bitmapOptions.outHeight; inputStream.close(); } catch (IOException ignored) { } dimension.initValues(); } else { try { dimension.error = true; InputStream inputStream = getBitmapFromAsset("broke.png"); BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); bitmapOptions.inJustDecodeBounds = true; BitmapFactory.decodeStream(inputStream, null, bitmapOptions); dimension.original_width = bitmapOptions.outWidth; dimension.original_height = bitmapOptions.outHeight; inputStream.close(); } catch (IOException ignored) { } dimension.initValues(); } return dimension; } protected void setPage(int page) { if (readerListener != null) readerListener.onPageChanged(page); currentPage = page; generateDrawPool(); } public boolean isLastPageVisible() { return pages != null && !pages.isEmpty() && pages.get(pages.size() - 1).isVisible(); } public void setScrollSensitive(float mScrollSensitive) { this.mScrollSensitive = mScrollSensitive; } public void setMaxTexture(int mTextureMax) { if (mTextureMax > 0) this.mTextureMax = mTextureMax; } @Override public boolean onTouchEvent(@NonNull MotionEvent ev) { stopAnimationsOnTouch = true; mScaleDetector.onTouchEvent(ev); if (!mScaleDetector.isInProgress()) mGestureDetector.onTouchEvent(ev); generateDrawPool(); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { distanceX = (distanceX * mScrollSensitive / mScaleFactor); distanceY = (distanceY * mScrollSensitive / mScaleFactor); relativeScroll(distanceX, distanceY); generateDrawPool(); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, final float velocityX, final float velocityY) { stopAnimationsOnTouch = false; stopAnimationOnHorizontalOver = false; stopAnimationOnVerticalOver = false; mHandler.post(new Runnable() { final int fps = 50; final float deceleration_rate = 0.90f; final int timeLapse = 1000 / fps; final float min_velocity = 500; float velocity_Y = velocityY * mScrollSensitive; float velocity_X = velocityX * mScrollSensitive; @Override public void run() { relativeScroll(-velocity_X / fps, -(velocity_Y / fps)); velocity_Y = velocity_Y * deceleration_rate; velocity_X = velocity_X * deceleration_rate; if (stopAnimationOnHorizontalOver) { velocity_X = 0; } if (stopAnimationOnVerticalOver) { velocity_Y = 0; } if ((Math.abs(velocity_Y) > min_velocity || Math.abs(velocity_X) > min_velocity) && !stopAnimationsOnTouch) { mHandler.postDelayed(this, timeLapse); } generateDrawPool(); } }); return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onDown(MotionEvent e) { return false; } @Override public boolean onDoubleTap(final MotionEvent e) { final float ini = mScaleFactor, end; if (mScaleFactor < 1.8) { end = 2f; } else if (mScaleFactor < 2.8) { end = 3f; } else { end = 1f; } ValueAnimator va = ValueAnimator.ofFloat(ini, end); va.setDuration(300); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { float nScale; float final_x = ((xScroll + e.getX() / ini)) - (screenWidth / 2) + (screenWidth * end - screenWidth) / (end * 2) - xScroll; float final_y = ((yScroll + e.getY() / ini)) - (screenHeight / 2) + (screenHeight * end - screenHeight) / (end * 2) - yScroll; float initial_x_scroll = xScroll; float initial_y_scroll = yScroll; float nPx, nPy, aP; @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { nScale = (float) valueAnimator.getAnimatedValue(); aP = valueAnimator.getAnimatedFraction(); nPx = initial_x_scroll + (final_x * aP); nPy = initial_y_scroll + (final_y * aP); mScaleFactor = nScale; absoluteScroll(nPx, nPy); generateDrawPool(); } }); va.start(); return false; } @Override protected Parcelable onSaveInstanceState() { Bundle b = new Bundle(); b.putParcelable("state", super.onSaveInstanceState()); return b; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { super.onRestoreInstanceState(((Bundle) state).getParcelable("state")); } else { super.onRestoreInstanceState(state); } } /* * Starting from 0 */ public abstract float getPagePosition(int page); private InputStream getBitmapFromAsset(String strName) { AssetManager assetManager = getContext().getAssets(); InputStream iStr = null; try { iStr = assetManager.open(strName); } catch (IOException e) { e.printStackTrace(); } return iStr; } public enum ImagesStates {NULL, RECYCLED, ERROR, LOADING, LOADED} public abstract class Page { String path; float original_width; float original_height; float init_visibility; float end_visibility; float unification_scale; float scaled_height; float scaled_width; boolean initialized = false; Segment[] segments; int pw, ph; int vp, hp, tp; //vertical and horizontal parts count and total boolean error = false; boolean lastVisibleState = false; public abstract boolean isVisible(); public abstract Segment getNewSegment(); public abstract float getVisiblePercent(); public void initValues() { vp = (int) (original_height / mTextureMax) + 1; hp = (int) (original_width / mTextureMax) + 1; pw = (int) (original_width / hp); ph = (int) (original_height / vp); tp = vp * hp; segments = new Segment[tp]; for (int i = 0; i < vp; i++) { for (int j = 0; j < hp; j++) { int idx = (i * hp) + j; segments[idx] = getNewSegment(); segments[idx].dy = i * ph; segments[idx].dx = j * pw; } } initialized = true; } public String getPath() { return path; } public void freeMemory() { if (segments != null) for (Segment s : segments) { s.freeMemory(); } } public ArrayList<Segment> getVisibleSegments() { ArrayList<Segment> _segments = new ArrayList<>(); if (segments != null) for (Segment s : segments) { if (s.isVisible()) { _segments.add(s); } } return _segments; } public void showOnLoad(final Segment segment) { if (segment.isVisible()) { mHandler.post(new Runnable() { @Override public void run() { ValueAnimator va = ValueAnimator.ofInt(0, 255); va.setDuration(300); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { segment.alpha = (int) valueAnimator.getAnimatedValue(); generateDrawPool(); } }); va.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { segment.alpha = 255; generateDrawPool(); } @Override public void onAnimationCancel(Animator animator) { segment.alpha = 255; generateDrawPool(); } @Override public void onAnimationRepeat(Animator animator) { } }); va.start(); } }); } else { segment.alpha = 255; generateDrawPool(); } } public abstract class Segment { boolean visible = false; Bitmap segment; ImagesStates state; int dx, dy; Paint mPaint; int alpha; public Segment() { mPaint = new Paint(); mPaint.setAlpha(255); mPaint.setFilterBitmap(true); alpha = 255; state = ImagesStates.NULL; } public boolean isVisible() { return visible; } public abstract boolean checkVisibility(); public abstract void draw(Canvas canvas); public void visibilityChanged() { if (!animatingSeek) { visible = !visible; if (visible) { loadBitmap(); } else { freeMemory(); } } } public void freeMemory() { if (segment != null) { visible = false; state = ImagesStates.RECYCLED; segment.recycle(); segment = null; alpha = 0; } state = ImagesStates.NULL; } public void loadBitmap() { if (!animatingSeek) new Thread(new Runnable() { @Override public void run() { try { if (state == ImagesStates.NULL) { state = ImagesStates.LOADING; alpha = 0; BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; if (!error) { if (tp == 1) { segment = BitmapDecoder.from(path).useBuiltInDecoder(false).config(Bitmap.Config.RGB_565).decode(); if (segments == null) { segment = BitmapDecoder.from(path).useBuiltInDecoder(true).config(Bitmap.Config.RGB_565).decode(); } } else { try { int right = dx + pw + 2, bottom = dy + ph + 2; if (right > original_width) right = (int) original_width; if (bottom > original_height) bottom = (int) original_height; segment = BitmapDecoder.from(path).region(dx, dy, right, bottom).useBuiltInDecoder(false).config(Bitmap.Config.RGB_565).decode(); if (segment == null) { segment = BitmapDecoder.from(path).region(dx, dy, right, bottom).useBuiltInDecoder(true).config(Bitmap.Config.RGB_565).decode(); } } catch (Exception e) { e.printStackTrace(); } } } else { InputStream inputStream = getBitmapFromAsset("broke.png"); if (tp == 1) { segment = BitmapFactory.decodeStream(inputStream, null, options); } else { try { int right = dx + pw + 2, bottom = dy + ph + 2; if (right > original_width) right = (int) original_width; if (bottom > original_height) bottom = (int) original_height; segment = BitmapDecoder.from(inputStream).region(dx, dy, right, bottom).useBuiltInDecoder(false).config(Bitmap.Config.RGB_565).decode(); } catch (Exception e) { e.printStackTrace(); } } inputStream.close(); } if (segment != null) { state = ImagesStates.LOADED; showOnLoad(Segment.this); } else { state = ImagesStates.NULL; } } } catch (Exception | OutOfMemoryError e) { e.printStackTrace(); } } }).start(); } } } protected class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { float nScale = Math.max(.8f, Math.min(mScaleFactor * detector.getScaleFactor(), 3.0f)); if ((nScale <= 3f && nScale >= 1f)) {//can be better, but how ? float final_x = (((((screenWidth * nScale) - screenWidth)) / nScale) - ((((screenWidth * mScaleFactor) - screenWidth)) / mScaleFactor)) * detector.getFocusX() / screenWidth; float final_y = (((((screenHeight * nScale) - screenHeight)) / nScale) - ((((screenHeight * mScaleFactor) - screenHeight)) / mScaleFactor)) * detector.getFocusX() / screenHeight; screenHeightSS = screenHeight; screenWidthSS = screenWidth; relativeScroll(final_x, final_y); } else if (nScale < 1) { screenHeightSS = (int) (nScale * screenHeight); screenWidthSS = (int) (nScale * screenWidth); relativeScroll(0, 0); } mScaleFactor = nScale; generateDrawPool(); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { super.onScaleEnd(detector); } } }