summaryrefslogtreecommitdiff
path: root/android
diff options
context:
space:
mode:
authorPaul Gardiner <paul@glidos.net>2012-02-07 15:22:55 +0000
committerRobin Watts <robin.watts@artifex.com>2012-02-20 13:37:23 +0000
commitb3708b652831202b8fc600ccdac8ee3218966dc9 (patch)
tree9902eafb6639b4fe40358d24ae69d31d026d1f1d /android
parent1404fbe67da5a72643730ee3000701c85a46a507 (diff)
downloadmupdf-b3708b652831202b8fc600ccdac8ee3218966dc9.tar.xz
Updated MuPDF Android app from Paul Gardiner.
Diffstat (limited to 'android')
-rw-r--r--android/AndroidManifest.xml17
-rw-r--r--android/ReadMe.txt3
-rw-r--r--android/jni/Application.mk1
-rw-r--r--android/jni/mupdf.c360
-rw-r--r--android/project.properties11
-rw-r--r--android/res/anim/fade_in.xml5
-rw-r--r--android/res/anim/fade_out.xml5
-rw-r--r--android/res/drawable-hdpi/btn_star_big_off.pngbin0 -> 2208 bytes
-rw-r--r--android/res/drawable-hdpi/ic_btn_search.pngbin0 -> 2365 bytes
-rw-r--r--android/res/drawable-hdpi/ic_media_next.pngbin0 -> 1675 bytes
-rw-r--r--android/res/drawable-hdpi/ic_media_previous.pngbin0 -> 1713 bytes
-rw-r--r--android/res/drawable-mdpi/btn_star_big_off.pngbin0 -> 1316 bytes
-rw-r--r--android/res/drawable-mdpi/ic_btn_search.pngbin0 -> 1390 bytes
-rw-r--r--android/res/drawable-mdpi/ic_media_next.pngbin0 -> 1309 bytes
-rw-r--r--android/res/drawable-mdpi/ic_media_previous.pngbin0 -> 1295 bytes
-rw-r--r--android/res/drawable/busy.xml10
-rw-r--r--android/res/drawable/page_num.xml10
-rw-r--r--android/res/drawable/slider.xml10
-rw-r--r--android/res/drawable/tile09.jpgbin0 -> 17986 bytes
-rw-r--r--android/res/drawable/tiled_background.xml4
-rw-r--r--android/res/layout/buttons.xml129
-rw-r--r--android/res/layout/outline_entry.xml25
-rw-r--r--android/res/layout/picker_entry.xml9
-rw-r--r--android/res/values/strings.xml10
-rw-r--r--android/src/com/artifex/mupdf/ChoosePDFActivity.java71
-rw-r--r--android/src/com/artifex/mupdf/MuPDFActivity.java547
-rw-r--r--android/src/com/artifex/mupdf/MuPDFCore.java77
-rw-r--r--android/src/com/artifex/mupdf/MuPDFPageAdapter.java72
-rw-r--r--android/src/com/artifex/mupdf/MuPDFPageView.java28
-rw-r--r--android/src/com/artifex/mupdf/OutlineActivity.java29
-rw-r--r--android/src/com/artifex/mupdf/OutlineActivityData.java17
-rw-r--r--android/src/com/artifex/mupdf/OutlineAdapter.java46
-rw-r--r--android/src/com/artifex/mupdf/OutlineItem.java14
-rw-r--r--android/src/com/artifex/mupdf/PageView.java323
-rw-r--r--android/src/com/artifex/mupdf/PixmapView.java579
-rw-r--r--android/src/com/artifex/mupdf/ReaderView.java554
36 files changed, 2260 insertions, 706 deletions
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 1bea0f8a..83c0ad86 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -11,13 +11,20 @@
<application android:label="@string/app_name"
android:icon="@drawable/icon"
android:debuggable="true">
- <activity android:name="MuPDFActivity"
- android:label="@string/app_name"
- android:theme="@style/Theme.NoBackground.NoTitle">
+ <activity android:name="ChoosePDFActivity" android:label="@string/picker_title">
<intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity><activity android:name="MuPDFActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:mimeType="application/pdf"/>
</intent-filter>
</activity>
+ <activity android:name="OutlineActivity" android:label="@string/outline_title"></activity>
+
</application>
</manifest>
diff --git a/android/ReadMe.txt b/android/ReadMe.txt
index 7d111ece..55624ea2 100644
--- a/android/ReadMe.txt
+++ b/android/ReadMe.txt
@@ -148,7 +148,8 @@ done once). With the emulator running type:
adb push ../../MyTests/pdf_reference17.pdf /mnt/sdcard/Download/test.pdf
(where obviously ../../MyTests/pdf_reference17.pdf is altered for your
-machine). (adb lives in <sdk>/platform-tools if it's not on your path).
+machine, and under Windows, should start c:/ even if invoked from cygwin) (adb lives
+in <sdk>/platform-tools if it's not on your path).
16) With the emulator running (see step 14), execute
diff --git a/android/jni/Application.mk b/android/jni/Application.mk
index 16de5796..580786f9 100644
--- a/android/jni/Application.mk
+++ b/android/jni/Application.mk
@@ -1,3 +1,4 @@
# The ARMv7 is significanly faster due to the use of the hardware FPU
+APP_PLATFORM=android-8
APP_ABI := armeabi armeabi-v7a
APP_OPTIM := debug
diff --git a/android/jni/mupdf.c b/android/jni/mupdf.c
index 3e471355..4775e072 100644
--- a/android/jni/mupdf.c
+++ b/android/jni/mupdf.c
@@ -20,6 +20,8 @@
/* Enable to log rendering times (render each frame 100 times and time) */
#undef TIME_DISPLAY_LIST
+#define MAX_SEARCH_HITS (500)
+
/* Globals */
fz_colorspace *colorspace;
pdf_document *xref;
@@ -30,12 +32,16 @@ float pageHeight = 100;
fz_display_list *currentPageList;
fz_rect currentMediabox;
fz_context *ctx;
+int currentPageNumber = -1;
+pdf_page *currentPage = NULL;
+fz_bbox *hit_bbox = NULL;
JNIEXPORT int JNICALL
Java_com_artifex_mupdf_MuPDFCore_openFile(JNIEnv * env, jobject thiz, jstring jfilename)
{
const char *filename;
int pages = 0;
+ int result = 0;
filename = (*env)->GetStringUTFChars(env, jfilename, NULL);
if (filename == NULL)
@@ -66,8 +72,8 @@ Java_com_artifex_mupdf_MuPDFCore_openFile(JNIEnv * env, jobject thiz, jstring jf
{
fz_throw(ctx, "Cannot open document: '%s'\n", filename);
}
- pages = pdf_count_pages(xref);
- LOGE("Done! %d pages", pages);
+ LOGE("Done!");
+ result = 1;
}
fz_catch(ctx)
{
@@ -78,7 +84,15 @@ Java_com_artifex_mupdf_MuPDFCore_openFile(JNIEnv * env, jobject thiz, jstring jf
ctx = NULL;
}
- return pages;
+ (*env)->ReleaseStringUTFChars(env, jfilename, filename);
+
+ return result;
+}
+
+JNIEXPORT int JNICALL
+Java_com_artifex_mupdf_MuPDFCore_countPagesInternal(JNIEnv *env, jobject thiz)
+{
+ return pdf_count_pages(xref);
}
JNIEXPORT void JNICALL
@@ -88,15 +102,20 @@ Java_com_artifex_mupdf_MuPDFCore_gotoPageInternal(JNIEnv *env, jobject thiz, int
fz_matrix ctm;
fz_bbox bbox;
fz_device *dev = NULL;
- pdf_page *currentPage = NULL;
fz_var(dev);
- fz_var(currentPage);
+
+ if (currentPage != NULL && page != currentPageNumber)
+ {
+ pdf_free_page(xref, currentPage);
+ currentPage = NULL;
+ }
/* In the event of an error, ensure we give a non-empty page */
pageWidth = 100;
pageHeight = 100;
+ currentPageNumber = page;
LOGE("Goto page %d...", page);
fz_try(ctx)
{
@@ -113,17 +132,12 @@ Java_com_artifex_mupdf_MuPDFCore_gotoPageInternal(JNIEnv *env, jobject thiz, int
bbox = fz_round_rect(fz_transform_rect(ctm, currentMediabox));
pageWidth = bbox.x1-bbox.x0;
pageHeight = bbox.y1-bbox.y0;
- /* Render to list */
- currentPageList = fz_new_display_list(ctx);
- dev = fz_new_list_device(ctx, currentPageList);
- pdf_run_page(xref, currentPage, dev, fz_identity, NULL);
}
fz_catch(ctx)
{
+ currentPageNumber = page;
LOGE("cannot make displaylist from page %d", pagenum);
}
- pdf_free_page(ctx, currentPage);
- currentPage = NULL;
fz_free_device(dev);
dev = NULL;
}
@@ -184,6 +198,13 @@ Java_com_artifex_mupdf_MuPDFCore_drawPage(JNIEnv *env, jobject thiz, jobject bit
fz_try(ctx)
{
+ if (currentPageList == NULL)
+ {
+ /* Render to list */
+ currentPageList = fz_new_display_list(ctx);
+ dev = fz_new_list_device(ctx, currentPageList);
+ pdf_run_page(xref, currentPage, dev, fz_identity, NULL);
+ }
rect.x0 = patchX;
rect.y0 = patchY;
rect.x1 = patchX + patchW;
@@ -191,10 +212,10 @@ Java_com_artifex_mupdf_MuPDFCore_drawPage(JNIEnv *env, jobject thiz, jobject bit
pix = fz_new_pixmap_with_rect_and_data(ctx, colorspace, rect, pixels);
if (currentPageList == NULL)
{
- fz_clear_pixmap_with_value(pix, 0xd0);
+ fz_clear_pixmap_with_value(ctx, pix, 0xd0);
break;
}
- fz_clear_pixmap_with_value(pix, 0xff);
+ fz_clear_pixmap_with_value(ctx, pix, 0xff);
zoom = resolution / 72;
ctm = fz_scale(zoom, zoom);
@@ -238,11 +259,324 @@ Java_com_artifex_mupdf_MuPDFCore_drawPage(JNIEnv *env, jobject thiz, jobject bit
return 1;
}
+static int
+charat(fz_text_span *span, int idx)
+{
+ int ofs = 0;
+ while (span) {
+ if (idx < ofs + span->len)
+ return span->text[idx - ofs].c;
+ if (span->eol) {
+ if (idx == ofs + span->len)
+ return ' ';
+ ofs ++;
+ }
+ ofs += span->len;
+ span = span->next;
+ }
+ return 0;
+}
+
+static fz_bbox
+bboxat(fz_text_span *span, int idx)
+{
+ int ofs = 0;
+ while (span) {
+ if (idx < ofs + span->len)
+ return span->text[idx - ofs].bbox;
+ if (span->eol) {
+ if (idx == ofs + span->len)
+ return fz_empty_bbox;
+ ofs ++;
+ }
+ ofs += span->len;
+ span = span->next;
+ }
+ return fz_empty_bbox;
+}
+
+static int
+textlen(fz_text_span *span)
+{
+ int len = 0;
+ while (span) {
+ len += span->len;
+ if (span->eol)
+ len ++;
+ span = span->next;
+ }
+ return len;
+}
+
+static int
+match(fz_text_span *span, const char *s, int n)
+{
+ int start = n, c;
+ while (*s) {
+ s += chartorune(&c, (char *)s);
+ if (c == ' ' && charat(span, n) == ' ') {
+ while (charat(span, n) == ' ')
+ n++;
+ } else {
+ if (tolower(c) != tolower(charat(span, n)))
+ return 0;
+ n++;
+ }
+ }
+ return n - start;
+}
+
+static int
+countOutlineItems(fz_outline *outline)
+{
+ int count = 0;
+
+ while (outline)
+ {
+ if (outline->dest.kind == FZ_LINK_GOTO
+ && outline->dest.ld.gotor.page >= 0
+ && outline->title)
+ count++;
+
+ count += countOutlineItems(outline->down);
+ outline = outline->next;
+ }
+
+ return count;
+}
+
+static int
+fillInOutlineItems(JNIEnv * env, jclass olClass, jmethodID ctor, jobjectArray arr, int pos, fz_outline *outline, int level)
+{
+ while (outline)
+ {
+ if (outline->dest.kind == FZ_LINK_GOTO)
+ {
+ int page = outline->dest.ld.gotor.page;
+ if (page >= 0 && outline->title)
+ {
+ jobject ol;
+ jstring title = (*env)->NewStringUTF(env, outline->title);
+ if (title == NULL) return -1;
+ ol = (*env)->NewObject(env, olClass, ctor, level, title, page);
+ if (ol == NULL) return -1;
+ (*env)->SetObjectArrayElement(env, arr, pos, ol);
+ (*env)->DeleteLocalRef(env, ol);
+ (*env)->DeleteLocalRef(env, title);
+ pos++;
+ }
+ }
+ pos = fillInOutlineItems(env, olClass, ctor, arr, pos, outline->down, level+1);
+ if (pos < 0) return -1;
+ outline = outline->next;
+ }
+
+ return pos;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_artifex_mupdf_MuPDFCore_needsPasswordInternal(JNIEnv * env, jobject thiz)
+{
+ return pdf_needs_password(xref) ? JNI_TRUE : JNI_FALSE;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_artifex_mupdf_MuPDFCore_authenticatePasswordInternal(JNIEnv *env, jobject thiz, jstring password)
+{
+ const char *pw;
+ int result;
+ pw = (*env)->GetStringUTFChars(env, password, NULL);
+ if (pw == NULL)
+ return JNI_FALSE;
+
+ result = pdf_authenticate_password(xref, (char *)pw);
+ (*env)->ReleaseStringUTFChars(env, password, pw);
+ return result;
+}
+
+JNIEXPORT jboolean JNICALL
+Java_com_artifex_mupdf_MuPDFCore_hasOutlineInternal(JNIEnv * env, jobject thiz)
+{
+ fz_outline *outline = pdf_load_outline(xref);
+ return (outline == NULL) ? JNI_FALSE : JNI_TRUE;
+}
+
+JNIEXPORT jobjectArray JNICALL
+Java_com_artifex_mupdf_MuPDFCore_getOutlineInternal(JNIEnv * env, jobject thiz)
+{
+ jclass olClass;
+ jmethodID ctor;
+ jobjectArray arr;
+ jobject ol;
+ fz_outline *outline;
+ int nItems;
+
+ olClass = (*env)->FindClass(env, "com/artifex/mupdf/OutlineItem");
+ if (olClass == NULL) return NULL;
+ ctor = (*env)->GetMethodID(env, olClass, "<init>", "(ILjava/lang/String;I)V");
+ if (ctor == NULL) return NULL;
+
+ outline = pdf_load_outline(xref);
+ nItems = countOutlineItems(outline);
+
+ arr = (*env)->NewObjectArray(env,
+ nItems,
+ olClass,
+ NULL);
+ if (arr == NULL) return NULL;
+
+ return fillInOutlineItems(env, olClass, ctor, arr, 0, outline, 0) > 0
+ ? arr
+ :NULL;
+}
+
+JNIEXPORT jobjectArray JNICALL
+Java_com_artifex_mupdf_MuPDFCore_searchPage(JNIEnv * env, jobject thiz, jstring jtext)
+{
+ jclass rectClass;
+ jmethodID ctor;
+ jobjectArray arr;
+ jobject rect;
+ fz_text_span *text = NULL;
+ fz_device *dev = NULL;
+ float zoom;
+ fz_matrix ctm;
+ int pos;
+ int len;
+ int i, n;
+ int hit_count = 0;
+ const char *str;
+
+ rectClass = (*env)->FindClass(env, "android/graphics/RectF");
+ if (rectClass == NULL) return NULL;
+ ctor = (*env)->GetMethodID(env, rectClass, "<init>", "(FFFF)V");
+ if (ctor == NULL) return NULL;
+ str = (*env)->GetStringUTFChars(env, jtext, NULL);
+ if (str == NULL) return NULL;
+
+ fz_var(text);
+ fz_var(dev);
+
+ fz_try(ctx)
+ {
+ if (hit_bbox == NULL)
+ hit_bbox = fz_malloc_array(ctx, MAX_SEARCH_HITS, sizeof(*hit_bbox));
+
+ text = fz_new_text_span(ctx);
+ dev = fz_new_text_device(ctx, text);
+ zoom = resolution / 72;
+ ctm = fz_scale(zoom, zoom);
+ pdf_run_page(xref, currentPage, dev, ctm, NULL);
+ fz_free_device(dev);
+ dev = NULL;
+
+ len = textlen(text);
+ for (pos = 0; pos < len; pos++)
+ {
+ fz_bbox rr = fz_empty_bbox;
+ n = match(text, str, pos);
+ for (i = 0; i < n; i++)
+ rr = fz_union_bbox(rr, bboxat(text, pos + i));
+
+ if (!fz_is_empty_bbox(rr) && hit_count < MAX_SEARCH_HITS)
+ hit_bbox[hit_count++] = rr;
+ }
+ fz_free_text_span(ctx, text);
+ text = NULL;
+ }
+ fz_catch(ctx)
+ {
+ jclass cls;
+ fz_free_device(dev);
+ fz_free_text_span(ctx, text);
+ (*env)->ReleaseStringUTFChars(env, jtext, str);
+ cls = (*env)->FindClass(env, "java/lang/OutOfMemoryError");
+ if (cls != NULL)
+ (*env)->ThrowNew(env, cls, "Out of memory in MuPDFCore_searchPage");
+ (*env)->DeleteLocalRef(env, cls);
+
+ return NULL;
+ }
+
+ (*env)->ReleaseStringUTFChars(env, jtext, str);
+
+ arr = (*env)->NewObjectArray(env,
+ hit_count,
+ rectClass,
+ NULL);
+ if (arr == NULL) return NULL;
+
+ for (i = 0; i < hit_count; i++) {
+ rect = (*env)->NewObject(env, rectClass, ctor,
+ (float) (hit_bbox[i].x0),
+ (float) (hit_bbox[i].y0),
+ (float) (hit_bbox[i].x1),
+ (float) (hit_bbox[i].y1));
+ if (rect == NULL)
+ return NULL;
+ (*env)->SetObjectArrayElement(env, arr, i, rect);
+ (*env)->DeleteLocalRef(env, rect);
+ }
+
+ return arr;
+}
+
JNIEXPORT void JNICALL
Java_com_artifex_mupdf_MuPDFCore_destroying(JNIEnv * env, jobject thiz)
{
+ fz_free(ctx, hit_bbox);
+ hit_bbox = NULL;
fz_free_display_list(ctx, currentPageList);
currentPageList = NULL;
+ if (currentPage != NULL)
+ {
+ pdf_free_page(xref, currentPage);
+ currentPage = NULL;
+ }
pdf_close_document(xref);
xref = NULL;
}
+
+JNIEXPORT int JNICALL
+Java_com_artifex_mupdf_MuPDFCore_getPageLink(JNIEnv * env, jobject thiz, int pageNumber, float x, float y)
+{
+ fz_matrix ctm;
+ float zoom;
+ fz_link *link;
+ fz_point p;
+
+ Java_com_artifex_mupdf_MuPDFCore_gotoPageInternal(env, thiz, pageNumber);
+ if (currentPageNumber == -1 || currentPage == NULL)
+ return -1;
+
+ p.x = x;
+ p.y = y;
+
+ /* Ultimately we should probably return a pointer to a java structure
+ * with the link details in, but for now, page number will suffice.
+ */
+ zoom = resolution / 72;
+ ctm = fz_scale(zoom, zoom);
+ ctm = fz_invert_matrix(ctm);
+
+ p = fz_transform_point(ctm, p);
+
+ for (link = currentPage->links; link; link = link->next)
+ {
+ if (p.x >= link->rect.x0 && p.x <= link->rect.x1)
+ if (p.y >= link->rect.y0 && p.y <= link->rect.y1)
+ break;
+ }
+
+ if (link == NULL)
+ return -1;
+
+ if (link->dest.kind == FZ_LINK_URI)
+ {
+ //gotouri(link->dest.ld.uri.uri);
+ return -1;
+ }
+ else if (link->dest.kind == FZ_LINK_GOTO)
+ return link->dest.ld.gotor.page;
+ return -1;
+}
diff --git a/android/project.properties b/android/project.properties
new file mode 100644
index 00000000..ea89160e
--- /dev/null
+++ b/android/project.properties
@@ -0,0 +1,11 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-8
diff --git a/android/res/anim/fade_in.xml b/android/res/anim/fade_in.xml
new file mode 100644
index 00000000..65bf6edd
--- /dev/null
+++ b/android/res/anim/fade_in.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"
+ android:duration="100" />
diff --git a/android/res/anim/fade_out.xml b/android/res/anim/fade_out.xml
new file mode 100644
index 00000000..efde8c13
--- /dev/null
+++ b/android/res/anim/fade_out.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:duration="100" />
diff --git a/android/res/drawable-hdpi/btn_star_big_off.png b/android/res/drawable-hdpi/btn_star_big_off.png
new file mode 100644
index 00000000..4be0f5df
--- /dev/null
+++ b/android/res/drawable-hdpi/btn_star_big_off.png
Binary files differ
diff --git a/android/res/drawable-hdpi/ic_btn_search.png b/android/res/drawable-hdpi/ic_btn_search.png
new file mode 100644
index 00000000..0a91eabc
--- /dev/null
+++ b/android/res/drawable-hdpi/ic_btn_search.png
Binary files differ
diff --git a/android/res/drawable-hdpi/ic_media_next.png b/android/res/drawable-hdpi/ic_media_next.png
new file mode 100644
index 00000000..2552f4ed
--- /dev/null
+++ b/android/res/drawable-hdpi/ic_media_next.png
Binary files differ
diff --git a/android/res/drawable-hdpi/ic_media_previous.png b/android/res/drawable-hdpi/ic_media_previous.png
new file mode 100644
index 00000000..05eba718
--- /dev/null
+++ b/android/res/drawable-hdpi/ic_media_previous.png
Binary files differ
diff --git a/android/res/drawable-mdpi/btn_star_big_off.png b/android/res/drawable-mdpi/btn_star_big_off.png
new file mode 100644
index 00000000..7e9342b5
--- /dev/null
+++ b/android/res/drawable-mdpi/btn_star_big_off.png
Binary files differ
diff --git a/android/res/drawable-mdpi/ic_btn_search.png b/android/res/drawable-mdpi/ic_btn_search.png
new file mode 100644
index 00000000..3f8913e2
--- /dev/null
+++ b/android/res/drawable-mdpi/ic_btn_search.png
Binary files differ
diff --git a/android/res/drawable-mdpi/ic_media_next.png b/android/res/drawable-mdpi/ic_media_next.png
new file mode 100644
index 00000000..84f38e8f
--- /dev/null
+++ b/android/res/drawable-mdpi/ic_media_next.png
Binary files differ
diff --git a/android/res/drawable-mdpi/ic_media_previous.png b/android/res/drawable-mdpi/ic_media_previous.png
new file mode 100644
index 00000000..1bba5441
--- /dev/null
+++ b/android/res/drawable-mdpi/ic_media_previous.png
Binary files differ
diff --git a/android/res/drawable/busy.xml b/android/res/drawable/busy.xml
new file mode 100644
index 00000000..f0bc5612
--- /dev/null
+++ b/android/res/drawable/busy.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="10dp" />
+ <padding android:left="20dp"
+ android:right="20dp"
+ android:top="20dp"
+ android:bottom="20dp" />
+ <solid android:color="#FF444444" />
+</shape> \ No newline at end of file
diff --git a/android/res/drawable/page_num.xml b/android/res/drawable/page_num.xml
new file mode 100644
index 00000000..8090fb45
--- /dev/null
+++ b/android/res/drawable/page_num.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="2dp" />
+ <padding android:left="5dp"
+ android:right="5dp"
+ android:top="5dp"
+ android:bottom="5dp" />
+ <solid android:color="#CC444444" />
+</shape> \ No newline at end of file
diff --git a/android/res/drawable/slider.xml b/android/res/drawable/slider.xml
new file mode 100644
index 00000000..dfd65694
--- /dev/null
+++ b/android/res/drawable/slider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="5dp" />
+ <padding android:left="10dp"
+ android:right="10dp"
+ android:top="10dp"
+ android:bottom="10dp" />
+ <solid android:color="#CC444444" />
+</shape> \ No newline at end of file
diff --git a/android/res/drawable/tile09.jpg b/android/res/drawable/tile09.jpg
new file mode 100644
index 00000000..3fcd274e
--- /dev/null
+++ b/android/res/drawable/tile09.jpg
Binary files differ
diff --git a/android/res/drawable/tiled_background.xml b/android/res/drawable/tiled_background.xml
new file mode 100644
index 00000000..8c5f64c5
--- /dev/null
+++ b/android/res/drawable/tiled_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/tile09"
+ android:tileMode="repeat" /> \ No newline at end of file
diff --git a/android/res/layout/buttons.xml b/android/res/layout/buttons.xml
new file mode 100644
index 00000000..cb5b4075
--- /dev/null
+++ b/android/res/layout/buttons.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ViewAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/buttons"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out">
+
+ <RelativeLayout
+ android:id="@+id/dummy"
+ android:layout_width="0dp"
+ android:layout_height="0dp" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <SeekBar
+ android:id="@+id/pageSlider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_margin="10dp"
+ android:background="@drawable/slider" />
+
+ <ViewSwitcher
+ android:id="@+id/switcher"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_below="@+id/topBar"
+ android:layout_centerHorizontal="true" >
+
+ <RelativeLayout
+ android:id="@+id/topBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#CC444444"
+ android:paddingBottom="0dp"
+ android:paddingLeft="5dp"
+ android:paddingRight="5dp"
+ android:paddingTop="0dp" >
+
+ <ImageButton
+ android:id="@+id/searchButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/search_document"
+ android:src="@drawable/ic_btn_search" />
+
+ <ImageButton
+ android:id="@+id/outlineButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@+id/searchButton"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/search_document"
+ android:src="@drawable/btn_star_big_off" />
+
+ <TextView
+ android:id="@+id/docNameText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@+id/outlineButton"
+ android:layout_centerVertical="true"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/topBar2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#CC444444" >
+
+ <Button
+ android:id="@+id/cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:text="@string/cancel" />
+
+ <EditText
+ android:id="@+id/searchText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_toLeftOf="@+id/searchBack"
+ android:layout_toRightOf="@+id/cancel"
+ android:singleLine="true"
+ android:inputType="text" />
+
+ <ImageButton
+ android:id="@+id/searchBack"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_toLeftOf="@+id/searchForward"
+ android:contentDescription="@string/search_backwards"
+ android:src="@drawable/ic_media_previous" />
+
+ <ImageButton
+ android:id="@+id/searchForward"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/search_forwards"
+ android:src="@drawable/ic_media_next" />
+ </RelativeLayout>
+ </ViewSwitcher>
+
+ <TextView
+ android:id="@+id/pageNumber"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/pageSlider"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="10dp"
+ android:background="@drawable/page_num"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ </RelativeLayout>
+
+</ViewAnimator> \ No newline at end of file
diff --git a/android/res/layout/outline_entry.xml b/android/res/layout/outline_entry.xml
new file mode 100644
index 00000000..5a617e99
--- /dev/null
+++ b/android/res/layout/outline_entry.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@+id/page"
+ android:singleLine="true"
+ android:layout_centerVertical="true"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:id="@+id/page"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@+id/title"
+ android:layout_alignBottom="@+id/title"
+ android:layout_alignParentRight="true"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/android/res/layout/picker_entry.xml b/android/res/layout/picker_entry.xml
new file mode 100644
index 00000000..050bbbfd
--- /dev/null
+++ b/android/res/layout/picker_entry.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="10dp"
+ android:textSize="20dp" >
+
+
+</TextView> \ No newline at end of file
diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml
index ec3f9ae6..7e1c9d46 100644
--- a/android/res/values/strings.xml
+++ b/android/res/values/strings.xml
@@ -1,4 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MuPDF</string>
+ <string name="no_media_warning">Storage Media not present</string>
+ <string name="no_media_hint">Sharing the storage media with a PC can make it inaccessible</string>
+ <string name="open_failed">Unable to open document</string>
+ <string name="cancel">Cancel</string>
+ <string name="search_backwards">Search backwards</string>
+ <string name="search_forwards">Search forwards</string>
+ <string name="search_document">Search document</string>
+ <string name="picker_title">PDF Documents</string>
+ <string name="outline_title">Table of Contents</string>
+ <string name="enter_password">Enter Password</string>
</resources>
diff --git a/android/src/com/artifex/mupdf/ChoosePDFActivity.java b/android/src/com/artifex/mupdf/ChoosePDFActivity.java
new file mode 100644
index 00000000..4fd636ce
--- /dev/null
+++ b/android/src/com/artifex/mupdf/ChoosePDFActivity.java
@@ -0,0 +1,71 @@
+package com.artifex.mupdf;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+public class ChoosePDFActivity extends ListActivity {
+ private File mDirectory;
+ private File [] mFiles;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String storageState = Environment.getExternalStorageState();
+
+ if (!Environment.MEDIA_MOUNTED.equals(storageState)
+ && !Environment.MEDIA_MOUNTED_READ_ONLY.equals(storageState))
+ {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.no_media_warning);
+ builder.setMessage(R.string.no_media_hint);
+ AlertDialog alert = builder.create();
+ alert.setButton(AlertDialog.BUTTON_POSITIVE,"Dismiss",
+ new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+ alert.show();
+ return;
+ }
+
+ mDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ mFiles = mDirectory.listFiles(new FilenameFilter() {
+ public boolean accept(File file, String name) {
+ return name.toLowerCase().endsWith(".pdf");
+ }
+
+ });
+ List<String> fileNames = new ArrayList<String>();
+ for (File f : mFiles)
+ fileNames.add(f.getName());
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.picker_entry, fileNames);
+ setListAdapter(adapter);
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+ Uri uri = Uri.parse(mFiles[position].getAbsolutePath());
+ Intent intent = new Intent(this,MuPDFActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setData(uri);
+ startActivity(intent);
+ }
+}
diff --git a/android/src/com/artifex/mupdf/MuPDFActivity.java b/android/src/com/artifex/mupdf/MuPDFActivity.java
index d5ba3bd8..45c92f96 100644
--- a/android/src/com/artifex/mupdf/MuPDFActivity.java
+++ b/android/src/com/artifex/mupdf/MuPDFActivity.java
@@ -1,46 +1,76 @@
package com.artifex.mupdf;
import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.RectF;
+import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.Environment;
-import android.view.*;
-import android.view.View.OnClickListener;
-import android.widget.*;
-import android.widget.LinearLayout.*;
-import java.io.File;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.method.PasswordTransformationMethod;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import android.widget.ViewSwitcher;
-import com.artifex.mupdf.PixmapView;
+class SearchTaskResult {
+ public final int pageNumber;
+ public final RectF searchBoxes[];
+ SearchTaskResult(int _pageNumber, RectF _searchBoxes[]) {
+ pageNumber = _pageNumber;
+ searchBoxes = _searchBoxes;
+ }
+}
public class MuPDFActivity extends Activity
{
/* The core rendering instance */
- private MuPDFCore core;
+ private MuPDFCore core;
+ private String mFileName;
+ private ReaderView mDocView;
+ private ViewAnimator mButtonsView;
+ private boolean mButtonsVisible;
+ private EditText mPasswordView;
+ private TextView mFilenameView;
+ private SeekBar mPageSlider;
+ private TextView mPageNumberView;
+ private ImageButton mSearchButton;
+ private Button mCancelButton;
+ private ImageButton mOutlineButton;
+ private ViewSwitcher mTopBarSwitcher;
+ private boolean mTopBarIsSearch;
+ private ImageButton mSearchBack;
+ private ImageButton mSearchFwd;
+ private EditText mSearchText;
+ private AsyncTask<Integer,Void,SearchTaskResult> mSearchTask;
+ private SearchTaskResult mSearchTaskResult;
+ private AlertDialog.Builder mAlertBuilder;
- private MuPDFCore openFile()
+ private MuPDFCore openFile(String path)
{
- String storageState = Environment.getExternalStorageState();
- File path, file;
- MuPDFCore core;
-
- if (Environment.MEDIA_MOUNTED.equals(storageState))
- {
- System.out.println("Media mounted read/write");
- }
- else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(storageState))
- {
- System.out.println("Media mounted read only");
- }
- else
- {
- System.out.println("No media at all! Bale!\n");
- return null;
- }
- path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- file = new File(path, "test.pdf");
- System.out.println("Trying to open "+file.toString());
+ int lastSlashPos = path.lastIndexOf('/');
+ mFileName = new String(lastSlashPos == -1
+ ? path
+ : path.substring(lastSlashPos+1));
+ System.out.println("Trying to open "+path);
try
{
- core = new MuPDFCore(file.toString());
+ core = new MuPDFCore(path);
+ // New file: drop the old outline data
+ OutlineActivityData.set(null);
}
catch (Exception e)
{
@@ -54,71 +84,258 @@ public class MuPDFActivity extends Activity
@Override
public void onCreate(Bundle savedInstanceState)
{
- PixmapView pixmapView;
+ super.onCreate(savedInstanceState);
+
+ mAlertBuilder = new AlertDialog.Builder(this);
if (core == null) {
core = (MuPDFCore)getLastNonConfigurationInstance();
+
+ if (savedInstanceState != null && savedInstanceState.containsKey("FileName")) {
+ mFileName = savedInstanceState.getString("FileName");
+ }
}
if (core == null) {
- core = openFile();
+ Intent intent = getIntent();
+ if (Intent.ACTION_VIEW.equals(intent.getAction()))
+ core = openFile(intent.getData().getEncodedPath());
+ if (core != null && core.needsPassword()) {
+ requestPassword(savedInstanceState);
+ return;
+ }
}
if (core == null)
{
- /* FIXME: Error handling here! */
+ AlertDialog alert = mAlertBuilder.create();
+ alert.setTitle(R.string.open_failed);
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, "Dismiss",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+ alert.show();
return;
}
- pixmapView = new PixmapView(this, core);
- super.onCreate(savedInstanceState);
+ createUI(savedInstanceState);
+ }
+
+ public void requestPassword(final Bundle savedInstanceState) {
+ mPasswordView = new EditText(this);
+ mPasswordView.setInputType(EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
+ mPasswordView.setTransformationMethod(new PasswordTransformationMethod());
+
+ AlertDialog alert = mAlertBuilder.create();
+ alert.setTitle(R.string.enter_password);
+ alert.setView(mPasswordView);
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, "Ok",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (core.authenticatePassword(mPasswordView.getText().toString())) {
+ createUI(savedInstanceState);
+ } else {
+ requestPassword(savedInstanceState);
+ }
+ }
+ });
+ alert.setButton(AlertDialog.BUTTON_NEGATIVE, "Cancel",
+ new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+ alert.show();
+ }
+
+ public void createUI(Bundle savedInstanceState) {
+ // Now create the UI.
+ // First create the document view making use of the ReaderView's internal
+ // gesture recognition
+ mDocView = new ReaderView(this) {
+ private boolean showButtonsDisabled;
+
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (!showButtonsDisabled) {
+ int linkPage = -1;
+ MuPDFPageView pageView = (MuPDFPageView) mDocView.getDisplayedView();
+ if (pageView != null) {
+ linkPage = pageView.hitLinkPage(e.getX(), e.getY());
+ }
+
+ if (linkPage != -1) {
+ mDocView.setDisplayedViewIndex(linkPage);
+ } else {
+ if (!mButtonsVisible) {
+ showButtons();
+ } else {
+ hideButtons();
+ }
+ }
+ }
+ return super.onSingleTapUp(e);
+ }
- /* Now create the UI */
- RelativeLayout layout;
- LinearLayout bar;
- MyButtonHandler bh = new MyButtonHandler(pixmapView);
-
- bar = new LinearLayout(this);
- bar.setOrientation(LinearLayout.HORIZONTAL);
- bh.buttonStart = new Button(this);
- bh.buttonStart.setText("<<");
- bh.buttonStart.setOnClickListener(bh);
- bar.addView(bh.buttonStart);
- bh.buttonPrev = new Button(this);
- bh.buttonPrev.setText("<");
- bh.buttonPrev.setOnClickListener(bh);
- bar.addView(bh.buttonPrev);
- bh.buttonNext = new Button(this);
- bh.buttonNext.setText(">");
- bh.buttonNext.setOnClickListener(bh);
- bar.addView(bh.buttonNext);
- bh.buttonEnd = new Button(this);
- bh.buttonEnd.setText(">>");
- bh.buttonEnd.setOnClickListener(bh);
- bar.addView(bh.buttonEnd);
-
- layout = new RelativeLayout(this);
- layout.setLayoutParams(new RelativeLayout.LayoutParams(
- RelativeLayout.LayoutParams.FILL_PARENT,
- RelativeLayout.LayoutParams.FILL_PARENT));
- layout.setGravity(Gravity.FILL);
-
- RelativeLayout.LayoutParams barParams =
- new RelativeLayout.LayoutParams(
- RelativeLayout.LayoutParams.FILL_PARENT,
- RelativeLayout.LayoutParams.WRAP_CONTENT);
- barParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
- bar.setId(100);
- layout.addView(bar, barParams);
-
- RelativeLayout.LayoutParams pixmapParams =
- new RelativeLayout.LayoutParams(
- RelativeLayout.LayoutParams.FILL_PARENT,
- RelativeLayout.LayoutParams.FILL_PARENT);
- pixmapParams.addRule(RelativeLayout.ABOVE,100);
- layout.addView(pixmapView, pixmapParams);
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (!showButtonsDisabled)
+ hideButtons();
+
+ return super.onScroll(e1, e2, distanceX, distanceY);
+ }
+
+ public boolean onScaleBegin(ScaleGestureDetector d) {
+ // Disabled showing the buttons until next touch.
+ // Not sure why this is needed, but without it
+ // pinch zoom can make the buttons appear
+ showButtonsDisabled = true;
+ return super.onScaleBegin(d);
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN)
+ showButtonsDisabled = false;
+
+ return super.onTouchEvent(event);
+ }
+
+ protected void onChildSetup(int i, View v) {
+ if (mSearchTaskResult != null && mSearchTaskResult.pageNumber == i)
+ ((PageView)v).setSearchBoxes(mSearchTaskResult.searchBoxes);
+ else
+ ((PageView)v).setSearchBoxes(null);
+ }
+
+ protected void onMoveToChild(int i) {
+ mPageNumberView.setText(String.format("%d/%d", i+1, core.countPages()));
+ mPageSlider.setMax(core.countPages()-1);
+ mPageSlider.setProgress(i);
+ if (mSearchTaskResult != null && mSearchTaskResult.pageNumber != i) {
+ mSearchTaskResult = null;
+ mDocView.resetupChildren();
+ }
+ }
+
+ protected void onSettle(View v) {
+ // When the layout has settled ask the page to render
+ // in HQ
+ ((PageView)v).addHq();
+ }
+
+ protected void onUnsettle(View v) {
+ // When something changes making the previous settled view
+ // no longer appropriate, tell the page to remove HQ
+ ((PageView)v).removeHq();
+ }
+ };
+ mDocView.setAdapter(new MuPDFPageAdapter(this, core));
+ mDocView.setBackgroundResource(R.drawable.tiled_background);
+
+ // Make the buttons overlay, and store all its
+ // controls in variables
+ makeButtonsView();
+
+ // Set the file-name text
+ mFilenameView.setText(mFileName);
+
+ // Activate the seekbar
+ mPageSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mDocView.setDisplayedViewIndex(seekBar.getProgress());
+ }
+
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ public void onProgressChanged(SeekBar seekBar, int progress,
+ boolean fromUser) {
+ updatePageNumView(progress);
+ }
+ });
+
+ // Activate the search-preparing button
+ mSearchButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ searchModeOn();
+ }
+ });
+
+ mCancelButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ searchModeOff();
+ }
+ });
+
+ // Search invoking buttons are disabled while there is no text specified
+ mSearchBack.setEnabled(false);
+ mSearchFwd.setEnabled(false);
+
+ // React to interaction with the text widget
+ mSearchText.addTextChangedListener(new TextWatcher() {
+
+ public void afterTextChanged(Editable s) {
+ boolean haveText = s.toString().length() > 0;
+ mSearchBack.setEnabled(haveText);
+ mSearchFwd.setEnabled(haveText);
+ }
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {}
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {}
+ });
+
+ // Activate search invoking buttons
+ mSearchBack.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ hideKeyboard();
+ search(-1);
+ }
+ });
+ mSearchFwd.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ hideKeyboard();
+ search(1);
+ }
+ });
+
+ if (core.hasOutline()) {
+ mOutlineButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ OutlineItem outline[] = core.getOutline();
+ if (outline != null) {
+ OutlineActivityData.get().items = outline;
+ Intent intent = new Intent(MuPDFActivity.this, OutlineActivity.class);
+ startActivityForResult(intent, 0);
+ }
+ }
+ });
+ } else {
+ mOutlineButton.setVisibility(View.GONE);
+ }
+ // Reenstate last state if it was recorded
+ SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+ mDocView.setDisplayedViewIndex(prefs.getInt("page"+mFileName, 0));
+
+ if (savedInstanceState == null || !savedInstanceState.getBoolean("ButtonsHidden", false))
+ showButtons();
+
+ if(savedInstanceState != null && savedInstanceState.getBoolean("SearchMode", false))
+ searchModeOn();
+
+ // Stick the document view and the buttons overlay into a parent view
+ RelativeLayout layout = new RelativeLayout(this);
+ layout.addView(mDocView);
+ layout.addView(mButtonsView);
setContentView(layout);
}
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode >= 0)
+ mDocView.setDisplayedViewIndex(resultCode);
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
public Object onRetainNonConfigurationInstance()
{
MuPDFCore mycore = core;
@@ -126,6 +343,41 @@ public class MuPDFActivity extends Activity
return mycore;
}
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ if (mFileName != null && mDocView != null) {
+ outState.putString("FileName", mFileName);
+
+ // Store current page in the prefs against the file name,
+ // so that we can pick it up each time the file is loaded
+ // Other info is needed only for screen-orientation change,
+ // so it can go in the bundle
+ SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex());
+ edit.commit();
+ }
+
+ if (!mButtonsVisible)
+ outState.putBoolean("ButtonsHidden", true);
+
+ if (mTopBarIsSearch)
+ outState.putBoolean("SearchMode", true);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mFileName != null && mDocView != null) {
+ SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
+ SharedPreferences.Editor edit = prefs.edit();
+ edit.putInt("page"+mFileName, mDocView.getDisplayedViewIndex());
+ edit.commit();
+ }
+ }
+
public void onDestroy()
{
if (core != null)
@@ -134,29 +386,126 @@ public class MuPDFActivity extends Activity
super.onDestroy();
}
- private class MyButtonHandler implements OnClickListener
- {
- Button buttonStart;
- Button buttonPrev;
- Button buttonNext;
- Button buttonEnd;
- PixmapView pixmapView;
+ void showButtons() {
+ if (!mButtonsVisible) {
+ mButtonsVisible = true;
+ // Update page number text and slider
+ int index = mDocView.getDisplayedViewIndex();
+ updatePageNumView(index);
+ mPageSlider.setMax(core.countPages()-1);
+ mPageSlider.setProgress(index);
+ if (mTopBarIsSearch) {
+ mSearchText.requestFocus();
+ showKeyboard();
+ }
+ mButtonsView.showNext();
+ }
+ }
- public MyButtonHandler(PixmapView pixmapView)
- {
- this.pixmapView = pixmapView;
+ void hideButtons() {
+ if (mButtonsVisible) {
+ mButtonsVisible = false;
+ hideKeyboard();
+ mButtonsView.showPrevious();
}
+ }
- public void onClick(View v)
- {
- if (v == buttonStart)
- pixmapView.changePage(Integer.MIN_VALUE);
- else if (v == buttonPrev)
- pixmapView.changePage(-1);
- else if (v == buttonNext)
- pixmapView.changePage(+1);
- else if (v == buttonEnd)
- pixmapView.changePage(Integer.MAX_VALUE);
+ void searchModeOn() {
+ mTopBarIsSearch = true;
+ //Focus on EditTextWidget
+ mSearchText.requestFocus();
+ showKeyboard();
+ mTopBarSwitcher.showNext();
+ }
+
+ void searchModeOff() {
+ mTopBarIsSearch = false;
+ hideKeyboard();
+ mTopBarSwitcher.showPrevious();
+ mSearchTaskResult = null;
+ // Make the ReaderView act on the change to mSearchTaskResult
+ // via overridden onChildSetup method.
+ mDocView.resetupChildren();
+ }
+
+ void updatePageNumView(int index) {
+ mPageNumberView.setText(String.format("%d/%d", index+1, core.countPages()));
+ }
+
+ void makeButtonsView() {
+ // mButtonsView is a ViewAnimator between an initial dummy empty view
+ // and the actual control view. showNext and showPrevious can be used
+ // to fade it in an out
+ mButtonsView = (ViewAnimator)getLayoutInflater().inflate(R.layout.buttons,null);
+ mFilenameView = (TextView)mButtonsView.findViewById(R.id.docNameText);
+ mPageSlider = (SeekBar)mButtonsView.findViewById(R.id.pageSlider);
+ mPageNumberView = (TextView)mButtonsView.findViewById(R.id.pageNumber);
+ mSearchButton = (ImageButton)mButtonsView.findViewById(R.id.searchButton);
+ mCancelButton = (Button)mButtonsView.findViewById(R.id.cancel);
+ mOutlineButton = (ImageButton)mButtonsView.findViewById(R.id.outlineButton);
+ mTopBarSwitcher = (ViewSwitcher)mButtonsView.findViewById(R.id.switcher);
+ mSearchBack = (ImageButton)mButtonsView.findViewById(R.id.searchBack);
+ mSearchFwd = (ImageButton)mButtonsView.findViewById(R.id.searchForward);
+ mSearchText = (EditText)mButtonsView.findViewById(R.id.searchText);
+ }
+
+ void showKeyboard() {
+ InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null)
+ imm.showSoftInput(mSearchText, 0);
+ }
+
+ void hideKeyboard() {
+ InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null)
+ imm.hideSoftInputFromWindow(mSearchText.getWindowToken(), 0);
+ }
+
+ void search(int direction) {
+ if (mSearchTask != null) {
+ mSearchTask.cancel(true);
+ mSearchTask = null;
}
+
+ mSearchTask = new AsyncTask<Integer,Void,SearchTaskResult>() {
+ @Override
+ protected SearchTaskResult doInBackground(Integer... params) {
+ int index;
+ if (mSearchTaskResult == null)
+ index = mDocView.getDisplayedViewIndex();
+ else
+ index = mSearchTaskResult.pageNumber + params[0].intValue();
+
+ while (0 <= index && index < core.countPages()) {
+ RectF searchHits[] = core.searchPage(index, mSearchText.getText().toString());
+
+ if (searchHits != null && searchHits.length > 0)
+ return new SearchTaskResult(index, searchHits);
+
+ index += params[0].intValue();
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(SearchTaskResult result) {
+ if (result != null) {
+ // Ask the ReaderView to move to the resulting page
+ mDocView.setDisplayedViewIndex(result.pageNumber);
+ mSearchTaskResult = result;
+ // Make the ReaderView act on the change to mSearchTaskResult
+ // via overridden onChildSetup method.
+ mDocView.resetupChildren();
+ } else {
+ mAlertBuilder.setTitle("Text not found");
+ AlertDialog alert = mAlertBuilder.create();
+ alert.setButton(AlertDialog.BUTTON_POSITIVE, "Dismiss",
+ (DialogInterface.OnClickListener)null);
+ alert.show();
+ }
+ }
+ };
+
+ mSearchTask.execute(new Integer(direction));
}
}
diff --git a/android/src/com/artifex/mupdf/MuPDFCore.java b/android/src/com/artifex/mupdf/MuPDFCore.java
index 321b4a64..fbdd3afa 100644
--- a/android/src/com/artifex/mupdf/MuPDFCore.java
+++ b/android/src/com/artifex/mupdf/MuPDFCore.java
@@ -1,5 +1,7 @@
package com.artifex.mupdf;
-import android.graphics.*;
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+import android.graphics.RectF;
public class MuPDFCore
{
@@ -9,13 +11,14 @@ public class MuPDFCore
}
/* Readable members */
- public int pageNum;
- public int numPages;
- public float pageWidth;
- public float pageHeight;
+ private int pageNum = -1;;
+ private int numPages = -1;
+ public float pageWidth;
+ public float pageHeight;
/* The native functions */
private static native int openFile(String filename);
+ private static native int countPagesInternal();
private static native void gotoPageInternal(int localActionPageNum);
private static native float getPageWidth();
private static native float getPageHeight();
@@ -23,16 +26,32 @@ public class MuPDFCore
int pageW, int pageH,
int patchX, int patchY,
int patchW, int patchH);
+ public static native RectF[] searchPage(String text);
+ public static native int getPageLink(int page, float x, float y);
+ public static native OutlineItem [] getOutlineInternal();
+ public static native boolean hasOutlineInternal();
+ public static native boolean needsPasswordInternal();
+ public static native boolean authenticatePasswordInternal(String password);
public static native void destroying();
public MuPDFCore(String filename) throws Exception
{
- numPages = openFile(filename);
- if (numPages <= 0)
+ if (openFile(filename) <= 0)
{
throw new Exception("Failed to open "+filename);
}
- pageNum = 0;
+ }
+
+ public int countPages()
+ {
+ if (numPages < 0)
+ numPages = countPagesSynchronized();
+
+ return numPages;
+ }
+
+ private synchronized int countPagesSynchronized() {
+ return countPagesInternal();
}
/* Shim function */
@@ -42,13 +61,53 @@ public class MuPDFCore
page = numPages-1;
else if (page < 0)
page = 0;
+ if (this.pageNum == page)
+ return;
gotoPageInternal(page);
this.pageNum = page;
this.pageWidth = getPageWidth();
this.pageHeight = getPageHeight();
}
- public void onDestroy() {
+ public synchronized PointF getPageSize(int page) {
+ gotoPage(page);
+ return new PointF(pageWidth, pageHeight);
+ }
+
+ public synchronized void onDestroy() {
destroying();
}
+
+ public synchronized void drawPage(int page, Bitmap bitmap,
+ int pageW, int pageH,
+ int patchX, int patchY,
+ int patchW, int patchH) {
+ gotoPage(page);
+ drawPage(bitmap, pageW, pageH, patchX, patchY, patchW, patchH);
+ }
+
+ public synchronized int hitLinkPage(int page, float x, float y) {
+ return getPageLink(page, x, y);
+ }
+
+ public synchronized RectF [] searchPage(int page, String text) {
+ gotoPage(page);
+ return searchPage(text);
+ }
+
+ public synchronized boolean hasOutline() {
+ return hasOutlineInternal();
+ }
+
+ public synchronized OutlineItem [] getOutline() {
+ return getOutlineInternal();
+ }
+
+ public synchronized boolean needsPassword() {
+ return needsPasswordInternal();
+ }
+
+ public synchronized boolean authenticatePassword(String password) {
+ return authenticatePasswordInternal(password);
+ }
}
diff --git a/android/src/com/artifex/mupdf/MuPDFPageAdapter.java b/android/src/com/artifex/mupdf/MuPDFPageAdapter.java
new file mode 100644
index 00000000..91b30dce
--- /dev/null
+++ b/android/src/com/artifex/mupdf/MuPDFPageAdapter.java
@@ -0,0 +1,72 @@
+package com.artifex.mupdf;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.os.AsyncTask;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+public class MuPDFPageAdapter extends BaseAdapter {
+ private final Context mContext;
+ private final MuPDFCore mCore;
+ private final SparseArray<PointF> mPageSizes = new SparseArray<PointF>();
+
+ public MuPDFPageAdapter(Context c, MuPDFCore core) {
+ mContext = c;
+ mCore = core;
+ }
+
+ public int getCount() {
+ return mCore.countPages();
+ }
+
+ public Object getItem(int position) {
+ return null;
+ }
+
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ final MuPDFPageView pageView;
+ if (convertView == null) {
+ pageView = new MuPDFPageView(mContext, mCore, new Point(parent.getWidth(), parent.getHeight()));
+ } else {
+ pageView = (MuPDFPageView) convertView;
+ }
+
+ PointF pageSize = mPageSizes.get(position);
+ if (pageSize != null) {
+ // We already know the page size. Set it up
+ // immediately
+ pageView.setPage(position, pageSize);
+ } else {
+ // Page size as yet unknown. Blank it for now, and
+ // start a background task to find the size
+ pageView.blank(position);
+ AsyncTask<Void,Void,PointF> sizingTask = new AsyncTask<Void,Void,PointF>() {
+ @Override
+ protected PointF doInBackground(Void... arg0) {
+ return mCore.getPageSize(position);
+ }
+
+ @Override
+ protected void onPostExecute(PointF result) {
+ super.onPostExecute(result);
+ // We now know the page size
+ mPageSizes.put(position, result);
+ // Check that this view hasn't been reused for
+ // another page since we started
+ if (pageView.getPage() == position)
+ pageView.setPage(position, result);
+ }
+ };
+ sizingTask.execute((Void)null);
+ }
+ return pageView;
+ }
+}
diff --git a/android/src/com/artifex/mupdf/MuPDFPageView.java b/android/src/com/artifex/mupdf/MuPDFPageView.java
new file mode 100644
index 00000000..fc5de6ba
--- /dev/null
+++ b/android/src/com/artifex/mupdf/MuPDFPageView.java
@@ -0,0 +1,28 @@
+package com.artifex.mupdf;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+
+public class MuPDFPageView extends PageView {
+ private final MuPDFCore mCore;
+
+ public MuPDFPageView(Context c, MuPDFCore core, Point parentSize) {
+ super(c, parentSize);
+ mCore = core;
+ }
+
+ public int hitLinkPage(float x, float y) {
+ float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ float docRelX = (x - getLeft())/scale;
+ float docRelY = (y - getTop())/scale;
+
+ return mCore.hitLinkPage(mPageNumber, docRelX, docRelY);
+ }
+
+ @Override
+ protected void drawPage(Bitmap bm, int sizeX, int sizeY,
+ int patchX, int patchY, int patchWidth, int patchHeight) {
+ mCore.drawPage(mPageNumber, bm, sizeX, sizeY, patchX, patchY, patchWidth, patchHeight);
+ }
+}
diff --git a/android/src/com/artifex/mupdf/OutlineActivity.java b/android/src/com/artifex/mupdf/OutlineActivity.java
new file mode 100644
index 00000000..dfb10639
--- /dev/null
+++ b/android/src/com/artifex/mupdf/OutlineActivity.java
@@ -0,0 +1,29 @@
+package com.artifex.mupdf;
+
+import android.app.ListActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ListView;
+
+public class OutlineActivity extends ListActivity {
+ OutlineItem mItems[];
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mItems = OutlineActivityData.get().items;
+ setListAdapter(new OutlineAdapter(getLayoutInflater(),mItems));
+ // Restore the position within the list from last viewing
+ getListView().setSelection(OutlineActivityData.get().position);
+ setResult(-1);
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+ OutlineActivityData.get().position = getListView().getFirstVisiblePosition();
+ setResult(mItems[position].page);
+ finish();
+ }
+}
diff --git a/android/src/com/artifex/mupdf/OutlineActivityData.java b/android/src/com/artifex/mupdf/OutlineActivityData.java
new file mode 100644
index 00000000..abf60eba
--- /dev/null
+++ b/android/src/com/artifex/mupdf/OutlineActivityData.java
@@ -0,0 +1,17 @@
+package com.artifex.mupdf;
+
+public class OutlineActivityData {
+ public OutlineItem items[];
+ public int position;
+ static private OutlineActivityData singleton;
+
+ static public void set(OutlineActivityData d) {
+ singleton = d;
+ }
+
+ static public OutlineActivityData get() {
+ if (singleton == null)
+ singleton = new OutlineActivityData();
+ return singleton;
+ }
+}
diff --git a/android/src/com/artifex/mupdf/OutlineAdapter.java b/android/src/com/artifex/mupdf/OutlineAdapter.java
new file mode 100644
index 00000000..ad1d6621
--- /dev/null
+++ b/android/src/com/artifex/mupdf/OutlineAdapter.java
@@ -0,0 +1,46 @@
+package com.artifex.mupdf;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+public class OutlineAdapter extends BaseAdapter {
+ private final OutlineItem mItems[];
+ private final LayoutInflater mInflater;
+ public OutlineAdapter(LayoutInflater inflater, OutlineItem items[]) {
+ mInflater = inflater;
+ mItems = items;
+ }
+
+ public int getCount() {
+ return mItems.length;
+ }
+
+ public Object getItem(int arg0) {
+ return null;
+ }
+
+ public long getItemId(int arg0) {
+ return 0;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v;
+ if (convertView == null) {
+ v = mInflater.inflate(R.layout.outline_entry, null);
+ } else {
+ v = convertView;
+ }
+ int level = mItems[position].level;
+ if (level > 8) level = 8;
+ String space = "";
+ for (int i=0; i<level;i++)
+ space += " ";
+ ((TextView)v.findViewById(R.id.title)).setText(space+mItems[position].title);
+ ((TextView)v.findViewById(R.id.page)).setText(String.valueOf(mItems[position].page+1));
+ return v;
+ }
+
+}
diff --git a/android/src/com/artifex/mupdf/OutlineItem.java b/android/src/com/artifex/mupdf/OutlineItem.java
new file mode 100644
index 00000000..63619a03
--- /dev/null
+++ b/android/src/com/artifex/mupdf/OutlineItem.java
@@ -0,0 +1,14 @@
+package com.artifex.mupdf;
+
+public class OutlineItem {
+ public final int level;
+ public final String title;
+ public final int page;
+
+ OutlineItem(int _level, String _title, int _page) {
+ level = _level;
+ title = _title;
+ page = _page;
+ }
+
+}
diff --git a/android/src/com/artifex/mupdf/PageView.java b/android/src/com/artifex/mupdf/PageView.java
new file mode 100644
index 00000000..6fe6d43d
--- /dev/null
+++ b/android/src/com/artifex/mupdf/PageView.java
@@ -0,0 +1,323 @@
+package com.artifex.mupdf;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.AsyncTask;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+
+class PatchInfo {
+ public Bitmap bm;
+ public Point patchViewSize;
+ public Rect patchArea;
+
+ public PatchInfo(Bitmap aBm, Point aPatchViewSize, Rect aPatchArea) {
+ bm = aBm;
+ patchViewSize = aPatchViewSize;
+ patchArea = aPatchArea;
+ }
+}
+
+// Make our ImageViews opaque to optimize redraw
+class OpaqueImageView extends ImageView {
+
+ public OpaqueImageView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return true;
+ }
+}
+
+public abstract class PageView extends ViewGroup {
+ private static final int HIGHLIGHT_COLOR = 0x805555FF;
+ private static final int BACKGROUND_COLOR = 0xFFFFFFFF;
+ private 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 AsyncTask<Void,Void,Void> mDrawEntire;
+
+ private Point mPatchViewSize; // View size on the basis of which the patch was created
+ private Rect mPatchArea;
+ private ImageView mPatch;
+ private AsyncTask<PatchInfo,Void,PatchInfo> mDrawPatch;
+ private RectF mSearchBoxes[];
+ private View mSearchView;
+ private boolean mIsBlank;
+
+ private ProgressBar mBusyIndicator;
+
+ public PageView(Context c, Point parentSize) {
+ super(c);
+ mContext = c;
+ mParentSize = parentSize;
+ setBackgroundColor(BACKGROUND_COLOR);
+ }
+
+ protected abstract void drawPage(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight);
+
+ public void blank(int page) {
+ // Cancel pending render task
+ if (mDrawEntire != null) {
+ mDrawEntire.cancel(true);
+ mDrawEntire = null;
+ }
+
+ mIsBlank = true;
+ mPageNumber = page;
+
+ if (mSize == null)
+ mSize = mParentSize;
+
+ if (mEntire != null)
+ mEntire.setImageBitmap(null);
+
+ if (mPatch != null)
+ mPatch.setImageBitmap(null);
+
+ if (mBusyIndicator == null) {
+ mBusyIndicator = new ProgressBar(mContext);
+ mBusyIndicator.setIndeterminate(true);
+ mBusyIndicator.setBackgroundResource(R.drawable.busy);
+ addView(mBusyIndicator);
+ }
+ }
+
+ public void setPage(int page, PointF size) {
+ // Cancel pending render task
+ if (mDrawEntire != null) {
+ mDrawEntire.cancel(true);
+ mDrawEntire = null;
+ }
+
+ mIsBlank = false;
+
+ mPageNumber = page;
+ if (mEntire == null) {
+ mEntire = new OpaqueImageView(mContext);
+ mEntire.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ 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;
+ if (mEntireBm == null || mEntireBm.getWidth() != newSize.x
+ || mEntireBm.getHeight() != newSize.y) {
+ mEntireBm = Bitmap.createBitmap(mSize.x, mSize.y, Bitmap.Config.ARGB_8888);
+ }
+
+ // Render the page in the background
+ mDrawEntire = new AsyncTask<Void,Void,Void>() {
+ protected Void doInBackground(Void... v) {
+ drawPage(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y);
+ return null;
+ }
+
+ protected void onPreExecute() {
+ mEntire.setImageBitmap(null);
+
+ if (mBusyIndicator == null) {
+ mBusyIndicator = new ProgressBar(mContext);
+ mBusyIndicator.setIndeterminate(true);
+ mBusyIndicator.setBackgroundResource(R.drawable.busy);
+ addView(mBusyIndicator);
+ }
+ }
+
+ protected void onPostExecute(Void v) {
+ removeView(mBusyIndicator);
+ mBusyIndicator = null;
+ mEntire.setImageBitmap(mEntireBm);
+ invalidate();
+ }
+ };
+
+ mDrawEntire.execute();
+
+ if (mSearchView == null) {
+ mSearchView = new View(mContext) {
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (!mIsBlank && mSearchBoxes != null) {
+ // Work out current total scale factor
+ // from source to view
+ float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
+ Paint paint = new Paint();
+ paint.setColor(HIGHLIGHT_COLOR);
+ for (RectF rect : mSearchBoxes)
+ canvas.drawRect(new RectF(rect.left * scale,
+ rect.top * scale, rect.right * scale,
+ rect.bottom * scale), paint);
+ }
+ }
+ };
+
+ addView(mSearchView);
+ }
+ requestLayout();
+ }
+
+ public void setSearchBoxes(RectF searchBoxes[]) {
+ mSearchBoxes = searchBoxes;
+ }
+
+ @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) {
+ 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);
+ } 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 addHq() {
+ Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom());
+ // If the viewArea's size matches the unzoomed size, there is no need for an hq patch
+ if (viewArea.width() != mSize.x || viewArea.height() != mSize.y) {
+ Point patchViewSize = new Point(viewArea.width(), viewArea.height());
+ 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);
+
+ // If being asked for the same area as last time, nothing to do
+ if (patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize))
+ return;
+
+ // Stop the drawing of previous patch if still going
+ if (mDrawPatch != null) {
+ mDrawPatch.cancel(true);
+ mDrawPatch = null;
+ }
+
+ // Create and add the image view if not already done
+ if (mPatch == null) {
+ mPatch = new OpaqueImageView(mContext);
+ mPatch.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ addView(mPatch);
+ mSearchView.bringToFront();
+ }
+
+ Bitmap bm = Bitmap.createBitmap(patchArea.width(), patchArea.height(), Bitmap.Config.ARGB_8888);
+
+ mDrawPatch = new AsyncTask<PatchInfo,Void,PatchInfo>() {
+ protected PatchInfo doInBackground(PatchInfo... v) {
+ drawPage(v[0].bm, v[0].patchViewSize.x, v[0].patchViewSize.y,
+ v[0].patchArea.left, v[0].patchArea.top,
+ v[0].patchArea.width(), v[0].patchArea.height());
+ return v[0];
+ }
+
+ protected void onPostExecute(PatchInfo v) {
+ mPatchViewSize = v.patchViewSize;
+ mPatchArea = v.patchArea;
+ mPatch.setImageBitmap(v.bm);
+ //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);
+ invalidate();
+ }
+ };
+
+ mDrawPatch.execute(new PatchInfo(bm, patchViewSize, patchArea));
+ }
+ }
+
+ public void removeHq() {
+ // Stop the drawing of the patch if still going
+ if (mDrawPatch != null) {
+ mDrawPatch.cancel(true);
+ mDrawPatch = null;
+ }
+
+ // And get rid of it
+ mPatchViewSize = null;
+ mPatchArea = null;
+ if (mPatch != null)
+ mPatch.setImageBitmap(null);
+ }
+
+ public int getPage() {
+ return mPageNumber;
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return true;
+ }
+}
diff --git a/android/src/com/artifex/mupdf/PixmapView.java b/android/src/com/artifex/mupdf/PixmapView.java
deleted file mode 100644
index 73e73eda..00000000
--- a/android/src/com/artifex/mupdf/PixmapView.java
+++ /dev/null
@@ -1,579 +0,0 @@
-package com.artifex.mupdf;
-
-import android.app.*;
-import android.os.*;
-import android.content.*;
-import android.content.res.*;
-import android.graphics.*;
-import android.util.*;
-import android.view.*;
-import android.widget.*;
-import java.net.*;
-import java.io.*;
-
-public class PixmapView extends SurfaceView implements SurfaceHolder.Callback
-{
- private SurfaceHolder holder;
- private MuPDFThread thread = null;
- private boolean threadStarted = false;
- private MuPDFCore core;
-
- /* Constructor */
- public PixmapView(Context context, MuPDFCore core)
- {
- super(context);
- System.out.println("PixmapView construct");
- this.core = core;
- holder = getHolder();
- holder.addCallback(this);
- thread = new MuPDFThread(holder, core);
- setFocusable(true); // need to get the key events
- }
-
- /* load our native library */
- static {
- System.loadLibrary("mupdf");
- }
-
- /* Handlers for keys - so we can actually do stuff */
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event)
- {
- if (thread.onKeyDown(keyCode, event))
- return true;
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event)
- {
- if (thread.onKeyUp(keyCode, event))
- return true;
- return super.onKeyUp(keyCode, event);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event)
- {
- if (thread.onTouchEvent(event))
- return true;
- return super.onTouchEvent(event);
- }
-
- public void changePage(int delta)
- {
- thread.changePage(delta);
- }
-
- /* Handlers for SurfaceHolder callbacks; these are called when the
- * surface is created/destroyed/changed. We need to ensure that we only
- * draw into the surface between the created and destroyed calls.
- * Therefore, we start/stop the thread that actually runs MuPDF on
- * creation/destruction. */
- public void surfaceCreated(SurfaceHolder holder)
- {
- }
-
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
- {
- thread.newScreenSize(width, height);
- if (!threadStarted)
- {
- threadStarted = true;
- thread.setRunning(true);
- thread.start();
- }
- }
-
- public void surfaceDestroyed(SurfaceHolder holder)
- {
- boolean retry = true;
- System.out.println("Surface destroyed 1 this="+this);
- thread.setRunning(false);
- System.out.println("Surface destroyed 2");
- while (retry)
- {
- try
- {
- thread.join();
- retry = false;
- }
- catch (InterruptedException e)
- {
- }
- }
- threadStarted = false;
- System.out.println("Surface destroyed 3");
- }
-
- class MuPDFThread extends Thread
- {
- private SurfaceHolder holder;
- private boolean running = false;
- private int keycode = -1;
- private int screenWidth;
- private int screenHeight;
- private int screenGeneration;
- private Bitmap bitmap;
- private MuPDFCore core;
-
- /* The following variables deal with the size of the current page;
- * specifically, its position on the screen, its raw size, its
- * current scale, and its current scaled size (in terms of whole
- * pixels).
- */
- private int pageOriginX;
- private int pageOriginY;
- private float pageScale;
- private int pageWidth;
- private int pageHeight;
-
- /* The following variables deal with the multitouch handling */
- private final int NONE = 0;
- private final int DRAG = 1;
- private final int ZOOM = 2;
- private int touchMode = NONE;
- private float touchInitialSpacing;
- private float touchDragStartX;
- private float touchDragStartY;
- private float touchInitialOriginX;
- private float touchInitialOriginY;
- private float touchInitialScale;
- private PointF touchZoomMidpoint;
-
- /* The following control the inner loop; other events etc cause
- * action to be set. The inner loop runs around a tight loop
- * performing the action requested of it.
- */
- private boolean wakeMe = false;
- private int action;
- private final int SLEEP = 0;
- private final int REDRAW = 1;
- private final int DIE = 2;
- private final int GOTOPAGE = 3;
- private int actionPageNum;
-
- /* Members for blitting, declared here to avoid causing gcs */
- private Rect srcRect;
- private RectF dstRect;
-
- public MuPDFThread(SurfaceHolder holder, MuPDFCore core)
- {
- this.holder = holder;
- this.core = core;
- touchZoomMidpoint = new PointF(0,0);
- srcRect = new Rect(0,0,0,0);
- dstRect = new RectF(0,0,0,0);
- }
-
- public void setRunning(boolean running)
- {
- this.running = running;
- if (!running)
- {
- System.out.println("killing 1");
- synchronized(this)
- {
- System.out.println("killing 2");
- action = DIE;
- if (wakeMe)
- {
- wakeMe = false;
- System.out.println("killing 3");
- this.notify();
- System.out.println("killing 4");
- }
- }
- }
- }
-
- public void newScreenSize(int width, int height)
- {
- this.screenWidth = width;
- this.screenHeight = height;
- this.screenGeneration++;
- }
-
- public boolean onKeyDown(int keyCode, KeyEvent msg)
- {
- keycode = keyCode;
- return false;
- }
-
- public boolean onKeyUp(int keyCode, KeyEvent msg)
- {
- return false;
- }
-
- public synchronized void changePage(int delta)
- {
- action = GOTOPAGE;
- if (delta == Integer.MIN_VALUE)
- actionPageNum = 0;
- else if (delta == Integer.MAX_VALUE)
- actionPageNum = core.numPages-1;
- else
- {
- actionPageNum += delta;
- if (actionPageNum < 0)
- actionPageNum = 0;
- if (actionPageNum > core.numPages-1)
- actionPageNum = core.numPages-1;
- }
- if (wakeMe)
- {
- wakeMe = false;
- this.notify();
- }
- }
-
- private float spacing(MotionEvent event)
- {
- float x = event.getX(0) - event.getX(1);
- float y = event.getY(0) - event.getY(1);
- return FloatMath.sqrt(x*x+y*y);
- }
-
- private void midpoint(PointF point, MotionEvent event)
- {
- float x = event.getX(0) + event.getX(1);
- float y = event.getY(0) + event.getY(1);
- point.set(x/2, y/2);
- }
-
- private synchronized void forceRedraw()
- {
- if (wakeMe)
- {
- wakeMe = false;
- this.notify();
- }
- action = REDRAW;
- }
-
- public synchronized void setPageOriginTo(int x, int y)
- {
- /* Adjust the coordinates so that the page always covers the
- * centre of the screen. */
- if (x + pageWidth < screenWidth/2)
- {
- x = screenWidth/2 - pageWidth;
- }
- else if (x > screenWidth/2)
- {
- x = screenWidth/2;
- }
- if (y + pageHeight < screenHeight/2)
- {
- y = screenHeight/2 - pageHeight;
- }
- else if (y > screenHeight/2)
- {
- y = screenHeight/2;
- }
- if ((x != pageOriginX) || (y != pageOriginY))
- {
- pageOriginX = x;
- pageOriginY = y;
- }
- forceRedraw();
- }
-
- public void setPageScaleTo(float scale, PointF midpoint)
- {
- float x, y;
- /* Convert midpoint (in screen coords) to page coords */
- x = (midpoint.x - pageOriginX)/pageScale;
- y = (midpoint.y - pageOriginY)/pageScale;
- /* Find new scaled page sizes */
- synchronized(this)
- {
- pageWidth = (int)(core.pageWidth*scale+0.5);
- if (pageWidth < screenWidth/2)
- {
- scale = screenWidth/2/core.pageWidth;
- pageWidth = (int)(core.pageWidth*scale+0.5);
- }
- pageHeight = (int)(core.pageHeight*scale+0.5);
- if (pageHeight < screenHeight/2)
- {
- scale = screenHeight/2/core.pageHeight;
- pageWidth = (int)(core.pageWidth *scale+0.5);
- pageHeight = (int)(core.pageHeight*scale+0.5);
- }
- pageScale = scale;
- /* Now given this new scale, calculate page origins so that
- * x and y are at midpoint */
- float xscale = (float)pageWidth /core.pageWidth;
- float yscale = (float)pageHeight/core.pageHeight;
- setPageOriginTo((int)(midpoint.x - x*xscale + 0.5),
- (int)(midpoint.y - y*yscale + 0.5));
- }
- }
-
- public void scalePageToScreen()
- {
- float scaleX, scaleY;
- scaleX = (float)screenWidth /core.pageWidth;
- scaleY = (float)screenHeight/core.pageHeight;
- synchronized(this)
- {
- if (scaleX < scaleY)
- pageScale = scaleX;
- else
- pageScale = scaleY;
- pageWidth = (int)(core.pageWidth * pageScale + 0.5);
- pageHeight = (int)(core.pageHeight * pageScale + 0.5);
- pageOriginX = (screenWidth - pageWidth)/2;
- pageOriginY = (screenHeight - pageHeight)/2;
- forceRedraw();
- }
- System.out.println("scalePageToScreen: Raw="+
- core.pageWidth+"x"+core.pageHeight+" scaled="+
- pageWidth+","+pageHeight+" pageScale="+
- pageScale);
- }
-
- public boolean onTouchEvent(MotionEvent event)
- {
- int action = event.getAction();
- boolean done = false;
- switch (action & MotionEvent.ACTION_MASK)
- {
- case MotionEvent.ACTION_DOWN:
- touchMode = DRAG;
- touchDragStartX = event.getX();
- touchDragStartY = event.getY();
- touchInitialOriginX = pageOriginX;
- touchInitialOriginY = pageOriginY;
- System.out.println("Starting dragging from: "+touchDragStartX+","+touchDragStartY+" ("+pageOriginX+","+pageOriginY+")");
- done = true;
- break;
- case MotionEvent.ACTION_POINTER_DOWN:
- touchInitialSpacing = spacing(event);
- if (touchInitialSpacing > 10f)
- {
- System.out.println("Started zooming: spacing="+touchInitialSpacing);
- touchInitialScale = pageScale;
- touchMode = ZOOM;
- done = true;
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_POINTER_UP:
- if (touchMode != NONE)
- {
- System.out.println("Released!");
- touchMode = NONE;
- done = true;
- }
- break;
- case MotionEvent.ACTION_MOVE:
- if (touchMode == DRAG)
- {
- float x = touchInitialOriginX+event.getX()-touchDragStartX;
- float y = touchInitialOriginY+event.getY()-touchDragStartY;
- System.out.println("Dragged to "+x+","+y);
- setPageOriginTo((int)(x+0.5),(int)(y+0.5));
- done = true;
- }
- else if (touchMode == ZOOM)
- {
- float newSpacing = spacing(event);
- if (newSpacing > 10f)
- {
- float newScale = touchInitialScale*newSpacing/touchInitialSpacing;
- System.out.println("Zoomed to "+newSpacing);
- midpoint(touchZoomMidpoint,event);
- setPageScaleTo(newScale,touchZoomMidpoint);
- done = true;
- }
- }
- }
- return done;
- }
-
- public void run()
- {
- boolean redraw = false;
- int patchW = 0;
- int patchH = 0;
- int patchX = 0;
- int patchY = 0;
- int localPageW = 0;
- int localPageH = 0;
- int localScreenGeneration = screenGeneration;
- int localAction;
- int localActionPageNum = core.pageNum;
-
- /* Set up our default action */
- action = GOTOPAGE;
- actionPageNum = core.pageNum;
- while (action != DIE)
- {
- synchronized(this)
- {
- while (action == SLEEP)
- {
- wakeMe = true;
- try
- {
- System.out.println("Render thread sleeping");
- this.wait();
- System.out.println("Render thread woken");
- }
- catch (java.lang.InterruptedException e)
- {
- System.out.println("Render thread exception:"+e);
- }
- }
-
- /* Now we do as little as we can get away with while
- * synchronised. In general this means copying any action
- * or global variables into local ones so that when we
- * unsynchronoise, other people can alter them again.
- */
- switch (action)
- {
- case DIE:
- System.out.println("Woken to die!");
- break;
- case GOTOPAGE:
- localActionPageNum = actionPageNum;
- break;
- case REDRAW:
- /* Figure out what area of the page we want to
- * redraw (in local variables, in docspace).
- * We'll always draw a screensized lump, unless
- * that's too big. */
- System.out.println("page="+pageWidth+","+pageHeight+" ("+core.pageWidth+","+core.pageHeight+"@"+pageScale+") @ "+pageOriginX+","+pageOriginY);
- localPageW = pageWidth;
- localPageH = pageHeight;
- patchW = pageWidth;
- patchH = pageHeight;
- patchX = -pageOriginX;
- patchY = -pageOriginY;
- if (patchX < 0)
- patchX = 0;
- if (patchW > screenWidth)
- patchW = screenWidth;
- srcRect.left = 0;
- if (patchX+patchW > pageWidth)
- {
- srcRect.left += patchX+patchW-pageWidth;
- patchX = pageWidth-patchW;
- }
- if (patchY < 0)
- patchY = 0;
- if (patchH > screenHeight)
- patchH = screenHeight;
- srcRect.top = 0;
- if (patchY+patchH > pageHeight)
- {
- srcRect.top += patchY+patchH-pageHeight;
- patchY = pageHeight-patchH;
- }
- dstRect.left = pageOriginX;
- if (dstRect.left < 0)
- dstRect.left = 0;
- dstRect.top = pageOriginY;
- if (dstRect.top < 0)
- dstRect.top = 0;
- dstRect.right = dstRect.left + patchW;
- srcRect.right = srcRect.left + patchW;
- if (srcRect.right > screenWidth)
- {
- dstRect.right -= srcRect.right-screenWidth;
- srcRect.right = screenWidth;
- }
- if (dstRect.right > screenWidth)
- {
- srcRect.right -= dstRect.right-screenWidth;
- dstRect.right = screenWidth;
- }
- dstRect.bottom = dstRect.top + patchH;
- srcRect.bottom = srcRect.top + patchH;
- if (srcRect.bottom > screenHeight)
- {
- dstRect.bottom -=srcRect.bottom-screenHeight;
- srcRect.bottom = screenHeight;
- }
- if (dstRect.bottom > screenHeight)
- {
- srcRect.bottom -=dstRect.bottom-screenHeight;
- dstRect.bottom = screenHeight;
- }
- System.out.println("patch=["+patchX+","+patchY+","+patchW+","+patchH+"]");
- break;
- }
- localAction = action;
- if (action != DIE)
- action = SLEEP;
- }
-
- /* In the redraw case:
- * pW, pH, pX, pY, localPageW, localPageH are now all set
- * in local variables, and we are safe from the global vars
- * being altered in calls from other threads. This is all
- * the information we need to actually do our render.
- */
- switch (localAction)
- {
- case GOTOPAGE:
- core.gotoPage(localActionPageNum);
- scalePageToScreen();
- action = REDRAW;
- break;
- case REDRAW:
- if ((bitmap == null) ||
- (bitmap.getWidth() != patchW) ||
- (bitmap.getHeight() != patchH))
- {
- /* make bitmap of required size */
- bitmap = Bitmap.createBitmap(patchW, patchH, Bitmap.Config.ARGB_8888);
- }
- System.out.println("Calling redraw native method");
- core.drawPage(bitmap, localPageW, localPageH, patchX, patchY, patchW, patchH);
- System.out.println("Called native method");
- {
- Canvas c = null;
- try
- {
- c = holder.lockCanvas(null);
- synchronized(holder)
- {
- if (localScreenGeneration == screenGeneration)
- {
- doDraw(c);
- }
- else
- {
- /* Someone has changed the screen
- * under us! Better redraw again...
- */
- action = REDRAW;
- }
- }
- }
- finally
- {
- if (c != null)
- holder.unlockCanvasAndPost(c);
- }
- }
- }
- }
- }
-
- protected void doDraw(Canvas canvas)
- {
- if ((canvas == null) || (bitmap == null))
- return;
- /* Clear the screen */
- canvas.drawRGB(128,128,128);
- /* Draw our bitmap on top */
- System.out.println("Blitting bitmap from "+srcRect.left+","+srcRect.top+","+srcRect.right+","+srcRect.bottom+" to "+dstRect.left+","+dstRect.top+","+dstRect.right+","+dstRect.bottom);
- canvas.drawBitmap(bitmap, srcRect, dstRect, (Paint)null);
- }
- }
-}
diff --git a/android/src/com/artifex/mupdf/ReaderView.java b/android/src/com/artifex/mupdf/ReaderView.java
new file mode 100644
index 00000000..2715b55d
--- /dev/null
+++ b/android/src/com/artifex/mupdf/ReaderView.java
@@ -0,0 +1,554 @@
+package com.artifex.mupdf;
+
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.Scroller;
+
+public class ReaderView extends AdapterView<Adapter>
+ implements GestureDetector.OnGestureListener,
+ ScaleGestureDetector.OnScaleGestureListener,
+ Runnable {
+ private static final int MOVING_DIAGONALLY = 0;
+ private static final int MOVING_LEFT = 1;
+ private static final int MOVING_RIGHT = 2;
+ private static final int MOVING_UP = 3;
+ private static final int MOVING_DOWN = 4;
+
+ private static final int FLING_MARGIN = 100;
+ private static final int GAP = 20;
+ private static final int SCROLL_SPEED = 2;
+
+ private static final float MIN_SCALE = 1.0f;
+ private static final float MAX_SCALE = 5.0f;
+
+ private Adapter mAdapter;
+ private int mCurrent; // Adapter's index for the current view
+ private boolean mResetLayout;
+ private final SparseArray<View>
+ mChildViews = new SparseArray<View>(3);
+ // Shadows the children of the adapter view
+ // but with more sensible indexing
+ private final LinkedList<View>
+ mViewCache = new LinkedList<View>();
+ private boolean mUserInteracting; // Whether the user is interacting
+ private boolean mScaling; // Whether the user is currently pinch zooming
+ private float mScale = 1.0f;
+ private int mXScroll; // Scroll amounts recorded from events.
+ private int mYScroll; // and then accounted for in onLayout
+ private final GestureDetector
+ mGestureDetector;
+ private final ScaleGestureDetector
+ mScaleGestureDetector;
+ private final Scroller mScroller;
+ private int mScrollerLastX;
+ private int mScrollerLastY;
+ private boolean mScrollDisabled;
+
+ public ReaderView(Context context) {
+ super(context);
+ mGestureDetector = new GestureDetector(this);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mScroller = new Scroller(context);
+ }
+
+ public ReaderView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mGestureDetector = new GestureDetector(this);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mScroller = new Scroller(context);
+ }
+
+ public ReaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mGestureDetector = new GestureDetector(this);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mScroller = new Scroller(context);
+ }
+
+ public int getDisplayedViewIndex() {
+ return mCurrent;
+ }
+
+ public void setDisplayedViewIndex(int i) {
+ if (0 <= i && i < mAdapter.getCount()) {
+ mCurrent = i;
+ onMoveToChild(i);
+ mResetLayout = true;
+ requestLayout();
+ }
+ }
+
+ public void resetupChildren() {
+ for (int i = 0; i < mChildViews.size(); i++)
+ onChildSetup(mChildViews.keyAt(i), mChildViews.valueAt(i));
+ }
+
+ protected void onChildSetup(int i, View v) {}
+
+ protected void onMoveToChild(int i) {}
+
+ protected void onSettle(View v) {};
+
+ protected void onUnsettle(View v) {};
+
+ public View getDisplayedView() {
+ return mChildViews.get(mCurrent);
+ }
+
+ public void run() {
+ if (!mScroller.isFinished()) {
+ mScroller.computeScrollOffset();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+ mXScroll += x - mScrollerLastX;
+ mYScroll += y - mScrollerLastY;
+ mScrollerLastX = x;
+ mScrollerLastY = y;
+ requestLayout();
+ post(this);
+ }
+ else if (!mUserInteracting) {
+ // End of an inertial scroll and the user is not interacting.
+ // The layout is stable
+ View v = mChildViews.get(mCurrent);
+ postSettle(v);
+ }
+ }
+
+ public boolean onDown(MotionEvent arg0) {
+ mScroller.forceFinished(true);
+ return true;
+ }
+
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ if (mScrollDisabled)
+ return true;
+
+ View v = mChildViews.get(mCurrent);
+ if (v != null) {
+ Rect bounds = getScrollBounds(v);
+ switch(directionOfTravel(velocityX, velocityY)) {
+ case MOVING_LEFT:
+ if (bounds.left >= 0) {
+ // Fling off to the left bring next view onto screen
+ View vl = mChildViews.get(mCurrent+1);
+
+ if (vl != null) {
+ slideViewOntoScreen(vl);
+ return true;
+ }
+ }
+ break;
+ case MOVING_RIGHT:
+ if (bounds.right <= 0) {
+ // Fling off to the right bring previous view onto screen
+ View vr = mChildViews.get(mCurrent-1);
+
+ if (vr != null) {
+ slideViewOntoScreen(vr);
+ return true;
+ }
+ }
+ break;
+ }
+ mScrollerLastX = mScrollerLastY = 0;
+ // If the page has been dragged out of bounds then we want to spring back
+ // nicely. fling jumps back into bounds instantly, so we don't want to use
+ // fling in that case. On the other hand, we don't want to forgo a fling
+ // just because of a slightly off-angle drag taking us out of bounds other
+ // than in the direction of the drag, so we test for out of bounds only
+ // in the direction of travel.
+ //
+ // Also don't fling if out of bounds in any direction by more than fling
+ // margin
+ Rect expandedBounds = new Rect(bounds);
+ expandedBounds.inset(-FLING_MARGIN, -FLING_MARGIN);
+
+ if(withinBoundsInDirectionOfTravel(bounds, velocityX, velocityY)
+ && expandedBounds.contains(0, 0)) {
+ mScroller.fling(0, 0, (int)velocityX, (int)velocityY, bounds.left, bounds.right, bounds.top, bounds.bottom);
+ post(this);
+ }
+ }
+
+ return true;
+ }
+
+ public void onLongPress(MotionEvent e) {
+ }
+
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ if (!mScrollDisabled) {
+ mXScroll -= distanceX;
+ mYScroll -= distanceY;
+ requestLayout();
+ }
+ return true;
+ }
+
+ public void onShowPress(MotionEvent e) {
+ }
+
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+
+ public boolean onScale(ScaleGestureDetector detector) {
+ float previousScale = mScale;
+ mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), MIN_SCALE), MAX_SCALE);
+ float factor = mScale/previousScale;
+
+ View v = mChildViews.get(mCurrent);
+ if (v != null) {
+ // Work out the focus point relative to the view top left
+ int viewFocusX = (int)detector.getFocusX() - (v.getLeft() + mXScroll);
+ int viewFocusY = (int)detector.getFocusY() - (v.getTop() + mYScroll);
+ // Scroll to maintain the focus point
+ mXScroll += viewFocusX - viewFocusX * factor;
+ mYScroll += viewFocusY - viewFocusY * factor;
+ requestLayout();
+ }
+ return true;
+ }
+
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ mScaling = true;
+ // Ignore any scroll amounts yet to be accounted for: the
+ // screen is not showing the effect of them, so they can
+ // only confuse the user
+ mXScroll = mYScroll = 0;
+ // Avoid jump at end of scaling by disabling scrolling
+ // until the next start of gesture
+ mScrollDisabled = true;
+ return true;
+ }
+
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mScaling = false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ mScaleGestureDetector.onTouchEvent(event);
+
+ if (!mScaling)
+ mGestureDetector.onTouchEvent(event);
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mUserInteracting = true;
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mScrollDisabled = false;
+ mUserInteracting = false;
+
+ View v = mChildViews.get(mCurrent);
+ if (v != null) {
+ if (mScroller.isFinished()) {
+ // If, at the end of user interaction, there is no
+ // current inertial scroll in operation then animate
+ // the view onto screen if necessary
+ slideViewOntoScreen(v);
+ }
+
+ if (mScroller.isFinished()) {
+ // If still there is no inertial scroll in operation
+ // then the layout is stable
+ postSettle(v);
+ }
+ }
+ }
+
+ requestLayout();
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int n = getChildCount();
+ for (int i = 0; i < n; i++)
+ measureView(getChildAt(i));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right,
+ int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ View cv = mChildViews.get(mCurrent);
+ Point cvOffset;
+
+ if (!mResetLayout) {
+ // Move to next or previous if current is sufficiently off center
+ if (cv != null) {
+ cvOffset = subScreenSizeOffset(cv);
+ // cv.getRight() may be out of date with the current scale
+ // so add left to the measured width for the correct position
+ if (cv.getLeft() + cv.getMeasuredWidth() + cvOffset.x + GAP/2 + mXScroll < getWidth()/2 && mCurrent + 1 < mAdapter.getCount()) {
+ postUnsettle(cv);
+ // post to invoke test for end of animation
+ // where we must set hq area for the new current view
+ post(this);
+
+ mCurrent++;
+ onMoveToChild(mCurrent);
+ }
+
+ if (cv.getLeft() - cvOffset.x - GAP/2 + mXScroll >= getWidth()/2 && mCurrent > 0) {
+ postUnsettle(cv);
+ // post to invoke test for end of animation
+ // where we must set hq area for the new current view
+ post(this);
+
+ mCurrent--;
+ onMoveToChild(mCurrent);
+ }
+ }
+
+ // Remove not needed children and hold them for reuse
+ int numChildren = mChildViews.size();
+ int childIndices[] = new int[numChildren];
+ for (int i = 0; i < numChildren; i++)
+ childIndices[i] = mChildViews.keyAt(i);
+
+ for (int i = 0; i < numChildren; i++) {
+ int ai = childIndices[i];
+ if (ai < mCurrent - 1 || ai > mCurrent + 1) {
+ View v = mChildViews.get(ai);
+ mViewCache.add(v);
+ removeViewInLayout(v);
+ mChildViews.remove(ai);
+ }
+ }
+ } else {
+ mResetLayout = false;
+ mXScroll = mYScroll = 0;
+
+ // Remove all children and hold them for reuse
+ int numChildren = mChildViews.size();
+ for (int i = 0; i < numChildren; i++) {
+ View v = mChildViews.valueAt(i);
+ postUnsettle(v);
+ mViewCache.add(v);
+ removeViewInLayout(v);
+ }
+ mChildViews.clear();
+ // post to ensure generation of hq area
+ post(this);
+ }
+
+ // Ensure current view is present
+ int cvLeft, cvRight, cvTop, cvBottom;
+ boolean notPresent = (mChildViews.get(mCurrent) == null);
+ cv = getOrCreateChild(mCurrent);
+ // When the view is sub-screen-size in either dimension we
+ // offset it to center within the screen area, and to keep
+ // the views spaced out
+ cvOffset = subScreenSizeOffset(cv);
+ if (notPresent) {
+ //Main item not already present. Just place it top left
+ cvLeft = cvOffset.x;
+ cvTop = cvOffset.y;
+ } else {
+ // Main item already present. Adjust by scroll offsets
+ cvLeft = cv.getLeft() + mXScroll;
+ cvTop = cv.getTop() + mYScroll;
+ }
+ // Scroll values have been accounted for
+ mXScroll = mYScroll = 0;
+ cvRight = cvLeft + cv.getMeasuredWidth();
+ cvBottom = cvTop + cv.getMeasuredHeight();
+
+ if (!mUserInteracting && mScroller.isFinished()) {
+ Point corr = getCorrection(getScrollBounds(cvLeft, cvTop, cvRight, cvBottom));
+ cvRight += corr.x;
+ cvLeft += corr.x;
+ cvTop += corr.y;
+ cvBottom += corr.y;
+ }
+
+ cv.layout(cvLeft, cvTop, cvRight, cvBottom);
+
+ if (mCurrent > 0) {
+ View lv = getOrCreateChild(mCurrent - 1);
+ Point leftOffset = subScreenSizeOffset(lv);
+ int gap = leftOffset.x + GAP + cvOffset.x;
+ lv.layout(cvLeft - lv.getMeasuredWidth() - gap,
+ (cvBottom + cvTop - lv.getMeasuredHeight())/2,
+ cvLeft - gap,
+ (cvBottom + cvTop + lv.getMeasuredHeight())/2);
+ }
+
+ if (mCurrent + 1 < mAdapter.getCount()) {
+ View rv = getOrCreateChild(mCurrent + 1);
+ Point rightOffset = subScreenSizeOffset(rv);
+ int gap = cvOffset.x + GAP + rightOffset.x;
+ rv.layout(cvRight + gap,
+ (cvBottom + cvTop - rv.getMeasuredHeight())/2,
+ cvRight + rv.getMeasuredWidth() + gap,
+ (cvBottom + cvTop + rv.getMeasuredHeight())/2);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public View getSelectedView() {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void setAdapter(Adapter adapter) {
+ mAdapter = adapter;
+ mChildViews.clear();
+ removeAllViewsInLayout();
+ requestLayout();
+ }
+
+ @Override
+ public void setSelection(int arg0) {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ private View getCached() {
+ if (mViewCache.size() == 0)
+ return null;
+ else
+ return mViewCache.removeFirst();
+ }
+
+ private View getOrCreateChild(int i) {
+ View v = mChildViews.get(i);
+ if (v == null) {
+ v = mAdapter.getView(i, getCached(), this);
+ addAndMeasureChild(i, v);
+ }
+ onChildSetup(i, v);
+
+ return v;
+ }
+
+ private void addAndMeasureChild(int i, View v) {
+ LayoutParams params = v.getLayoutParams();
+ if (params == null) {
+ params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+ addViewInLayout(v, 0, params, true);
+ mChildViews.append(i, v); // Record the view against it's adapter index
+ measureView(v);
+ }
+
+ private void measureView(View v) {
+ // See what size the view wants to be
+ v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ // Work out a scale that will fit it to this view
+ float scale = Math.min((float)getWidth()/(float)v.getMeasuredWidth(),
+ (float)getHeight()/(float)v.getMeasuredHeight());
+ // Use the fitting values scaled by our current scale factor
+ v.measure(View.MeasureSpec.EXACTLY | (int)(v.getMeasuredWidth()*scale*mScale),
+ View.MeasureSpec.EXACTLY | (int)(v.getMeasuredHeight()*scale*mScale));
+ }
+
+ private Rect getScrollBounds(int left, int top, int right, int bottom) {
+ int xmin = getWidth() - right;
+ int xmax = -left;
+ int ymin = getHeight() - bottom;
+ int ymax = -top;
+
+ // In either dimension, if view smaller than screen then
+ // constrain it to be central
+ if (xmin > xmax) xmin = xmax = (xmin + xmax)/2;
+ if (ymin > ymax) ymin = ymax = (ymin + ymax)/2;
+
+ return new Rect(xmin, ymin, xmax, ymax);
+ }
+
+ private Rect getScrollBounds(View v) {
+ // There can be scroll amounts not yet accounted for in
+ // onLayout, so add mXScroll and mYScroll to the current
+ // positions when calculating the bounds.
+ return getScrollBounds(v.getLeft() + mXScroll,
+ v.getTop() + mYScroll,
+ v.getLeft() + v.getMeasuredWidth() + mXScroll,
+ v.getTop() + v.getMeasuredHeight() + mYScroll);
+ }
+
+ private Point getCorrection(Rect bounds) {
+ return new Point(Math.min(Math.max(0,bounds.left),bounds.right),
+ Math.min(Math.max(0,bounds.top),bounds.bottom));
+ }
+
+ private void postSettle(final View v) {
+ // onSettle and onUnsettle are posted so that the calls
+ // wont be executed until after the system has performed
+ // layout.
+ post (new Runnable() {
+ public void run () {
+ onSettle(v);
+ }
+ });
+ }
+
+ private void postUnsettle(final View v) {
+ post (new Runnable() {
+ public void run () {
+ onUnsettle(v);
+ }
+ });
+ }
+
+ private void slideViewOntoScreen(View v) {
+ Point corr = getCorrection(getScrollBounds(v));
+ if (corr.x != 0 || corr.y != 0) {
+ mScrollerLastX = mScrollerLastY = 0;
+ mScroller.startScroll(0, 0, corr.x, corr.y,
+ Math.max(Math.abs(corr.x), Math.abs(corr.y))*SCROLL_SPEED);
+ post(this);
+ }
+ }
+
+ private Point subScreenSizeOffset(View v) {
+ return new Point(Math.max((getWidth() - v.getMeasuredWidth())/2, 0),
+ Math.max((getHeight() - v.getMeasuredHeight())/2, 0));
+ }
+
+ private static int directionOfTravel(float vx, float vy) {
+ if (Math.abs(vx) > 2 * Math.abs(vy))
+ return (vx > 0) ? MOVING_RIGHT : MOVING_LEFT;
+ else if (Math.abs(vy) > 2 * Math.abs(vx))
+ return (vy > 0) ? MOVING_DOWN : MOVING_UP;
+ else
+ return MOVING_DIAGONALLY;
+ }
+
+ private static boolean withinBoundsInDirectionOfTravel(Rect bounds, float vx, float vy) {
+ switch (directionOfTravel(vx, vy)) {
+ case MOVING_DIAGONALLY: return bounds.contains(0, 0);
+ case MOVING_LEFT: return bounds.left <= 0;
+ case MOVING_RIGHT: return bounds.right >= 0;
+ case MOVING_UP: return bounds.top <= 0;
+ case MOVING_DOWN: return bounds.bottom >= 0;
+ default: throw new NoSuchElementException();
+ }
+ }
+}