[Libreoffice-commits] .: android/experimental

Tor Lillqvist tml at kemper.freedesktop.org
Thu May 31 12:41:55 PDT 2012


 android/experimental/DocumentLoader/src/com/polites/android/Animation.java                     |   32 
 android/experimental/DocumentLoader/src/com/polites/android/Animator.java                      |   96 +
 android/experimental/DocumentLoader/src/com/polites/android/FlingAnimation.java                |   74 +
 android/experimental/DocumentLoader/src/com/polites/android/FlingAnimationListener.java        |   29 
 android/experimental/DocumentLoader/src/com/polites/android/FlingListener.java                 |   45 
 android/experimental/DocumentLoader/src/com/polites/android/GestureImageView.java              |  712 ++++++++++
 android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewListener.java      |   30 
 android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewTouchListener.java |  540 +++++++
 android/experimental/DocumentLoader/src/com/polites/android/MathUtils.java                     |   76 +
 android/experimental/DocumentLoader/src/com/polites/android/MoveAnimation.java                 |  107 +
 android/experimental/DocumentLoader/src/com/polites/android/MoveAnimationListener.java         |   27 
 android/experimental/DocumentLoader/src/com/polites/android/VectorF.java                       |   63 
 android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimation.java                 |  167 ++
 android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimationListener.java         |   26 
 android/experimental/DocumentLoader/src/org/libreoffice/android/examples/DocumentLoader.java   |  109 -
 15 files changed, 2076 insertions(+), 57 deletions(-)

New commits:
commit 5dc2b43e46c9f2c77f3ca236511eaf615a62f672
Author: Tor Lillqvist <tlillqvist at suse.com>
Date:   Thu May 31 22:31:58 2012 +0300

    Use Jason Polites's GestureImageView, and some cleanup
    
    Change-Id: I916c36b3b55681cdf8f0d1ffd0236e54f3b67b86

