diff options
author | Tor Lillqvist <tlillqvist@suse.com> | 2012-05-31 22:31:58 +0300 |
---|---|---|
committer | Tor Lillqvist <tlillqvist@suse.com> | 2012-05-31 22:32:40 +0300 |
commit | 5dc2b43e46c9f2c77f3ca236511eaf615a62f672 (patch) | |
tree | ac2412e7114004f274d34252984e234b2889e656 /android/experimental | |
parent | 984239ef0d4563d30606f30e640b1e3b2d8f0c0e (diff) |
Use Jason Polites's GestureImageView, and some cleanup
Change-Id: I916c36b3b55681cdf8f0d1ffd0236e54f3b67b86
Diffstat (limited to 'android/experimental')
15 files changed, 2076 insertions, 57 deletions
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 000000000000..993620893f91 --- /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 000000000000..fb0728b7bf13 --- /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 000000000000..3124b6201464 --- /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 000000000000..b9611d51c040 --- /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 000000000000..ab3007a14b00 --- /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 000000000000..1cde6e4b4889 --- /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 000000000000..4a52358216d5 --- /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 000000000000..76751d145b58 --- /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 000000000000..df7f30db54a7 --- /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 000000000000..5303d646672b --- /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 000000000000..a19a265e5844 --- /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 000000000000..1ff4b19d7e4f --- /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 000000000000..673b7f9cb148 --- /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 000000000000..8df4bf641952 --- /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 0778ad95143e..8ead7a96240d 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); |