package com.artifex.mupdf.viewer; import com.artifex.mupdf.fitz.Cookie; import com.artifex.mupdf.fitz.Link; import android.content.Context; import android.graphics.Bitmap.Config; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.os.Handler; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ProgressBar; import android.os.AsyncTask; // Make our ImageViews opaque to optimize redraw class OpaqueImageView extends ImageView { public OpaqueImageView(Context context) { super(context); } @Override public boolean isOpaque() { return true; } } public class PageView extends ViewGroup { private final MuPDFCore mCore; private static final int HIGHLIGHT_COLOR = 0x80cc6600; private static final int LINK_COLOR = 0x800066cc; private static final int BOX_COLOR = 0xFF4444FF; private static final int BACKGROUND_COLOR = 0xFFFFFFFF; private static final int PROGRESS_DIALOG_DELAY = 200; protected final Context mContext; protected int mPageNumber; private Point mParentSize; protected Point mSize; // Size of page at minimum zoom protected float mSourceScale; private ImageView mEntire; // Image rendered at minimum zoom private Bitmap mEntireBm; private Matrix mEntireMat; private AsyncTask<Void,Void,Link[]> mGetLinkInfo; private CancellableAsyncTask<Void, Void> mDrawEntire; private Point mPatchViewSize; // View size on the basis of which the patch was created private Rect mPatchArea; private ImageView mPatch; private Bitmap mPatchBm; private CancellableAsyncTask<Void,Void> mDrawPatch; private RectF mSearchBoxes[]; protected Link mLinks[]; private View mSearchView; private boolean mIsBlank; private boolean mHighlightLinks; private ProgressBar mBusyIndicator; private final Handler mHandler = new Handler(); public PageView(Context c, MuPDFCore core, Point parentSize, Bitmap sharedHqBm) { super(c); mContext = c; mCore = core; mParentSize = parentSize; setBackgroundColor(BACKGROUND_COLOR); mEntireBm = Bitmap.createBitmap(parentSize.x, parentSize.y, Config.ARGB_8888); mPatchBm = sharedHqBm; mEntireMat = new Matrix(); } private void reinit() { // Cancel pending render task if (mDrawEntire != null) { mDrawEntire.cancel(); mDrawEntire = null; } if (mDrawPatch != null) { mDrawPatch.cancel(); mDrawPatch = null; } if (mGetLinkInfo != null) { mGetLinkInfo.cancel(true); mGetLinkInfo = null; } mIsBlank = true; mPageNumber = 0; if (mSize == null) mSize = mParentSize; if (mEntire != null) { mEntire.setImageBitmap(null); mEntire.invalidate(); } if (mPatch != null) { mPatch.setImageBitmap(null); mPatch.invalidate(); } mPatchViewSize = null; mPatchArea = null; mSearchBoxes = null; mLinks = null; } public void releaseResources() { reinit(); if (mBusyIndicator != null) { removeView(mBusyIndicator); mBusyIndicator = null; } } public void releaseBitmaps() { reinit(); // recycle bitmaps before releasing them. if (mEntireBm!=null) mEntireBm.recycle(); mEntireBm = null; if (mPatchBm!=null) mPatchBm.recycle(); mPatchBm = null; } public void blank(int page) { reinit(); mPageNumber = page; if (mBusyIndicator == null) { mBusyIndicator = new ProgressBar(mContext); mBusyIndicator.setIndeterminate(true); addView(mBusyIndicator); } setBackgroundColor(BACKGROUND_COLOR); } public void setPage(int page, PointF size) { // Cancel pending render task if (mDrawEntire != null) { mDrawEntire.cancel(); mDrawEntire = null; } mIsBlank = false; // Highlights may be missing because mIsBlank was true on last draw if (mSearchView != null) mSearchView.invalidate(); mPageNumber = page; if (mEntire == null) { mEntire = new OpaqueImageView(mContext); mEntire.setScaleType(ImageView.ScaleType.MATRIX); addView(mEntire); } // Calculate scaled size that fits within the screen limits // This is the size at minimum zoom mSourceScale = Math.min(mParentSize.x/size.x, mParentSize.y/size.y); Point newSize = new Point((int)(size.x*mSourceScale), (int)(size.y*mSourceScale)); mSize = newSize; mEntire.setImageBitmap(null); mEntire.invalidate(); // Get the link info in the background mGetLinkInfo = new AsyncTask<Void,Void,Link[]>() { protected Link[] doInBackground(Void... v) { return getLinkInfo(); } protected void onPostExecute(Link[] v) { mLinks = v; if (mSearchView != null) mSearchView.invalidate(); } }; mGetLinkInfo.execute(); // Render the page in the background mDrawEntire = new CancellableAsyncTask<Void, Void>(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) { @Override public void onPreExecute() { setBackgroundColor(BACKGROUND_COLOR); mEntire.setImageBitmap(null); mEntire.invalidate(); if (mBusyIndicator == null) { mBusyIndicator = new ProgressBar(mContext); mBusyIndicator.setIndeterminate(true); addView(mBusyIndicator); mBusyIndicator.setVisibility(INVISIBLE); mHandler.postDelayed(new Runnable() { public void run() { if (mBusyIndicator != null) mBusyIndicator.setVisibility(VISIBLE); } }, PROGRESS_DIALOG_DELAY); } } @Override public void onPostExecute(Void result) { removeView(mBusyIndicator); mBusyIndicator = null; mEntire.setImageBitmap(mEntireBm); mEntire.invalidate(); setBackgroundColor(Color.TRANSPARENT); } }; mDrawEntire.execute(); if (mSearchView == null) { mSearchView = new View(mContext) { @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); // Work out current total scale factor // from source to view final float scale = mSourceScale*(float)getWidth()/(float)mSize.x; final Paint paint = new Paint(); if (!mIsBlank && mSearchBoxes != null) { paint.setColor(HIGHLIGHT_COLOR); for (RectF rect : mSearchBoxes) canvas.drawRect(rect.left*scale, rect.top*scale, rect.right*scale, rect.bottom*scale, paint); } if (!mIsBlank && mLinks != null && mHighlightLinks) { paint.setColor(LINK_COLOR); for (Link link : mLinks) canvas.drawRect(link.bounds.x0*scale, link.bounds.y0*scale, link.bounds.x1*scale, link.bounds.y1*scale, paint); } } }; addView(mSearchView); } requestLayout(); } public void setSearchBoxes(RectF searchBoxes[]) { mSearchBoxes = searchBoxes; if (mSearchView != null) mSearchView.invalidate(); } public void setLinkHighlighting(boolean f) { mHighlightLinks = f; if (mSearchView != null) mSearchView.invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int x, y; switch(View.MeasureSpec.getMode(widthMeasureSpec)) { case View.MeasureSpec.UNSPECIFIED: x = mSize.x; break; default: x = View.MeasureSpec.getSize(widthMeasureSpec); } switch(View.MeasureSpec.getMode(heightMeasureSpec)) { case View.MeasureSpec.UNSPECIFIED: y = mSize.y; break; default: y = View.MeasureSpec.getSize(heightMeasureSpec); } setMeasuredDimension(x, y); if (mBusyIndicator != null) { int limit = Math.min(mParentSize.x, mParentSize.y)/2; mBusyIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int w = right-left; int h = bottom-top; if (mEntire != null) { if (mEntire.getWidth() != w || mEntire.getHeight() != h) { mEntireMat.setScale(w/(float)mSize.x, h/(float)mSize.y); mEntire.setImageMatrix(mEntireMat); mEntire.invalidate(); } mEntire.layout(0, 0, w, h); } if (mSearchView != null) { mSearchView.layout(0, 0, w, h); } if (mPatchViewSize != null) { if (mPatchViewSize.x != w || mPatchViewSize.y != h) { // Zoomed since patch was created mPatchViewSize = null; mPatchArea = null; if (mPatch != null) { mPatch.setImageBitmap(null); mPatch.invalidate(); } } else { mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom); } } if (mBusyIndicator != null) { int bw = mBusyIndicator.getMeasuredWidth(); int bh = mBusyIndicator.getMeasuredHeight(); mBusyIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2); } } public void updateHq(boolean update) { Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom()); if (viewArea.width() == mSize.x || viewArea.height() == mSize.y) { // If the viewArea's size matches the unzoomed size, there is no need for an hq patch if (mPatch != null) { mPatch.setImageBitmap(null); mPatch.invalidate(); } } else { final Point patchViewSize = new Point(viewArea.width(), viewArea.height()); final Rect patchArea = new Rect(0, 0, mParentSize.x, mParentSize.y); // Intersect and test that there is an intersection if (!patchArea.intersect(viewArea)) return; // Offset patch area to be relative to the view top left patchArea.offset(-viewArea.left, -viewArea.top); boolean area_unchanged = patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize); // If being asked for the same area as last time and not because of an update then nothing to do if (area_unchanged && !update) return; boolean completeRedraw = !(area_unchanged && update); // Stop the drawing of previous patch if still going if (mDrawPatch != null) { mDrawPatch.cancel(); mDrawPatch = null; } // Create and add the image view if not already done if (mPatch == null) { mPatch = new OpaqueImageView(mContext); mPatch.setScaleType(ImageView.ScaleType.MATRIX); addView(mPatch); mSearchView.bringToFront(); } CancellableTaskDefinition<Void, Void> task; if (completeRedraw) task = getDrawPageTask(mPatchBm, patchViewSize.x, patchViewSize.y, patchArea.left, patchArea.top, patchArea.width(), patchArea.height()); else task = getUpdatePageTask(mPatchBm, patchViewSize.x, patchViewSize.y, patchArea.left, patchArea.top, patchArea.width(), patchArea.height()); mDrawPatch = new CancellableAsyncTask<Void,Void>(task) { public void onPostExecute(Void result) { mPatchViewSize = patchViewSize; mPatchArea = patchArea; mPatch.setImageBitmap(mPatchBm); mPatch.invalidate(); //requestLayout(); // Calling requestLayout here doesn't lead to a later call to layout. No idea // why, but apparently others have run into the problem. mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom); } }; mDrawPatch.execute(); } } public void update() { // Cancel pending render task if (mDrawEntire != null) { mDrawEntire.cancel(); mDrawEntire = null; } if (mDrawPatch != null) { mDrawPatch.cancel(); mDrawPatch = null; } // Render the page in the background mDrawEntire = new CancellableAsyncTask<Void, Void>(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) { public void onPostExecute(Void result) { mEntire.setImageBitmap(mEntireBm); mEntire.invalidate(); } }; mDrawEntire.execute(); updateHq(true); } public void removeHq() { // Stop the drawing of the patch if still going if (mDrawPatch != null) { mDrawPatch.cancel(); mDrawPatch = null; } // And get rid of it mPatchViewSize = null; mPatchArea = null; if (mPatch != null) { mPatch.setImageBitmap(null); mPatch.invalidate(); } } public int getPage() { return mPageNumber; } @Override public boolean isOpaque() { return true; } public Link hitLink(float x, float y) { // Since link highlighting was implemented, the super class // PageView has had sufficient information to be able to // perform this method directly. Making that change would // make MuPDFCore.hitLinkPage superfluous. float scale = mSourceScale*(float)getWidth()/(float)mSize.x; float docRelX = (x - getLeft())/scale; float docRelY = (y - getTop())/scale; if (mLinks != null) for (Link l: mLinks) if (l.bounds.contains(docRelX, docRelY)) return l; return null; } protected CancellableTaskDefinition<Void, Void> getDrawPageTask(final Bitmap bm, final int sizeX, final int sizeY, final int patchX, final int patchY, final int patchWidth, final int patchHeight) { return new MuPDFCancellableTaskDefinition<Void, Void>() { @Override public Void doInBackground(Cookie cookie, Void ... params) { // Workaround bug in Android Honeycomb 3.x, where the bitmap generation count // is not incremented when drawing. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) bm.eraseColor(0); mCore.drawPage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie); return null; } }; } protected CancellableTaskDefinition<Void, Void> getUpdatePageTask(final Bitmap bm, final int sizeX, final int sizeY, final int patchX, final int patchY, final int patchWidth, final int patchHeight) { return new MuPDFCancellableTaskDefinition<Void, Void>() { @Override public Void doInBackground(Cookie cookie, Void ... params) { // Workaround bug in Android Honeycomb 3.x, where the bitmap generation count // is not incremented when drawing. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) bm.eraseColor(0); mCore.updatePage(bm, mPageNumber, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight, cookie); return null; } }; } protected Link[] getLinkInfo() { return mCore.getPageLinks(mPageNumber); } }