diff --git a/android/experimental/DocumentLoader/src/com/polites/android/Animation.java b/android/experimental/DocumentLoader/src/com/polites/android/Animation.java
new file mode 100644
index 0000000..9936208
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/Animation.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+/**
+ * @author Jason Polites
+ *
+ */
+public interface Animation {
+
+	/**
+	 * Transforms the view.
+	 * @param view
+	 * @param diffTime
+	 * @return true if this animation should remain active.  False otherwise.
+	 */
+	public boolean update(GestureImageView view, long time);
+
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/Animator.java b/android/experimental/DocumentLoader/src/com/polites/android/Animator.java
new file mode 100644
index 0000000..fb0728b
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/Animator.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+
+/**
+ * @author Jason Polites
+ *
+ */
+public class Animator extends Thread {
+
+	private GestureImageView view;
+	private Animation animation;
+	private boolean running = false;
+	private boolean active = false;
+	private long lastTime = -1L;
+
+	public Animator(GestureImageView view, String threadName) {
+		super(threadName);
+		this.view = view;
+	}
+
+	@Override
+	public void run() {
+
+		running = true;
+
+		while(running) {
+
+			while(active && animation != null) {
+				long time = System.currentTimeMillis();
+				active = animation.update(view, time - lastTime);
+				view.redraw();
+				lastTime = time;
+
+				while(active) {
+					try {
+						if(view.waitForDraw(32)) { // 30Htz
+							break;
+						}
+					}
+					catch (InterruptedException ignore) {
+						active = false;
+					}
+				}
+			}
+
+			synchronized(this) {
+				if(running) {
+					try {
+						wait();
+					}
+					catch (InterruptedException ignore) {}
+				}
+			}
+		}
+	}
+
+	public synchronized void finish() {
+		running = false;
+		active = false;
+		notifyAll();
+	}
+
+	public void play(Animation transformer) {
+		if(active) {
+			cancel();
+		}
+		this.animation = transformer;
+
+		activate();
+	}
+
+	public synchronized void activate() {
+		lastTime = System.currentTimeMillis();
+		active = true;
+		notifyAll();
+	}
+
+	public void cancel() {
+		active = false;
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/FlingAnimation.java b/android/experimental/DocumentLoader/src/com/polites/android/FlingAnimation.java
new file mode 100644
index 0000000..3124b62
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/FlingAnimation.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+/**
+ * @author Jason Polites
+ *
+ */
+public class FlingAnimation implements Animation {
+
+	private float velocityX;
+	private float velocityY;
+
+	private float factor = 0.85f;
+
+	private float threshold = 10;
+
+	private FlingAnimationListener listener;
+
+	/* (non-Javadoc)
+	 * @see com.polites.android.Transformer#update(com.polites.android.GestureImageView, long)
+	 */
+	@Override
+	public boolean update(GestureImageView view, long time) {
+		float seconds = (float) time / 1000.0f;
+
+		float dx = velocityX * seconds;
+		float dy = velocityY * seconds;
+
+		velocityX *= factor;
+		velocityY *= factor;
+
+		boolean active = (Math.abs(velocityX) > threshold && Math.abs(velocityY) > threshold);
+
+		if(listener != null) {
+			listener.onMove(dx, dy);
+
+			if(!active) {
+				listener.onComplete();
+			}
+		}
+
+		return active;
+	}
+
+	public void setVelocityX(float velocityX) {
+		this.velocityX = velocityX;
+	}
+
+	public void setVelocityY(float velocityY) {
+		this.velocityY = velocityY;
+	}
+
+	public void setFactor(float factor) {
+		this.factor = factor;
+	}
+
+	public void setListener(FlingAnimationListener listener) {
+		this.listener = listener;
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/FlingAnimationListener.java b/android/experimental/DocumentLoader/src/com/polites/android/FlingAnimationListener.java
new file mode 100644
index 0000000..b9611d5
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/FlingAnimationListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+
+/**
+ * @author Jason Polites
+ *
+ */
+public interface FlingAnimationListener {
+
+	public void onMove(float x, float y);
+
+	public void onComplete();
+
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/FlingListener.java b/android/experimental/DocumentLoader/src/com/polites/android/FlingListener.java
new file mode 100644
index 0000000..ab3007a
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/FlingListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+
+/**
+ * @author Jason Polites
+ *
+ */
+public class FlingListener extends SimpleOnGestureListener {
+
+	private float velocityX;
+	private float velocityY;
+
+	@Override
+	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+		this.velocityX = velocityX;
+		this.velocityY = velocityY;
+		return true;
+	}
+
+	public float getVelocityX() {
+		return velocityX;
+	}
+
+	public float getVelocityY() {
+		return velocityY;
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/GestureImageView.java b/android/experimental/DocumentLoader/src/com/polites/android/GestureImageView.java
new file mode 100644
index 0000000..1cde6e4
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/GestureImageView.java
@@ -0,0 +1,712 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+import java.io.InputStream;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ImageView;
+
+public class GestureImageView extends ImageView  {
+
+	public static final String GLOBAL_NS = "http://schemas.android.com/apk/res/android";
+	public static final String LOCAL_NS = "http://schemas.polites.com/android";
+
+	private final Semaphore drawLock = new Semaphore(0);
+	private Animator animator;
+
+	private Drawable drawable;
+
+	private float x = 0, y = 0;
+
+	private boolean layout = false;
+
+	private float scaleAdjust = 1.0f;
+	private float startingScale = -1.0f;
+
+	private float scale = 1.0f;
+	private float maxScale = 5.0f;
+	private float minScale = 0.75f;
+	private float fitScaleHorizontal = 1.0f;
+	private float fitScaleVertical = 1.0f;
+	private float rotation = 0.0f;
+
+	private float centerX;
+	private float centerY;
+
+	private Float startX, startY;
+
+	private int hWidth;
+	private int hHeight;
+
+	private int resId = -1;
+	private boolean recycle = false;
+	private boolean strict = false;
+
+	private int displayHeight;
+	private int displayWidth;
+
+	private int alpha = 255;
+	private ColorFilter colorFilter;
+
+	private int deviceOrientation = -1;
+	private int imageOrientation;
+
+	private GestureImageViewListener gestureImageViewListener;
+	private GestureImageViewTouchListener gestureImageViewTouchListener;
+
+	private OnTouchListener customOnTouchListener;
+	private OnClickListener onClickListener;
+
+	public GestureImageView(Context context, AttributeSet attrs, int defStyle) {
+		this(context, attrs);
+	}
+
+	public GestureImageView(Context context, AttributeSet attrs) {
+		super(context, attrs);
+
+		String scaleType = attrs.getAttributeValue(GLOBAL_NS, "scaleType");
+
+		if(scaleType == null || scaleType.trim().length() == 0) {
+			setScaleType(ScaleType.CENTER_INSIDE);
+		}
+
+		String strStartX = attrs.getAttributeValue(LOCAL_NS, "start-x");
+		String strStartY = attrs.getAttributeValue(LOCAL_NS, "start-y");
+
+		if(strStartX != null && strStartX.trim().length() > 0) {
+			startX = Float.parseFloat(strStartX);
+		}
+
+		if(strStartY != null && strStartY.trim().length() > 0) {
+			startY = Float.parseFloat(strStartY);
+		}
+
+		setStartingScale(attrs.getAttributeFloatValue(LOCAL_NS, "start-scale", startingScale));
+		setMinScale(attrs.getAttributeFloatValue(LOCAL_NS, "min-scale", minScale));
+		setMaxScale(attrs.getAttributeFloatValue(LOCAL_NS, "max-scale", maxScale));
+		setStrict(attrs.getAttributeBooleanValue(LOCAL_NS, "strict", strict));
+		setRecycle(attrs.getAttributeBooleanValue(LOCAL_NS, "recycle", recycle));
+
+		initImage();
+	}
+
+	public GestureImageView(Context context) {
+		super(context);
+		setScaleType(ScaleType.CENTER_INSIDE);
+		initImage();
+	}
+
+	@Override
+	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+		if(drawable != null) {
+			int orientation = getResources().getConfiguration().orientation;
+			if(orientation == Configuration.ORIENTATION_LANDSCAPE) {
+				displayHeight = MeasureSpec.getSize(heightMeasureSpec);
+
+				if(getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
+					float ratio = (float) getImageWidth() / (float) getImageHeight();
+					displayWidth = Math.round( (float) displayHeight * ratio) ;
+				}
+				else {
+					displayWidth = MeasureSpec.getSize(widthMeasureSpec);
+				}
+			}
+			else {
+				displayWidth = MeasureSpec.getSize(widthMeasureSpec);
+				if(getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
+					float ratio = (float) getImageHeight() / (float) getImageWidth();
+					displayHeight = Math.round( (float) displayWidth * ratio) ;
+				}
+				else {
+					displayHeight = MeasureSpec.getSize(heightMeasureSpec);
+				}
+			}
+		}
+		else {
+			displayHeight = MeasureSpec.getSize(heightMeasureSpec);
+			displayWidth = MeasureSpec.getSize(widthMeasureSpec);
+		}
+
+		setMeasuredDimension(displayWidth, displayHeight);
+	}
+
+	@Override
+	protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+		super.onLayout(changed, left, top, right, bottom);
+		if(changed || !layout) {
+			setupCanvas(displayWidth, displayHeight, getResources().getConfiguration().orientation);
+		}
+	}
+
+	protected void setupCanvas(int measuredWidth, int measuredHeight, int orientation) {
+
+		if(deviceOrientation != orientation) {
+			layout = false;
+			deviceOrientation = orientation;
+		}
+
+		if(drawable != null && !layout) {
+			int imageWidth = getImageWidth();
+			int imageHeight = getImageHeight();
+
+			hWidth = Math.round(((float)imageWidth / 2.0f));
+			hHeight = Math.round(((float)imageHeight / 2.0f));
+
+			measuredWidth -= (getPaddingLeft() + getPaddingRight());
+			measuredHeight -= (getPaddingTop() + getPaddingBottom());
+
+			computeCropScale(imageWidth, imageHeight, measuredWidth, measuredHeight);
+
+			if(startingScale <= 0.0f) {
+				computeStartingScale(imageWidth, imageHeight, measuredWidth, measuredHeight);
+			}
+
+			scaleAdjust = startingScale;
+
+			this.centerX = (float) measuredWidth / 2.0f;
+			this.centerY = (float) measuredHeight / 2.0f;
+
+			if(startX == null) {
+				x = centerX;
+			}
+			else {
+				x = startX;
+			}
+
+			if(startY == null) {
+				y = centerY;
+			}
+			else {
+				y = startY;
+			}
+
+			gestureImageViewTouchListener = new GestureImageViewTouchListener(this, measuredWidth, measuredHeight);
+
+			if(isLandscape()) {
+				gestureImageViewTouchListener.setMinScale(minScale * fitScaleHorizontal);
+			}
+			else {
+				gestureImageViewTouchListener.setMinScale(minScale * fitScaleVertical);
+			}
+
+
+			gestureImageViewTouchListener.setMaxScale(maxScale * startingScale);
+
+			gestureImageViewTouchListener.setFitScaleHorizontal(fitScaleHorizontal);
+			gestureImageViewTouchListener.setFitScaleVertical(fitScaleVertical);
+			gestureImageViewTouchListener.setCanvasWidth(measuredWidth);
+			gestureImageViewTouchListener.setCanvasHeight(measuredHeight);
+			gestureImageViewTouchListener.setOnClickListener(onClickListener);
+
+			drawable.setBounds(-hWidth,-hHeight,hWidth,hHeight);
+
+			super.setOnTouchListener(new OnTouchListener() {
+				@Override
+				public boolean onTouch(View v, MotionEvent event) {
+					if(customOnTouchListener != null) {
+						customOnTouchListener.onTouch(v, event);
+					}
+					return gestureImageViewTouchListener.onTouch(v, event);
+				}
+			});
+
+			layout = true;
+		}
+	}
+
+	protected void computeCropScale(int imageWidth, int imageHeight, int measuredWidth, int measuredHeight) {
+		fitScaleHorizontal = (float) measuredWidth / (float) imageWidth;
+		fitScaleVertical = (float) measuredHeight / (float) imageHeight;
+	}
+
+	protected void computeStartingScale(int imageWidth, int imageHeight, int measuredWidth, int measuredHeight) {
+		switch(getScaleType()) {
+			case CENTER:
+				// Center the image in the view, but perform no scaling.
+				startingScale = 1.0f;
+				break;
+
+			case CENTER_CROP:
+				startingScale = Math.max((float) measuredHeight / (float) imageHeight, (float) measuredWidth/ (float) imageWidth);
+				break;
+
+			case CENTER_INSIDE:
+				if(isLandscape()) {
+					startingScale = fitScaleHorizontal;
+				}
+				else {
+					startingScale = fitScaleVertical;
+				}
+				break;
+		}
+	}
+
+	protected boolean isRecycled() {
+		if(drawable != null && drawable instanceof BitmapDrawable) {
+			Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();
+			if(bitmap != null) {
+				return bitmap.isRecycled();
+			}
+		}
+		return false;
+	}
+
+	protected void recycle() {
+		if(recycle && drawable != null && drawable instanceof BitmapDrawable) {
+			Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();
+			if(bitmap != null) {
+				bitmap.recycle();
+			}
+		}
+	}
+
+	@Override
+	protected void onDraw(Canvas canvas) {
+		if(layout) {
+			if(drawable != null && !isRecycled()) {
+				canvas.save();
+
+				float adjustedScale = scale * scaleAdjust;
+
+				canvas.translate(x, y);
+
+				if(rotation != 0.0f) {
+					canvas.rotate(rotation);
+				}
+
+				if(adjustedScale != 1.0f) {
+					canvas.scale(adjustedScale, adjustedScale);
+				}
+
+				drawable.draw(canvas);
+
+				canvas.restore();
+			}
+
+			if(drawLock.availablePermits() <= 0) {
+				drawLock.release();
+			}
+		}
+	}
+
+	/**
+	 * Waits for a draw
+	 * @param max time to wait for draw (ms)
+	 * @throws InterruptedException
+	 */
+	public boolean waitForDraw(long timeout) throws InterruptedException {
+		return drawLock.tryAcquire(timeout, TimeUnit.MILLISECONDS);
+	}
+
+	@Override
+	protected void onAttachedToWindow() {
+		animator = new Animator(this, "GestureImageViewAnimator");
+		animator.start();
+
+		if(resId >= 0 && drawable == null) {
+			setImageResource(resId);
+		}
+
+		super.onAttachedToWindow();
+	}
+
+	public void animationStart(Animation animation) {
+		if(animator != null) {
+			animator.play(animation);
+		}
+	}
+
+	public void animationStop() {
+		if(animator != null) {
+			animator.cancel();
+		}
+	}
+
+	@Override
+	protected void onDetachedFromWindow() {
+		if(animator != null) {
+			animator.finish();
+		}
+		if(recycle && drawable != null && !isRecycled()) {
+			recycle();
+			drawable = null;
+		}
+		super.onDetachedFromWindow();
+	}
+
+	protected void initImage() {
+		if(this.drawable != null) {
+			this.drawable.setAlpha(alpha);
+			this.drawable.setFilterBitmap(true);
+			if(colorFilter != null) {
+				this.drawable.setColorFilter(colorFilter);
+			}
+		}
+
+		if(!layout) {
+			requestLayout();
+			redraw();
+		}
+	}
+
+	public void setImageBitmap(Bitmap image) {
+		this.drawable = new BitmapDrawable(getResources(), image);
+		initImage();
+	}
+
+	@Override
+	public void setImageDrawable(Drawable drawable) {
+		this.drawable = drawable;
+		initImage();
+	}
+
+	public void setImageResource(int id) {
+		if(this.drawable != null) {
+			this.recycle();
+		}
+		if(id >= 0) {
+			this.resId = id;
+			setImageDrawable(getContext().getResources().getDrawable(id));
+		}
+	}
+
+	public int getScaledWidth() {
+		return Math.round(getImageWidth() * getScale());
+	}
+
+	public int getScaledHeight() {
+		return Math.round(getImageHeight() * getScale());
+	}
+
+	public int getImageWidth() {
+		if(drawable != null) {
+			return drawable.getIntrinsicWidth();
+		}
+		return 0;
+	}
+
+	public int getImageHeight() {
+		if(drawable != null) {
+			return drawable.getIntrinsicHeight();
+		}
+		return 0;
+	}
+
+	public void moveBy(float x, float y) {
+		this.x += x;
+		this.y += y;
+	}
+
+	public void setPosition(float x, float y) {
+		this.x = x;
+		this.y = y;
+	}
+
+	public void redraw() {
+		postInvalidate();
+	}
+
+	public void setMinScale(float min) {
+		this.minScale = min;
+		if(gestureImageViewTouchListener != null) {
+			gestureImageViewTouchListener.setMinScale(min * fitScaleHorizontal);
+		}
+	}
+
+	public void setMaxScale(float max) {
+		this.maxScale = max;
+		if(gestureImageViewTouchListener != null) {
+			gestureImageViewTouchListener.setMaxScale(max * startingScale);
+		}
+	}
+
+	public void setScale(float scale) {
+		scaleAdjust = scale;
+	}
+
+	public float getScale() {
+		return scaleAdjust;
+	}
+
+	public float getImageX() {
+		return x;
+	}
+
+	public float getImageY() {
+		return y;
+	}
+
+	public boolean isStrict() {
+		return strict;
+	}
+
+	public void setStrict(boolean strict) {
+		this.strict = strict;
+	}
+
+	public boolean isRecycle() {
+		return recycle;
+	}
+
+	public void setRecycle(boolean recycle) {
+		this.recycle = recycle;
+	}
+
+	public void reset() {
+		x = centerX;
+		y = centerY;
+		scaleAdjust = startingScale;
+		redraw();
+	}
+
+	public void setRotation(float rotation) {
+		this.rotation = rotation;
+	}
+
+	public void setGestureImageViewListener(GestureImageViewListener pinchImageViewListener) {
+		this.gestureImageViewListener = pinchImageViewListener;
+	}
+
+	public GestureImageViewListener getGestureImageViewListener() {
+		return gestureImageViewListener;
+	}
+
+	@Override
+	public Drawable getDrawable() {
+		return drawable;
+	}
+
+	@Override
+	public void setAlpha(int alpha) {
+		this.alpha = alpha;
+		if(drawable != null) {
+			drawable.setAlpha(alpha);
+		}
+	}
+
+	@Override
+	public void setColorFilter(ColorFilter cf) {
+		this.colorFilter = cf;
+		if(drawable != null) {
+			drawable.setColorFilter(cf);
+		}
+	}
+
+	@Override
+	public void setImageURI(Uri mUri) {
+		if ("content".equals(mUri.getScheme())) {
+			try {
+				String[] orientationColumn = {MediaStore.Images.Media.ORIENTATION};
+
+				Cursor cur = getContext().getContentResolver().query(mUri, orientationColumn, null, null, null);
+
+				if (cur != null && cur.moveToFirst()) {
+					imageOrientation = cur.getInt(cur.getColumnIndex(orientationColumn[0]));
+				}
+
+				InputStream in = null;
+
+				try {
+					in = getContext().getContentResolver().openInputStream(mUri);
+					Bitmap bmp = BitmapFactory.decodeStream(in);
+
+					if(imageOrientation != 0) {
+						Matrix m = new Matrix();
+						m.postRotate(imageOrientation);
+						Bitmap rotated = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true);
+						bmp.recycle();
+						setImageDrawable(new BitmapDrawable(getResources(), rotated));
+					}
+					else {
+						setImageDrawable(new BitmapDrawable(getResources(), bmp));
+					}
+				}
+				finally {
+					if(in != null) {
+						in.close();
+					}
+
+					if(cur != null) {
+						cur.close();
+					}
+				}
+			}
+			catch (Exception e) {
+				Log.w("GestureImageView", "Unable to open content: " + mUri, e);
+			}
+		}
+		else {
+			setImageDrawable(Drawable.createFromPath(mUri.toString()));
+		}
+
+		if (drawable == null) {
+			Log.e("GestureImageView", "resolveUri failed on bad bitmap uri: " + mUri);
+			// Don't try again.
+			mUri = null;
+		}
+	}
+
+	@Override
+	public Matrix getImageMatrix() {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+		return super.getImageMatrix();
+	}
+
+	@Override
+	public void setScaleType(ScaleType scaleType) {
+		if(scaleType == ScaleType.CENTER ||
+			scaleType == ScaleType.CENTER_CROP ||
+			scaleType == ScaleType.CENTER_INSIDE) {
+
+			super.setScaleType(scaleType);
+		}
+		else if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+	}
+
+	@Override
+	public void invalidateDrawable(Drawable dr) {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+		super.invalidateDrawable(dr);
+	}
+
+	@Override
+	public int[] onCreateDrawableState(int extraSpace) {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+		return super.onCreateDrawableState(extraSpace);
+	}
+
+	@Override
+	public void setAdjustViewBounds(boolean adjustViewBounds) {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+		super.setAdjustViewBounds(adjustViewBounds);
+	}
+
+	@Override
+	public void setImageLevel(int level) {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+		super.setImageLevel(level);
+	}
+
+	@Override
+	public void setImageMatrix(Matrix matrix) {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+	}
+
+	@Override
+	public void setImageState(int[] state, boolean merge) {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+	}
+
+	@Override
+	public void setSelected(boolean selected) {
+		if(strict) {
+			throw new UnsupportedOperationException("Not supported");
+		}
+		super.setSelected(selected);
+	}
+
+	@Override
+	public void setOnTouchListener(OnTouchListener l) {
+		this.customOnTouchListener = l;
+	}
+
+	public float getCenterX() {
+		return centerX;
+	}
+
+	public float getCenterY() {
+		return centerY;
+	}
+
+	public boolean isLandscape() {
+		return getImageWidth() >= getImageHeight();
+	}
+
+	public boolean isPortrait() {
+		return getImageWidth() <= getImageHeight();
+	}
+
+	public void setStartingScale(float startingScale) {
+		this.startingScale = startingScale;
+	}
+
+	public void setStartingPosition(float x, float y) {
+		this.startX = x;
+		this.startY = y;
+	}
+
+	@Override
+	public void setOnClickListener(OnClickListener l) {
+		this.onClickListener = l;
+
+		if(gestureImageViewTouchListener != null) {
+			gestureImageViewTouchListener.setOnClickListener(l);
+		}
+	}
+
+	/**
+	 * Returns true if the image dimensions are aligned with the orientation of the device.
+	 * @return
+	 */
+	public boolean isOrientationAligned() {
+		if(deviceOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+			return isLandscape();
+		}
+		else if(deviceOrientation == Configuration.ORIENTATION_PORTRAIT) {
+			return isPortrait();
+		}
+		return true;
+	}
+
+	public int getDeviceOrientation() {
+		return deviceOrientation;
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewListener.java b/android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewListener.java
new file mode 100644
index 0000000..4a52358
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+/**
+ * @author jasonpolites
+ *
+ */
+public interface GestureImageViewListener {
+
+	public void onTouch(float x, float y);
+
+	public void onScale(float scale);
+
+	public void onPosition(float x, float y);
+
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewTouchListener.java b/android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewTouchListener.java
new file mode 100644
index 0000000..76751d1
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/GestureImageViewTouchListener.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+import android.content.res.Configuration;
+import android.graphics.PointF;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+
+public class GestureImageViewTouchListener implements OnTouchListener {
+
+	private GestureImageView image;
+	private OnClickListener onClickListener;
+
+	private final PointF current = new PointF();
+	private final PointF last = new PointF();
+	private final PointF next = new PointF();
+	private final PointF midpoint = new PointF();
+
+	private final VectorF scaleVector = new VectorF();
+	private final VectorF pinchVector = new VectorF();
+
+	private boolean touched = false;
+	private boolean inZoom = false;
+
+	private float initialDistance;
+	private float lastScale = 1.0f;
+	private float currentScale = 1.0f;
+
+	private float boundaryLeft = 0;
+	private float boundaryTop = 0;
+	private float boundaryRight = 0;
+	private float boundaryBottom = 0;
+
+	private float maxScale = 5.0f;
+	private float minScale = 0.25f;
+	private float fitScaleHorizontal = 1.0f;
+	private float fitScaleVertical = 1.0f;
+
+	private int canvasWidth = 0;
+	private int canvasHeight = 0;
+
+	private float centerX = 0;
+	private float centerY = 0;
+
+	private float startingScale = 0;
+
+	private boolean canDragX = false;
+	private boolean canDragY = false;
+
+	private boolean multiTouch = false;
+
+	private int displayWidth;
+	private int displayHeight;
+
+	private int imageWidth;
+	private int imageHeight;
+
+	private FlingListener flingListener;
+	private FlingAnimation flingAnimation;
+	private ZoomAnimation zoomAnimation;
+	private MoveAnimation moveAnimation;
+	private GestureDetector tapDetector;
+	private GestureDetector flingDetector;
+	private GestureImageViewListener imageListener;
+
+	public GestureImageViewTouchListener(final GestureImageView image, int displayWidth, int displayHeight) {
+		super();
+
+		this.image = image;
+
+		this.displayWidth = displayWidth;
+		this.displayHeight = displayHeight;
+
+		this.centerX = (float) displayWidth / 2.0f;
+		this.centerY = (float) displayHeight / 2.0f;
+
+		this.imageWidth = image.getImageWidth();
+		this.imageHeight = image.getImageHeight();
+
+		startingScale = image.getScale();
+
+		currentScale = startingScale;
+		lastScale = startingScale;
+
+		boundaryRight = displayWidth;
+		boundaryBottom = displayHeight;
+		boundaryLeft = 0;
+		boundaryTop = 0;
+
+		next.x = image.getImageX();
+		next.y = image.getImageY();
+
+		flingListener = new FlingListener();
+		flingAnimation = new FlingAnimation();
+		zoomAnimation = new ZoomAnimation();
+		moveAnimation = new MoveAnimation();
+
+		flingAnimation.setListener(new FlingAnimationListener() {
+			@Override
+			public void onMove(float x, float y) {
+				handleDrag(current.x + x, current.y + y);
+			}
+
+			@Override
+			public void onComplete() {}
+		});
+
+		zoomAnimation.setZoom(2.0f);
+		zoomAnimation.setZoomAnimationListener(new ZoomAnimationListener() {
+			@Override
+			public void onZoom(float scale, float x, float y) {
+				if(scale <= maxScale && scale >= minScale) {
+					handleScale(scale, x, y);
+				}
+			}
+
+			@Override
+			public void onComplete() {
+				inZoom = false;
+				handleUp();
+			}
+		});
+
+		moveAnimation.setMoveAnimationListener(new MoveAnimationListener() {
+
+			@Override
+			public void onMove(float x, float y) {
+				image.setPosition(x, y);
+				image.redraw();
+			}
+		});
+
+		tapDetector = new GestureDetector(image.getContext(), new SimpleOnGestureListener() {
+			@Override
+			public boolean onDoubleTap(MotionEvent e) {
+				startZoom(e);
+				return true;
+			}
+
+			@Override
+			public boolean onSingleTapConfirmed(MotionEvent e) {
+				if(!inZoom) {
+					if(onClickListener != null) {
+						onClickListener.onClick(image);
+						return true;
+					}
+				}
+
+				return false;
+			}
+		});
+
+		flingDetector = new GestureDetector(image.getContext(), flingListener);
+		imageListener = image.getGestureImageViewListener();
+
+		calculateBoundaries();
+	}
+
+	private void startFling() {
+		flingAnimation.setVelocityX(flingListener.getVelocityX());
+		flingAnimation.setVelocityY(flingListener.getVelocityY());
+		image.animationStart(flingAnimation);
+	}
+
+	private void startZoom(MotionEvent e) {
+		inZoom = true;
+		zoomAnimation.reset();
+
+		float zoomTo = 1.0f;
+
+		if(image.isLandscape()) {
+			if(image.getDeviceOrientation() == Configuration.ORIENTATION_PORTRAIT) {
+				int scaledHeight = image.getScaledHeight();
+
+				if(scaledHeight < canvasHeight) {
+					zoomTo = fitScaleVertical / currentScale;
+					zoomAnimation.setTouchX(e.getX());
+					zoomAnimation.setTouchY(image.getCenterY());
+				}
+				else {
+					zoomTo = fitScaleHorizontal / currentScale;
+					zoomAnimation.setTouchX(image.getCenterX());
+					zoomAnimation.setTouchY(image.getCenterY());
+				}
+			}
+			else {
+				int scaledWidth = image.getScaledWidth();
+
+				if(scaledWidth == canvasWidth) {
+					zoomTo = currentScale*4.0f;
+					zoomAnimation.setTouchX(e.getX());
+					zoomAnimation.setTouchY(e.getY());
+				}
+				else if(scaledWidth < canvasWidth) {
+					zoomTo = fitScaleHorizontal / currentScale;
+					zoomAnimation.setTouchX(image.getCenterX());
+					zoomAnimation.setTouchY(e.getY());
+				}
+				else {
+					zoomTo = fitScaleHorizontal / currentScale;
+					zoomAnimation.setTouchX(image.getCenterX());
+					zoomAnimation.setTouchY(image.getCenterY());
+				}
+			}
+		}
+		else {
+			if(image.getDeviceOrientation() == Configuration.ORIENTATION_PORTRAIT) {
+
+				int scaledHeight = image.getScaledHeight();
+
+				if(scaledHeight == canvasHeight) {
+					zoomTo = currentScale*4.0f;
+					zoomAnimation.setTouchX(e.getX());
+					zoomAnimation.setTouchY(e.getY());
+				}
+				else if(scaledHeight < canvasHeight) {
+					zoomTo = fitScaleVertical / currentScale;
+					zoomAnimation.setTouchX(e.getX());
+					zoomAnimation.setTouchY(image.getCenterY());
+				}
+				else {
+					zoomTo = fitScaleVertical / currentScale;
+					zoomAnimation.setTouchX(image.getCenterX());
+					zoomAnimation.setTouchY(image.getCenterY());
+				}
+			}
+			else {
+				int scaledWidth = image.getScaledWidth();
+
+				if(scaledWidth < canvasWidth) {
+					zoomTo = fitScaleHorizontal / currentScale;
+					zoomAnimation.setTouchX(image.getCenterX());
+					zoomAnimation.setTouchY(e.getY());
+				}
+				else {
+					zoomTo = fitScaleVertical / currentScale;
+					zoomAnimation.setTouchX(image.getCenterX());
+					zoomAnimation.setTouchY(image.getCenterY());
+				}
+			}
+		}
+
+		zoomAnimation.setZoom(zoomTo);
+		image.animationStart(zoomAnimation);
+	}
+
+
+	private void stopAnimations() {
+		image.animationStop();
+	}
+
+	@Override
+	public boolean onTouch(View v, MotionEvent event) {
+
+		if(!inZoom) {
+
+			if(!tapDetector.onTouchEvent(event)) {
+				if(event.getPointerCount() == 1 && flingDetector.onTouchEvent(event)) {
+					startFling();
+				}
+
+				if(event.getAction() == MotionEvent.ACTION_UP) {
+					handleUp();
+				}
+				else if(event.getAction() == MotionEvent.ACTION_DOWN) {
+					stopAnimations();
+
+					last.x = event.getX();
+					last.y = event.getY();
+
+					if(imageListener != null) {
+						imageListener.onTouch(last.x, last.y);
+					}
+
+					touched = true;
+				}
+				else if(event.getAction() == MotionEvent.ACTION_MOVE) {
+					if(event.getPointerCount() > 1) {
+						multiTouch = true;
+						if(initialDistance > 0) {
+
+							pinchVector.set(event);
+							pinchVector.calculateLength();
+
+							float distance = pinchVector.length;
+
+							if(initialDistance != distance) {
+
+								float newScale = (distance / initialDistance) * lastScale;
+
+								if(newScale <= maxScale) {
+									scaleVector.length *= newScale;
+
+									scaleVector.calculateEndPoint();
+
+									scaleVector.length /= newScale;
+
+									float newX = scaleVector.end.x;
+									float newY = scaleVector.end.y;
+
+									handleScale(newScale, newX, newY);
+								}
+							}
+						}
+						else {
+							initialDistance = MathUtils.distance(event);
+
+							MathUtils.midpoint(event, midpoint);
+
+							scaleVector.setStart(midpoint);
+							scaleVector.setEnd(next);
+
+							scaleVector.calculateLength();
+							scaleVector.calculateAngle();
+
+							scaleVector.length /= lastScale;
+						}
+					}
+					else {
+						if(!touched) {
+							touched = true;
+							last.x = event.getX();
+							last.y = event.getY();
+							next.x = image.getImageX();
+							next.y = image.getImageY();
+						}
+						else if(!multiTouch) {
+							if(handleDrag(event.getX(), event.getY())) {
+								image.redraw();
+							}
+						}
+					}
+				}
+			}
+		}
+
+		return true;
+	}
+
+	protected void handleUp() {
+
+		multiTouch = false;
+
+		initialDistance = 0;
+		lastScale = currentScale;
+
+		if(!canDragX) {
+			next.x = centerX;
+		}
+
+		if(!canDragY) {
+			next.y = centerY;
+		}
+
+		boundCoordinates();
+
+		if(!canDragX && !canDragY) {
+
+			if(image.isLandscape()) {
+				currentScale = fitScaleHorizontal;
+				lastScale = fitScaleHorizontal;
+			}
+			else {
+				currentScale = fitScaleVertical;
+				lastScale = fitScaleVertical;
+			}
+		}
+
+		image.setScale(currentScale);
+		image.setPosition(next.x, next.y);
+
+		if(imageListener != null) {
+			imageListener.onScale(currentScale);
+			imageListener.onPosition(next.x, next.y);
+		}
+
+		image.redraw();
+	}
+
+	protected void handleScale(float scale, float x, float y) {
+
+		currentScale = scale;
+
+		if(currentScale > maxScale) {
+			currentScale = maxScale;
+		}
+		else if (currentScale < minScale) {
+			currentScale = minScale;
+		}
+		else {
+			next.x = x;
+			next.y = y;
+		}
+
+		calculateBoundaries();
+
+		image.setScale(currentScale);
+		image.setPosition(next.x, next.y);
+
+		if(imageListener != null) {
+			imageListener.onScale(currentScale);
+			imageListener.onPosition(next.x, next.y);
+		}
+
+		image.redraw();
+	}
+
+	protected boolean handleDrag(float x, float y) {
+		current.x = x;
+		current.y = y;
+
+		float diffX = (current.x - last.x);
+		float diffY = (current.y - last.y);
+
+		if(diffX != 0 || diffY != 0) {
+
+			if(canDragX) next.x += diffX;
+			if(canDragY) next.y += diffY;
+
+			boundCoordinates();
+
+			last.x = current.x;
+			last.y = current.y;
+
+			if(canDragX || canDragY) {
+				image.setPosition(next.x, next.y);
+
+				if(imageListener != null) {
+					imageListener.onPosition(next.x, next.y);
+				}
+
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	public void reset() {
+		currentScale = startingScale;
+		next.x = centerX;
+		next.y = centerY;
+		calculateBoundaries();
+		image.setScale(currentScale);
+		image.setPosition(next.x, next.y);
+		image.redraw();
+	}
+
+
+	public float getMaxScale() {
+		return maxScale;
+	}
+
+	public void setMaxScale(float maxScale) {
+		this.maxScale = maxScale;
+	}
+
+	public float getMinScale() {
+		return minScale;
+	}
+
+	public void setMinScale(float minScale) {
+		this.minScale = minScale;
+	}
+
+	public void setOnClickListener(OnClickListener onClickListener) {
+		this.onClickListener = onClickListener;
+	}
+
+	protected void setCanvasWidth(int canvasWidth) {
+		this.canvasWidth = canvasWidth;
+	}
+
+	protected void setCanvasHeight(int canvasHeight) {
+		this.canvasHeight = canvasHeight;
+	}
+
+	protected void setFitScaleHorizontal(float fitScale) {
+		this.fitScaleHorizontal = fitScale;
+	}
+
+	protected void setFitScaleVertical(float fitScaleVertical) {
+		this.fitScaleVertical = fitScaleVertical;
+	}
+
+	protected void boundCoordinates() {
+		if(next.x < boundaryLeft) {
+			next.x = boundaryLeft;
+		}
+		else if(next.x > boundaryRight) {
+			next.x = boundaryRight;
+		}
+
+		if(next.y < boundaryTop) {
+			next.y = boundaryTop;
+		}
+		else if(next.y > boundaryBottom) {
+			next.y = boundaryBottom;
+		}
+	}
+
+	protected void calculateBoundaries() {
+
+		int effectiveWidth = Math.round( (float) imageWidth * currentScale );
+		int effectiveHeight = Math.round( (float) imageHeight * currentScale );
+
+		canDragX = effectiveWidth > displayWidth;
+		canDragY = effectiveHeight > displayHeight;
+
+		if(canDragX) {
+			float diff = (float)(effectiveWidth - displayWidth) / 2.0f;
+			boundaryLeft = centerX - diff;
+			boundaryRight = centerX + diff;
+		}
+
+		if(canDragY) {
+			float diff = (float)(effectiveHeight - displayHeight) / 2.0f;
+			boundaryTop = centerY - diff;
+			boundaryBottom = centerY + diff;
+		}
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/MathUtils.java b/android/experimental/DocumentLoader/src/com/polites/android/MathUtils.java
new file mode 100644
index 0000000..df7f30d
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/MathUtils.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+import android.graphics.PointF;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+
+public class MathUtils {
+
+	public static float distance(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);
+	}
+
+	public static float distance(PointF p1, PointF p2) {
+		float x = p1.x - p2.x;
+		float y = p1.y - p2.y;
+		return FloatMath.sqrt(x * x + y * y);
+	}
+
+	public static float distance(float x1, float y1, float x2, float y2) {
+		float x = x1 - x2;
+		float y = y1 - y2;
+		return FloatMath.sqrt(x * x + y * y);
+	}
+
+	public static void midpoint(MotionEvent event, PointF point) {
+		float x1 = event.getX(0);
+		float y1 = event.getY(0);
+		float x2 = event.getX(1);
+		float y2 = event.getY(1);
+		midpoint(x1, y1, x2, y2, point);
+	}
+
+	public static void midpoint(float x1, float y1, float x2, float y2, PointF point) {
+		point.x = (x1 + x2) / 2.0f;
+		point.y = (y1 + y2) / 2.0f;
+	}
+	/**
+	 * Rotates p1 around p2 by angle degrees.
+	 * @param p1
+	 * @param p2
+	 * @param angle
+	 */
+	public void rotate(PointF p1, PointF p2, float angle) {
+		float px = p1.x;
+		float py = p1.y;
+		float ox = p2.x;
+		float oy = p2.y;
+		p1.x = (FloatMath.cos(angle) * (px-ox) - FloatMath.sin(angle) * (py-oy) + ox);
+		p1.y = (FloatMath.sin(angle) * (px-ox) + FloatMath.cos(angle) * (py-oy) + oy);
+	}
+
+	public static float angle(PointF p1, PointF p2) {
+		return angle(p1.x, p1.y, p2.x, p2.y);
+	}
+
+	public static float angle(float x1, float y1, float x2, float y2) {
+		return (float) Math.atan2(y2 - y1, x2 - x1);
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/MoveAnimation.java b/android/experimental/DocumentLoader/src/com/polites/android/MoveAnimation.java
new file mode 100644
index 0000000..5303d64
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/MoveAnimation.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+
+/**
+ * @author Jason Polites
+ *
+ */
+public class MoveAnimation implements Animation {
+
+	private boolean firstFrame = true;
+
+	private float startX;
+	private float startY;
+
+	private float targetX;
+	private float targetY;
+	private long animationTimeMS = 100;
+	private long totalTime = 0;
+
+	private MoveAnimationListener moveAnimationListener;
+
+	/* (non-Javadoc)
+	 * @see com.polites.android.Animation#update(com.polites.android.GestureImageView, long)
+	 */
+	@Override
+	public boolean update(GestureImageView view, long time) {
+		totalTime += time;
+
+		if(firstFrame) {
+			firstFrame = false;
+			startX = view.getImageX();
+			startY = view.getImageY();
+		}
+
+		if(totalTime < animationTimeMS) {
+
+			float ratio = (float) totalTime / animationTimeMS;
+
+			float newX = ((targetX - startX) * ratio) + startX;
+			float newY = ((targetY - startY) * ratio) + startY;
+
+			if(moveAnimationListener != null) {
+				moveAnimationListener.onMove(newX, newY);
+			}
+
+			return true;
+		}
+		else {
+			if(moveAnimationListener != null) {
+				moveAnimationListener.onMove(targetX, targetY);
+			}
+		}
+
+		return false;
+	}
+
+	public void reset() {
+		firstFrame = true;
+		totalTime = 0;
+	}
+
+
+	public float getTargetX() {
+		return targetX;
+	}
+
+
+	public void setTargetX(float targetX) {
+		this.targetX = targetX;
+	}
+
+
+	public float getTargetY() {
+		return targetY;
+	}
+
+	public void setTargetY(float targetY) {
+		this.targetY = targetY;
+	}
+
+	public long getAnimationTimeMS() {
+		return animationTimeMS;
+	}
+
+	public void setAnimationTimeMS(long animationTimeMS) {
+		this.animationTimeMS = animationTimeMS;
+	}
+
+	public void setMoveAnimationListener(MoveAnimationListener moveAnimationListener) {
+		this.moveAnimationListener = moveAnimationListener;
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/MoveAnimationListener.java b/android/experimental/DocumentLoader/src/com/polites/android/MoveAnimationListener.java
new file mode 100644
index 0000000..a19a265
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/MoveAnimationListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+
+/**
+ * @author Jason Polites
+ *
+ */
+public interface MoveAnimationListener {
+
+	public void onMove(float x, float y);
+
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/VectorF.java b/android/experimental/DocumentLoader/src/com/polites/android/VectorF.java
new file mode 100644
index 0000000..1ff4b19
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/VectorF.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+import android.graphics.PointF;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+
+public class VectorF {
+
+	public float angle;
+	public float length;
+
+	public final PointF start = new PointF();
+	public final PointF end = new PointF();
+
+	public void calculateEndPoint() {
+		end.x = FloatMath.cos(angle) * length + start.x;
+		end.y = FloatMath.sin(angle) * length + start.y;
+	}
+
+	public void setStart(PointF p) {
+		this.start.x = p.x;
+		this.start.y = p.y;
+	}
+
+	public void setEnd(PointF p) {
+		this.end.x = p.x;
+		this.end.y = p.y;
+	}
+
+	public void set(MotionEvent event) {
+		this.start.x = event.getX(0);
+		this.start.y = event.getY(0);
+		this.end.x = event.getX(1);
+		this.end.y = event.getY(1);
+	}
+
+	public float calculateLength() {
+		length = MathUtils.distance(start, end);
+		return length;
+	}
+
+	public float calculateAngle() {
+		angle = MathUtils.angle(start, end);
+		return angle;
+	}
+
+
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimation.java b/android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimation.java
new file mode 100644
index 0000000..673b7f9
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimation.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+import android.graphics.PointF;
+
+
+/**
+ * @author Jason Polites
+ *
+ */
+public class ZoomAnimation implements Animation {
+
+	private boolean firstFrame = true;
+
+	private float touchX;
+	private float touchY;
+
+	private float zoom;
+
+	private float startX;
+	private float startY;
+	private float startScale;
+
+	private float xDiff;
+	private float yDiff;
+	private float scaleDiff;
+
+	private long animationLengthMS = 200;
+	private long totalTime = 0;
+
+	private ZoomAnimationListener zoomAnimationListener;
+
+	/* (non-Javadoc)
+	 * @see com.polites.android.Animation#update(com.polites.android.GestureImageView, long)
+	 */
+	@Override
+	public boolean update(GestureImageView view, long time) {
+		if(firstFrame) {
+			firstFrame = false;
+
+			startX = view.getImageX();
+			startY = view.getImageY();
+			startScale = view.getScale();
+			scaleDiff = (zoom * startScale) - startScale;
+
+			if(scaleDiff > 0) {
+				// Calculate destination for midpoint
+				VectorF vector = new VectorF();
+
+				// Set the touch point as start because we want to move the end
+				vector.setStart(new PointF(touchX, touchY));
+				vector.setEnd(new PointF(startX, startY));
+
+				vector.calculateAngle();
+
+				// Get the current length
+				float length = vector.calculateLength();
+
+				// Multiply length by zoom to get the new length
+				vector.length = length*zoom;
+
+				// Now deduce the new endpoint
+				vector.calculateEndPoint();
+
+				xDiff = vector.end.x - startX;
+				yDiff = vector.end.y - startY;
+			}
+			else {
+				// Zoom out to center
+				xDiff = view.getCenterX() - startX;
+				yDiff = view.getCenterY() - startY;
+			}
+		}
+
+		totalTime += time;
+
+		float ratio = (float) totalTime / (float) animationLengthMS;
+
+		if(ratio < 1) {
+
+			if(ratio > 0) {
+				// we still have time left
+				float newScale = (ratio * scaleDiff) + startScale;
+				float newX = (ratio * xDiff) + startX;
+				float newY = (ratio * yDiff) + startY;
+
+				if(zoomAnimationListener != null) {
+					zoomAnimationListener.onZoom(newScale, newX, newY);
+				}
+			}
+
+			return true;
+		}
+		else {
+
+			float newScale = scaleDiff + startScale;
+			float newX = xDiff + startX;
+			float newY = yDiff + startY;
+
+			if(zoomAnimationListener != null) {
+				zoomAnimationListener.onZoom(newScale, newX, newY);
+				zoomAnimationListener.onComplete();
+			}
+
+			return false;
+		}
+	}
+
+	public void reset() {
+		firstFrame = true;
+		totalTime = 0;
+	}
+
+	public float getZoom() {
+		return zoom;
+	}
+
+	public void setZoom(float zoom) {
+		this.zoom = zoom;
+	}
+
+	public float getTouchX() {
+		return touchX;
+	}
+
+	public void setTouchX(float touchX) {
+		this.touchX = touchX;
+	}
+
+	public float getTouchY() {
+		return touchY;
+	}
+
+	public void setTouchY(float touchY) {
+		this.touchY = touchY;
+	}
+
+	public long getAnimationLengthMS() {
+		return animationLengthMS;
+	}
+
+	public void setAnimationLengthMS(long animationLengthMS) {
+		this.animationLengthMS = animationLengthMS;
+	}
+
+	public ZoomAnimationListener getZoomAnimationListener() {
+		return zoomAnimationListener;
+	}
+
+	public void setZoomAnimationListener(ZoomAnimationListener zoomAnimationListener) {
+		this.zoomAnimationListener = zoomAnimationListener;
+	}
+}
diff --git a/android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimationListener.java b/android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimationListener.java
new file mode 100644
index 0000000..8df4bf6
--- /dev/null
+++ b/android/experimental/DocumentLoader/src/com/polites/android/ZoomAnimationListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2012 Jason Polites
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.polites.android;
+
+
+/**
+ * @author Jason Polites
+ *
+ */
+public interface ZoomAnimationListener {
+	public void onZoom(float scale, float x, float y);
+	public void onComplete();
+}
diff --git a/android/experimental/DocumentLoader/src/org/libreoffice/android/examples/DocumentLoader.java b/android/experimental/DocumentLoader/src/org/libreoffice/android/examples/DocumentLoader.java
index 0778ad9..8ead7a9 100644
--- a/android/experimental/DocumentLoader/src/org/libreoffice/android/examples/DocumentLoader.java
+++ b/android/experimental/DocumentLoader/src/org/libreoffice/android/examples/DocumentLoader.java
@@ -34,11 +34,28 @@ import android.os.Bundle;
 import android.util.Log;
 import android.widget.ImageView;
 
+import com.polites.android.GestureImageView;
+
+import com.sun.star.awt.XBitmap;
+import com.sun.star.awt.XControl;
+import com.sun.star.awt.XDevice;
+import com.sun.star.awt.XToolkit;
+import com.sun.star.beans.PropertyValue;
+import com.sun.star.frame.XComponentLoader;
+import com.sun.star.frame.XController;
+import com.sun.star.frame.XFrame;
+import com.sun.star.frame.XModel;
+import com.sun.star.lang.XEventListener;
+import com.sun.star.lang.XMultiComponentFactory;
+import com.sun.star.lang.XTypeProvider;
+import com.sun.star.uno.Type;
+import com.sun.star.uno.UnoRuntime;
+import com.sun.star.uno.XComponentContext;
+import com.sun.star.view.XRenderable;
+
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 
-import com.sun.star.uno.UnoRuntime;
-
 import org.libreoffice.android.Bootstrap;
 
 public class DocumentLoader
@@ -47,18 +64,18 @@ public class DocumentLoader
     private static String TAG = "DocumentLoader";
 
     class MyXController
-        implements com.sun.star.frame.XController {
+        implements XController {
 
-        com.sun.star.frame.XModel model;
-        com.sun.star.frame.XFrame frame;
+        XFrame frame;
+        XModel model;
 
-        public void attachFrame(com.sun.star.frame.XFrame frame)
+        public void attachFrame(XFrame frame)
         {
             Log.i(TAG, "attachFrame");
             this.frame = frame;
         }
 
-        public boolean attachModel(com.sun.star.frame.XModel model)
+        public boolean attachModel(XModel model)
         {
             Log.i(TAG, "attachModel");
             this.model = model;
@@ -82,13 +99,13 @@ public class DocumentLoader
             Log.i(TAG, "restoreViewData");
         }
 
-        public com.sun.star.frame.XModel getModel()
+        public XModel getModel()
         {
             Log.i(TAG, "getModel");
             return model;
         }
 
-        public com.sun.star.frame.XFrame getFrame()
+        public XFrame getFrame()
         {
             Log.i(TAG, "getFrame");
             return frame;
@@ -99,12 +116,12 @@ public class DocumentLoader
             Log.i(TAG, "dispose");
         }
 
-        public void addEventListener(com.sun.star.lang.XEventListener listener)
+        public void addEventListener(XEventListener listener)
         {
             Log.i(TAG, "addEventListener");
         }
 
-        public void removeEventListener(com.sun.star.lang.XEventListener listener)
+        public void removeEventListener(XEventListener listener)
         {
             Log.i(TAG, "removeEventListener");
         }
@@ -117,19 +134,19 @@ public class DocumentLoader
         if (object == null)
             return;
 
-        com.sun.star.lang.XTypeProvider typeProvider = (com.sun.star.lang.XTypeProvider)
-            UnoRuntime.queryInterface(com.sun.star.lang.XTypeProvider.class, object);
+        XTypeProvider typeProvider = (XTypeProvider)
+            UnoRuntime.queryInterface(XTypeProvider.class, object);
 
         Log.i(TAG, "typeProvider is " + (typeProvider != null ? typeProvider.toString() : "null"));
 
         if (typeProvider == null)
             return;
 
-        com.sun.star.uno.Type[] types = typeProvider.getTypes();
+        Type[] types = typeProvider.getTypes();
         if (types == null)
             return;
 
-        for (com.sun.star.uno.Type t : types)
+        for (Type t : types)
             Log.i(TAG, "  " + t.getTypeName());
     }
 
@@ -171,14 +188,13 @@ public class DocumentLoader
             Log.i(TAG, "Sleeping NOW");
             Thread.sleep(20000);
 
-            com.sun.star.uno.XComponentContext xContext = null;
+            XComponentContext xContext = null;
 
             xContext = com.sun.star.comp.helper.Bootstrap.defaultBootstrap_InitialComponentContext();
 
             Log.i(TAG, "xContext is" + (xContext!=null ? " not" : "") + " null");
 
-            com.sun.star.lang.XMultiComponentFactory xMCF =
-                xContext.getServiceManager();
+            XMultiComponentFactory xMCF = xContext.getServiceManager();
 
             Log.i(TAG, "xMCF is" + (xMCF!=null ? " not" : "") + " null");
 
@@ -204,23 +220,22 @@ public class DocumentLoader
 
             Bootstrap.initUCBHelper();
 
-            com.sun.star.frame.XComponentLoader xCompLoader = (com.sun.star.frame.XComponentLoader)
-                UnoRuntime.queryInterface(com.sun.star.frame.XComponentLoader.class, oDesktop);
+            XComponentLoader xCompLoader = (XComponentLoader)
+                UnoRuntime.queryInterface(XComponentLoader.class, oDesktop);
 
             Log.i(TAG, "xCompLoader is" + (xCompLoader!=null ? " not" : "") + " null");
 
             // Load the wanted document(s)
             String[] inputs = input.split(":");
             for (int i = 0; i < inputs.length; i++) {
-                com.sun.star.beans.PropertyValue loadProps[] =
-                    new com.sun.star.beans.PropertyValue[3];
-                loadProps[0] = new com.sun.star.beans.PropertyValue();
+                PropertyValue loadProps[] = new PropertyValue[3];
+                loadProps[0] = new PropertyValue();
                 loadProps[0].Name = "Hidden";
                 loadProps[0].Value = new Boolean(true);
-                loadProps[1] = new com.sun.star.beans.PropertyValue();
+                loadProps[1] = new PropertyValue();
                 loadProps[1].Name = "ReadOnly";
                 loadProps[1].Value = new Boolean(true);
-                loadProps[2] = new com.sun.star.beans.PropertyValue();
+                loadProps[2] = new PropertyValue();
                 loadProps[2].Name = "Preview";
                 loadProps[2].Value = new Boolean(true);
 
@@ -234,53 +249,33 @@ public class DocumentLoader
 
                 dumpUNOObject("oDoc", oDoc);
 
-                // Test stuff, try creating various services, see what types
-                // they offer, stuff that is hard to find out by reading
-                // nonexistent useful documentation.
-
-                Log.i(TAG, "Attempting to load private:factory/swriter");
-
-                Object swriter =
-                    xCompLoader.loadComponentFromURL
-                    ("private:factory/swriter", "_blank", 0, loadProps);
-
-                dumpUNOObject("swriter", swriter);
-
-                Object frameControl = xMCF.createInstanceWithContext
-                    ("com.sun.star.frame.FrameControl", xContext);
-
-                dumpUNOObject("frameControl", frameControl);
-
-                com.sun.star.awt.XControl control = (com.sun.star.awt.XControl)
-                    UnoRuntime.queryInterface(com.sun.star.awt.XControl.class, frameControl);
-
                 Object toolkit = xMCF.createInstanceWithContext
                     ("com.sun.star.awt.Toolkit", xContext);
 
                 dumpUNOObject("toolkit", toolkit);
 
-                com.sun.star.awt.XToolkit xToolkit = (com.sun.star.awt.XToolkit)
-                    UnoRuntime.queryInterface(com.sun.star.awt.XToolkit.class, toolkit);
+                XToolkit xToolkit = (XToolkit)
+                    UnoRuntime.queryInterface(XToolkit.class, toolkit);
 
-                com.sun.star.awt.XDevice device = xToolkit.createScreenCompatibleDevice(1024, 1024);
+                XDevice device = xToolkit.createScreenCompatibleDevice(1024, 1024);
 
                 dumpUNOObject("device", device);
 
                 // I guess the XRenderable thing might be what we want to use,
                 // having the code pretend it is printing?
 
-                com.sun.star.view.XRenderable renderBabe = (com.sun.star.view.XRenderable)
-                    UnoRuntime.queryInterface(com.sun.star.view.XRenderable.class, oDoc);
+                XRenderable renderBabe = (XRenderable)
+                    UnoRuntime.queryInterface(XRenderable.class, oDoc);
 
-                com.sun.star.beans.PropertyValue renderProps[] =
-                    new com.sun.star.beans.PropertyValue[3];
-                renderProps[0] = new com.sun.star.beans.PropertyValue();
+                PropertyValue renderProps[] =
+                    new PropertyValue[3];
+                renderProps[0] = new PropertyValue();
                 renderProps[0].Name = "IsPrinter";
                 renderProps[0].Value = new Boolean(true);
-                renderProps[1] = new com.sun.star.beans.PropertyValue();
+                renderProps[1] = new PropertyValue();
                 renderProps[1].Name = "RenderDevice";
                 renderProps[1].Value = device;
-                renderProps[2] = new com.sun.star.beans.PropertyValue();
+                renderProps[2] = new PropertyValue();
                 renderProps[2].Name = "View";
                 renderProps[2].Value = new MyXController();
 
@@ -288,7 +283,7 @@ public class DocumentLoader
 
                 renderBabe.render(0, oDoc, renderProps);
 
-                com.sun.star.awt.XBitmap bitmap = device.createBitmap(0, 0, 1024, 1024);
+                XBitmap bitmap = device.createBitmap(0, 0, 1024, 1024);
 
                 byte[] image = bitmap.getDIB();
 
@@ -328,7 +323,7 @@ public class DocumentLoader
 
                 Bootstrap.twiddle_BGR_to_RGBA(image, imagebb.getInt(0x0a), width, height, argb);
 
-                ImageView imageView = new ImageView(this);
+                ImageView imageView = new GestureImageView(this);
 
                 Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                 bm.copyPixelsFromBuffer(argb);


More information about the Libreoffice-commits mailing list