diff options
author | Paul Gardiner <paul@glidos.net> | 2012-02-07 15:22:55 +0000 |
---|---|---|
committer | Robin Watts <robin.watts@artifex.com> | 2012-02-20 13:37:23 +0000 |
commit | b3708b652831202b8fc600ccdac8ee3218966dc9 (patch) | |
tree | 9902eafb6639b4fe40358d24ae69d31d026d1f1d | |
parent | 1404fbe67da5a72643730ee3000701c85a46a507 (diff) | |
download | mupdf-b3708b652831202b8fc600ccdac8ee3218966dc9.tar.xz |
Updated MuPDF Android app from Paul Gardiner.
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 Binary files differnew file mode 100644 index 00000000..4be0f5df --- /dev/null +++ b/android/res/drawable-hdpi/btn_star_big_off.png diff --git a/android/res/drawable-hdpi/ic_btn_search.png b/android/res/drawable-hdpi/ic_btn_search.png Binary files differnew file mode 100644 index 00000000..0a91eabc --- /dev/null +++ b/android/res/drawable-hdpi/ic_btn_search.png diff --git a/android/res/drawable-hdpi/ic_media_next.png b/android/res/drawable-hdpi/ic_media_next.png Binary files differnew file mode 100644 index 00000000..2552f4ed --- /dev/null +++ b/android/res/drawable-hdpi/ic_media_next.png diff --git a/android/res/drawable-hdpi/ic_media_previous.png b/android/res/drawable-hdpi/ic_media_previous.png Binary files differnew file mode 100644 index 00000000..05eba718 --- /dev/null +++ b/android/res/drawable-hdpi/ic_media_previous.png diff --git a/android/res/drawable-mdpi/btn_star_big_off.png b/android/res/drawable-mdpi/btn_star_big_off.png Binary files differnew file mode 100644 index 00000000..7e9342b5 --- /dev/null +++ b/android/res/drawable-mdpi/btn_star_big_off.png diff --git a/android/res/drawable-mdpi/ic_btn_search.png b/android/res/drawable-mdpi/ic_btn_search.png Binary files differnew file mode 100644 index 00000000..3f8913e2 --- /dev/null +++ b/android/res/drawable-mdpi/ic_btn_search.png diff --git a/android/res/drawable-mdpi/ic_media_next.png b/android/res/drawable-mdpi/ic_media_next.png Binary files differnew file mode 100644 index 00000000..84f38e8f --- /dev/null +++ b/android/res/drawable-mdpi/ic_media_next.png diff --git a/android/res/drawable-mdpi/ic_media_previous.png b/android/res/drawable-mdpi/ic_media_previous.png Binary files differnew file mode 100644 index 00000000..1bba5441 --- /dev/null +++ b/android/res/drawable-mdpi/ic_media_previous.png 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 Binary files differnew file mode 100644 index 00000000..3fcd274e --- /dev/null +++ b/android/res/drawable/tile09.jpg 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(); + } + } +} |