From f8878f8fbbb912aae0f856e6e1b6b0c2c17d6380 Mon Sep 17 00:00:00 2001 From: Florens Douwes Date: Thu, 31 Jul 2014 22:55:25 +0200 Subject: [PATCH] Changed the imageload and displayer. Now using Ion to load the images, with a custom file cache. This allows downloads bigger than the available ram, and a progressbar. The subsampling-scale-image-view allows huge images to be displayed, even when there's not a lot of ram. --- Clover/app/build.gradle | 1 + Clover/app/src/main/assets/html/licences.html | 62 +- .../labs/subscaleview/ImageViewState.java | 55 + .../SubsamplingScaleImageView.java | 1802 +++++++++++++++++ .../java/org/floens/chan/ChanApplication.java | 27 +- .../org/floens/chan/core/net/GIFRequest.java | 59 - .../chan/ui/activity/ImageViewActivity.java | 88 +- .../floens/chan/ui/adapter/PinnedAdapter.java | 3 +- .../chan/ui/fragment/ImageViewFragment.java | 25 +- .../chan/ui/fragment/ReplyFragment.java | 2 +- .../java/org/floens/chan/ui/view/GIFView.java | 37 +- .../org/floens/chan/ui/view/PostView.java | 4 +- .../chan/ui/view/ThumbnailImageView.java | 287 +-- .../java/org/floens/chan/utils/FileCache.java | 86 + .../java/uk/co/senab/photoview/Compat.java | 60 - .../photoview/DefaultOnDoubleTapListener.java | 98 - .../uk/co/senab/photoview/IPhotoView.java | 260 --- .../java/uk/co/senab/photoview/PhotoView.java | 256 --- .../co/senab/photoview/PhotoViewAttacher.java | 1085 ---------- .../gestures/CupcakeGestureDetector.java | 143 -- .../gestures/EclairGestureDetector.java | 85 - .../gestures/FroyoGestureDetector.java | 63 - .../photoview/gestures/GestureDetector.java | 28 - .../photoview/gestures/OnGestureListener.java | 27 - .../gestures/VersionedGestureDetector.java | 42 - .../uk/co/senab/photoview/log/LogManager.java | 35 - .../uk/co/senab/photoview/log/Logger.java | 116 -- .../co/senab/photoview/log/LoggerDefault.java | 76 - .../scrollerproxy/GingerScroller.java | 68 - .../photoview/scrollerproxy/IcsScroller.java | 33 - .../scrollerproxy/PreGingerScroller.java | 58 - .../scrollerproxy/ScrollerProxy.java | 48 - .../progress_primary_holo_light.9.png | Bin 0 -> 873 bytes .../progress_primary_holo_light.9.png | Bin 0 -> 561 bytes .../progress_primary_holo_light.9.png | Bin 0 -> 1285 bytes .../progress_primary_holo_light.9.png | Bin 0 -> 3029 bytes .../main/res/drawable/progressbar_no_bg.xml | 24 + Clover/app/src/main/res/values/dimens.xml | 1 - 38 files changed, 2297 insertions(+), 2847 deletions(-) create mode 100644 Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.java create mode 100644 Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.java delete mode 100644 Clover/app/src/main/java/org/floens/chan/core/net/GIFRequest.java create mode 100644 Clover/app/src/main/java/org/floens/chan/utils/FileCache.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/Compat.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/DefaultOnDoubleTapListener.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/IPhotoView.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/PhotoView.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/PhotoViewAttacher.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/gestures/CupcakeGestureDetector.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/gestures/EclairGestureDetector.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/gestures/FroyoGestureDetector.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/gestures/GestureDetector.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/gestures/OnGestureListener.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/gestures/VersionedGestureDetector.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/log/LogManager.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/log/Logger.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/log/LoggerDefault.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/GingerScroller.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/IcsScroller.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/PreGingerScroller.java delete mode 100644 Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/ScrollerProxy.java create mode 100644 Clover/app/src/main/res/drawable-hdpi/progress_primary_holo_light.9.png create mode 100644 Clover/app/src/main/res/drawable-mdpi/progress_primary_holo_light.9.png create mode 100644 Clover/app/src/main/res/drawable-xhdpi/progress_primary_holo_light.9.png create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/progress_primary_holo_light.9.png create mode 100644 Clover/app/src/main/res/drawable/progressbar_no_bg.xml diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle index 3c3de90f..000dd71c 100644 --- a/Clover/app/build.gradle +++ b/Clover/app/build.gradle @@ -71,6 +71,7 @@ dependencies { compile 'com.j256.ormlite:ormlite-core:4.48' compile 'com.j256.ormlite:ormlite-android:4.48' compile 'com.android.support:support-v13:18.0.+' + compile 'com.koushikdutta.ion:ion:1.+' compile files('libs/httpclientandroidlib-1.2.1.jar') } diff --git a/Clover/app/src/main/assets/html/licences.html b/Clover/app/src/main/assets/html/licences.html index 98e5ea2d..7f1dc949 100644 --- a/Clover/app/src/main/assets/html/licences.html +++ b/Clover/app/src/main/assets/html/licences.html @@ -17,27 +17,6 @@ -

PhotoView

- https://github.com/chrisbanes/PhotoView -
-        
-Copyright 2011, 2012 Chris Banes
-
-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.
-        
-    
-
-

This software includes several Android classes from the Android Open Source Project.

         
@@ -141,5 +120,46 @@ limitations under the License.
     

+

Ion

+ https://github.com/koush/ion +
+        
+Copyright 2013 Koushik Dutta
+
+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.
+        
+    
+
+ +

subsampling-scale-image-view

+ https://github.com/davemorrissey/subsampling-scale-image-view +
+        
+Copyright 2014 David Morrissey
+
+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.
+        
+    
+
diff --git a/Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.java b/Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.java new file mode 100644 index 00000000..b36c36fc --- /dev/null +++ b/Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.java @@ -0,0 +1,55 @@ +/* +Copyright 2014 David Morrissey + +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.davemorrissey.labs.subscaleview; + +import android.graphics.PointF; + +import java.io.Serializable; + +/** + * Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate. + */ +public class ImageViewState implements Serializable { + + private float scale; + + private float centerX; + + private float centerY; + + private int orientation; + + public ImageViewState(float scale, PointF center, int orientation) { + this.scale = scale; + this.centerX = center.x; + this.centerY = center.y; + this.orientation = orientation; + } + + public float getScale() { + return scale; + } + + public PointF getCenter() { + return new PointF(centerX, centerY); + } + + public int getOrientation() { + return orientation; + } + +} diff --git a/Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.java b/Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.java new file mode 100644 index 00000000..c722f817 --- /dev/null +++ b/Clover/app/src/main/java/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.java @@ -0,0 +1,1802 @@ +/* +Copyright 2014 David Morrissey + +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.davemorrissey.labs.subscaleview; + +import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.os.AsyncTask; +import android.os.Build.VERSION; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.FloatMath; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Displays an image subsampled as necessary to avoid loading too much image data into memory. After a pinch to zoom in, + * a set of image tiles subsampled at higher resolution are loaded and displayed over the base layer. During pinch and + * zoom, tiles off screen or higher/lower resolution than required are discarded from memory. + * + * Tiles over 2048px are not used due to hardware rendering limitations. + * + * This view will not work very well with images that are far larger in one dimension than the other because the tile grid + * for each subsampling level has the same number of rows as columns, so each tile has the same width:height ratio as + * the source image. This could result in image data totalling several times the screen area being loaded. + * + * v prefixes - coordinates, translations and distances measured in screen (view) pixels + * s prefixes - coordinates, translations and distances measured in source image pixels (scaled) + */ +@SuppressWarnings("unused") +public class SubsamplingScaleImageView extends View { + + private static final String TAG = SubsamplingScaleImageView.class.getSimpleName(); + + /** Attempt to use EXIF information on the image to rotate it. Works for external files only. */ + public static final int ORIENTATION_USE_EXIF = -1; + /** Display the image file in its native orientation. */ + public static final int ORIENTATION_0 = 0; + /** Rotate the image 90 degrees clockwise. */ + public static final int ORIENTATION_90 = 90; + /** Rotate the image 180 degrees. */ + public static final int ORIENTATION_180 = 180; + /** Rotate the image 270 degrees clockwise. */ + public static final int ORIENTATION_270 = 270; + + private static final List VALID_ORIENTATIONS = Arrays.asList(ORIENTATION_0, ORIENTATION_90, ORIENTATION_180, ORIENTATION_270, ORIENTATION_USE_EXIF); + + /** During zoom animation, keep the point of the image that was tapped in the same place, and scale the image around it. */ + public static final int ZOOM_FOCUS_FIXED = 1; + /** During zoom animation, move the point of the image that was tapped to the center of the screen. */ + public static final int ZOOM_FOCUS_CENTER = 2; + /** Zoom in to and center the tapped point immediately without animating. */ + public static final int ZOOM_FOCUS_CENTER_IMMEDIATE = 3; + + private static final List VALID_ZOOM_STYLES = Arrays.asList(ZOOM_FOCUS_FIXED, ZOOM_FOCUS_CENTER, ZOOM_FOCUS_CENTER_IMMEDIATE); + + /** Quadratic ease out. Not recommended for scale animation, but good for panning. */ + public static final int EASE_OUT_QUAD = 1; + /** Quadratic ease in and out. */ + public static final int EASE_IN_OUT_QUAD = 2; + + private static final List VALID_EASING_STYLES = Arrays.asList(EASE_IN_OUT_QUAD, EASE_OUT_QUAD); + + /** Don't allow the image to be panned off screen. As much of the image as possible is always displayed, centered in the view when it is smaller. This is the best option for galleries. */ + public static final int PAN_LIMIT_INSIDE = 1; + /** Allows the image to be panned until it is just off screen, but no further. The edge of the image will stop when it is flush with the screen edge. */ + public static final int PAN_LIMIT_OUTSIDE = 2; + /** Allows the image to be panned until a corner reaches the center of the screen but no further. Useful when you want to pan any spot on the image to the exact center of the screen. */ + public static final int PAN_LIMIT_CENTER = 3; + + private static final List VALID_PAN_LIMITS = Arrays.asList(PAN_LIMIT_INSIDE, PAN_LIMIT_OUTSIDE, PAN_LIMIT_CENTER); + + // Overlay tile boundaries and other info + private boolean debug = false; + + // Image orientation setting + private int orientation = ORIENTATION_0; + + // Max scale allowed (prevent infinite zoom) + private float maxScale = 2F; + + // Density to reach before loading higher resolution tiles + private int minimumTileDpi = -1; + + // Pan limiting style + private int panLimit = PAN_LIMIT_INSIDE; + + // Gesture detection settings + private boolean panEnabled = true; + private boolean zoomEnabled = true; + + // Double tap zoom behaviour + private float doubleTapZoomScale = 1F; + private int doubleTapZoomStyle = ZOOM_FOCUS_FIXED; + + // Current scale and scale at start of zoom + private float scale; + private float scaleStart; + + // Screen coordinate of top-left corner of source image + private PointF vTranslate; + private PointF vTranslateStart; + + // Source coordinate to center on, used when new position is set externally before view is ready + private Float pendingScale; + private PointF sPendingCenter; + private PointF sRequestedCenter; + + // Source image dimensions and orientation - dimensions relate to the unrotated image + private int sWidth; + private int sHeight; + private int sOrientation; + + // Is two-finger zooming in progress + private boolean isZooming; + // Is one-finger panning in progress + private boolean isPanning; + // Max touches used in current gesture + private int maxTouchCount; + + // Fling detector + private GestureDetector detector; + + // Tile decoder + private BitmapRegionDecoder decoder; + private final Object decoderLock = new Object(); + + // Sample size used to display the whole image when fully zoomed out + private int fullImageSampleSize; + + // Map of zoom level to tile grid + private Map> tileMap; + + // Debug values + private PointF vCenterStart; + private float vDistStart; + + // Scale and center animation tracking + private Anim anim; + + // Whether a ready notification has been sent to subclasses + private boolean readySent = false; + + // Long click listener + private OnLongClickListener onLongClickListener; + + // Long click handler + private Handler handler; + private static final int MESSAGE_LONG_CLICK = 1; + + // Paint objects created once and reused for efficiency + private Paint bitmapPaint; + private Paint debugPaint; + + public SubsamplingScaleImageView(Context context, AttributeSet attr) { + super(context, attr); + setMinimumDpi(160); + setDoubleTapZoomDpi(160); + this.handler = new Handler(new Handler.Callback() { + public boolean handleMessage(Message message) { + if (message.what == MESSAGE_LONG_CLICK && onLongClickListener != null) { + maxTouchCount = 0; + SubsamplingScaleImageView.super.setOnLongClickListener(onLongClickListener); + performLongClick(); + SubsamplingScaleImageView.super.setOnLongClickListener(null); + } + return true; + } + }); + this.detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (panEnabled && readySent && vTranslate != null && (Math.abs(e1.getX() - e2.getX()) > 50 || Math.abs(e1.getY() - e2.getY()) > 50) && (Math.abs(velocityX) > 500 || Math.abs(velocityY) > 500) && !isZooming) { + PointF vTranslateEnd = new PointF(vTranslate.x + (velocityX * 0.25f), vTranslate.y + (velocityY * 0.25f)); + float sCenterXEnd = ((getWidth()/2) - vTranslateEnd.x)/scale; + float sCenterYEnd = ((getHeight()/2) - vTranslateEnd.y)/scale; + new AnimationBuilder(new PointF(sCenterXEnd, sCenterYEnd)).withEasing(EASE_OUT_QUAD).withPanLimited(false).start(); + return true; + } + return super.onFling(e1, e2, velocityX, velocityY); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + performClick(); + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (zoomEnabled && readySent && vTranslate != null) { + float doubleTapZoomScale = Math.min(maxScale, SubsamplingScaleImageView.this.doubleTapZoomScale); + boolean zoomIn = scale <= doubleTapZoomScale * 0.9; + float targetScale = zoomIn ? doubleTapZoomScale : Math.min(getWidth() / (float) sWidth(), getHeight() / (float) sHeight()); + PointF targetSCenter = viewToSourceCoord(new PointF(e.getX(), e.getY())); + if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER_IMMEDIATE) { + setScaleAndCenter(targetScale, targetSCenter); + } else if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER || !zoomIn) { + new AnimationBuilder(targetScale, targetSCenter).withInterruptible(false).start(); + } else if (doubleTapZoomStyle == ZOOM_FOCUS_FIXED) { + new AnimationBuilder(targetScale, targetSCenter, new PointF(e.getX(), e.getY())).withInterruptible(false).start(); + } + + invalidate(); + return true; + } + return super.onDoubleTapEvent(e); + } + }); + + // Handle XML attributes + /*if (attr != null) { + TypedArray typedAttr = getContext().obtainStyledAttributes(attr, styleable.SubsamplingScaleImageView); + if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_assetName)) { + String assetName = typedAttr.getString(styleable.SubsamplingScaleImageView_assetName); + if (assetName != null && assetName.length() > 0) { + setImageAsset(assetName); + } + } + if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_panEnabled)) { + setPanEnabled(typedAttr.getBoolean(styleable.SubsamplingScaleImageView_panEnabled, true)); + } + if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_zoomEnabled)) { + setZoomEnabled(typedAttr.getBoolean(styleable.SubsamplingScaleImageView_zoomEnabled, true)); + } + }*/ + } + + public SubsamplingScaleImageView(Context context) { + this(context, null); + } + + /** + * Sets the image orientation. It's best to call this before setting the image file or asset, because it may waste + * loading of tiles. However, this can be freely called at any time. + */ + public final void setOrientation(int orientation) { + if (!VALID_ORIENTATIONS.contains(orientation)) { + throw new IllegalArgumentException("Invalid orientation: " + orientation); + } + this.orientation = orientation; + reset(false); + invalidate(); + requestLayout(); + } + + /** + * Display an image from a file in internal or external storage. + * @param extFile URI of the file to display. + */ + public final void setImageFile(String extFile) { + reset(true); + BitmapInitTask task = new BitmapInitTask(this, getContext(), extFile, false); + task.execute(); + invalidate(); + } + + /** + * Display an image from a file in internal or external storage, starting with a given orientation setting, scale + * and center. This is the best method to use when you want scale and center to be restored after screen orientation + * change; it avoids any redundant loading of tiles in the wrong orientation. + * @param extFile URI of the file to display. + * @param state State to be restored. Nullable. + */ + public final void setImageFile(String extFile, ImageViewState state) { + reset(true); + restoreState(state); + BitmapInitTask task = new BitmapInitTask(this, getContext(), extFile, false); + task.execute(); + invalidate(); + } + + /** + * Display an image from a file in assets. + * @param assetName asset name. + */ + public final void setImageAsset(String assetName) { + setImageAsset(assetName, null); + } + + /** + * Display an image from a file in assets, starting with a given orientation setting, scale and center. This is the + * best method to use when you want scale and center to be restored after screen orientation change; it avoids any + * redundant loading of tiles in the wrong orientation. + * @param assetName asset name. + * @param state State to be restored. Nullable. + */ + public final void setImageAsset(String assetName, ImageViewState state) { + reset(true); + restoreState(state); + BitmapInitTask task = new BitmapInitTask(this, getContext(), assetName, true); + task.execute(); + invalidate(); + } + + /** + * Reset all state before setting/changing image or setting new rotation. + */ + private void reset(boolean newImage) { + scale = 0f; + scaleStart = 0f; + vTranslate = null; + vTranslateStart = null; + pendingScale = 0f; + sPendingCenter = null; + sRequestedCenter = null; + isZooming = false; + isPanning = false; + maxTouchCount = 0; + fullImageSampleSize = 0; + vCenterStart = null; + vDistStart = 0; + anim = null; + if (newImage) { + if (decoder != null) { + synchronized (decoderLock) { + decoder.recycle(); + decoder = null; + } + } + sWidth = 0; + sHeight = 0; + sOrientation = 0; + readySent = false; + } + if (tileMap != null) { + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + for (Tile tile : tileMapEntry.getValue()) { + tile.visible = false; + if (tile.bitmap != null) { + tile.bitmap.recycle(); + tile.bitmap = null; + } + } + } + tileMap = null; + } + } + + /** + * On resize, preserve center and scale. Various behaviours are possible, override this method to use another. + */ + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (readySent) { + setScaleAndCenter(getScale(), getCenter()); + } + } + + /** + * Measures the width and height of the view, preserving the aspect ratio of the image displayed if wrap_content is + * used. The image will scale within this box, not resizing the view as it is zoomed. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; + boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; + int width = parentWidth; + int height = parentHeight; + if (sWidth > 0 && sHeight > 0) { + if (resizeWidth && resizeHeight) { + width = sWidth(); + height = sHeight(); + } else if (resizeHeight) { + height = (int)((((double)sHeight()/(double)sWidth()) * width)); + } else if (resizeWidth) { + width = (int)((((double)sWidth()/(double)sHeight()) * height)); + } + } + width = Math.max(width, getSuggestedMinimumWidth()); + height = Math.max(height, getSuggestedMinimumHeight()); + setMeasuredDimension(width, height); + } + + /** + * Handle touch events. One finger pans, and two finger pinch and zoom plus panning. + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + PointF vCenterEnd; + float vDistEnd; + // During non-interruptible anims, ignore all touch events + if (anim != null && !anim.interruptible) { + getParent().requestDisallowInterceptTouchEvent(true); + return true; + } else { + anim = null; + } + + // Abort if not ready + if (vTranslate == null) { + return true; + } + // Detect flings, taps and double taps + if (detector == null || detector.onTouchEvent(event)) { + return true; + } + + int touchCount = event.getPointerCount(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_1_DOWN: + case MotionEvent.ACTION_POINTER_2_DOWN: + anim = null; + getParent().requestDisallowInterceptTouchEvent(true); + maxTouchCount = Math.max(maxTouchCount, touchCount); + if (touchCount >= 2) { + if (zoomEnabled) { + // Start pinch to zoom. Calculate distance between touch points and center point of the pinch. + float distance = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)); + scaleStart = scale; + vDistStart = distance; + vTranslateStart = new PointF(vTranslate.x, vTranslate.y); + vCenterStart = new PointF((event.getX(0) + event.getX(1))/2, (event.getY(0) + event.getY(1))/2); + } else { + // Abort all gestures on second touch + maxTouchCount = 0; + } + // Cancel long click timer + handler.removeMessages(MESSAGE_LONG_CLICK); + } else { + // Start one-finger pan + vTranslateStart = new PointF(vTranslate.x, vTranslate.y); + vCenterStart = new PointF(event.getX(), event.getY()); + + // Start long click timer + handler.sendEmptyMessageDelayed(MESSAGE_LONG_CLICK, 600); + } + return true; + case MotionEvent.ACTION_MOVE: + boolean consumed = false; + if (maxTouchCount > 0) { + if (touchCount >= 2) { + // Calculate new distance between touch points, to scale and pan relative to start values. + vDistEnd = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)); + vCenterEnd = new PointF((event.getX(0) + event.getX(1))/2, (event.getY(0) + event.getY(1))/2); + + if (zoomEnabled && (distance(vCenterStart.x, vCenterEnd.x, vCenterStart.y, vCenterEnd.y) > 5 || Math.abs(vDistEnd - vDistStart) > 5 || isPanning)) { + isZooming = true; + isPanning = true; + consumed = true; + + scale = Math.min(maxScale, (vDistEnd / vDistStart) * scaleStart); + + if (scale <= minScale()) { + // Minimum scale reached so don't pan. Adjust start settings so any expand will zoom in. + vDistStart = vDistEnd; + scaleStart = minScale(); + vCenterStart = vCenterEnd; + vTranslateStart = vTranslate; + } else if (panEnabled) { + // Translate to place the source image coordinate that was at the center of the pinch at the start + // at the center of the pinch now, to give simultaneous pan + zoom. + float vLeftStart = vCenterStart.x - vTranslateStart.x; + float vTopStart = vCenterStart.y - vTranslateStart.y; + float vLeftNow = vLeftStart * (scale/scaleStart); + float vTopNow = vTopStart * (scale/scaleStart); + vTranslate.x = vCenterEnd.x - vLeftNow; + vTranslate.y = vCenterEnd.y - vTopNow; + } else if (sRequestedCenter != null) { + // With a center specified from code, zoom around that point. + vTranslate.x = (getWidth()/2) - (scale * sRequestedCenter.x); + vTranslate.y = (getHeight()/2) - (scale * sRequestedCenter.y); + } else { + // With no requested center, scale around the image center. + vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2)); + vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2)); + } + + fitToBounds(true); + refreshRequiredTiles(false); + } + } else if (!isZooming) { + // One finger pan - translate the image. We do this calculation even with pan disabled so click + // and long click behaviour is preserved. + float dx = Math.abs(event.getX() - vCenterStart.x); + float dy = Math.abs(event.getY() - vCenterStart.y); + if (dx > 5 || dy > 5 || isPanning) { + consumed = true; + vTranslate.x = vTranslateStart.x + (event.getX() - vCenterStart.x); + vTranslate.y = vTranslateStart.y + (event.getY() - vCenterStart.y); + + float lastX = vTranslate.x; + float lastY = vTranslate.y; + fitToBounds(true); + if (lastX == vTranslate.x || (lastY == vTranslate.y && dy > 10) || isPanning) { + isPanning = true; + } else if (dx > 5) { + // Haven't panned the image, and we're at the left or right edge. Switch to page swipe. + maxTouchCount = 0; + handler.removeMessages(MESSAGE_LONG_CLICK); + getParent().requestDisallowInterceptTouchEvent(false); + } + + if (!panEnabled) { + vTranslate.x = vTranslateStart.x; + vTranslate.y = vTranslateStart.y; + getParent().requestDisallowInterceptTouchEvent(false); + } + + refreshRequiredTiles(false); + } + } + } + if (consumed) { + handler.removeMessages(MESSAGE_LONG_CLICK); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_POINTER_2_UP: + handler.removeMessages(MESSAGE_LONG_CLICK); + if (maxTouchCount > 0 && (isZooming || isPanning)) { + if (isZooming && touchCount == 2) { + // Convert from zoom to pan with remaining touch + isPanning = true; + vTranslateStart = new PointF(vTranslate.x, vTranslate.y); + if (event.getActionIndex() == 1) { + vCenterStart = new PointF(event.getX(0), event.getY(0)); + } else { + vCenterStart = new PointF(event.getX(1), event.getY(1)); + } + } + if (touchCount < 3) { + // End zooming when only one touch point + isZooming = false; + } + if (touchCount < 2) { + // End panning when no touch points + isPanning = false; + maxTouchCount = 0; + } + // Trigger load of tiles now required + refreshRequiredTiles(true); + return true; + } + if (touchCount == 1) { + isZooming = false; + isPanning = false; + maxTouchCount = 0; + } + return true; + } + return super.onTouchEvent(event); + } + + @Override + public void setOnLongClickListener(OnLongClickListener onLongClickListener) { + this.onLongClickListener = onLongClickListener; + } + + /** + * Draw method should not be called until the view has dimensions so the first calls are used as triggers to calculate + * the scaling and tiling required. Once the view is setup, tiles are displayed as they are loaded. + */ + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + createPaints(); + + // If image or view dimensions are not known yet, abort. + if (sWidth == 0 || sHeight == 0 || decoder == null || getWidth() == 0 || getHeight() == 0) { + return; + } + + // On first render with no tile map ready, initialise it and kick off async base image loading. + if (tileMap == null) { + initialiseBaseLayer(getMaxBitmapDimensions(canvas)); + return; + } + + // If waiting to translate to new center position, set translate now + if (sPendingCenter != null && pendingScale != null) { + scale = pendingScale; + vTranslate.x = (getWidth()/2) - (scale * sPendingCenter.x); + vTranslate.y = (getHeight()/2) - (scale * sPendingCenter.y); + sPendingCenter = null; + pendingScale = null; + fitToBounds(true); + refreshRequiredTiles(true); + } + + // On first display of base image set up position, and in other cases make sure scale is correct. + fitToBounds(false); + + // Everything is set up and coordinates are valid. Inform subclasses. + if (!readySent) { + readySent = true; + new Thread(new Runnable() { + public void run() { + onImageReady(); + } + }).start(); + } + + // If animating scale, calculate current scale and center with easing equations + if (anim != null) { + long scaleElapsed = System.currentTimeMillis() - anim.time; + boolean finished = scaleElapsed > anim.duration; + scaleElapsed = Math.min(scaleElapsed, anim.duration); + scale = ease(anim.easing, scaleElapsed, anim.scaleStart, anim.scaleEnd - anim.scaleStart, anim.duration); + + // Apply required animation to the focal point + float vFocusNowX = ease(anim.easing, scaleElapsed, anim.vFocusStart.x, anim.vFocusEnd.x - anim.vFocusStart.x, anim.duration); + float vFocusNowY = ease(anim.easing, scaleElapsed, anim.vFocusStart.y, anim.vFocusEnd.y - anim.vFocusStart.y, anim.duration); + // Find out where the focal point is at this scale and adjust its position to follow the animation path + PointF vFocus = sourceToViewCoord(anim.sCenterEnd); + vTranslate.x -= vFocus.x - vFocusNowX; + vTranslate.y -= vFocus.y - vFocusNowY; + + // For translate anims, showing the image non-centered is never allowed, for scaling anims it is during the animation. + fitToBounds(finished || (anim.scaleStart == anim.scaleEnd)); + refreshRequiredTiles(finished); + if (finished) { + anim = null; + } + invalidate(); + } + + // Optimum sample size for current scale + int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize()); + + // First check for missing tiles - if there are any we need the base layer underneath to avoid gaps + boolean hasMissingTiles = false; + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + if (tileMapEntry.getKey() == sampleSize) { + for (Tile tile : tileMapEntry.getValue()) { + if (tile.visible && (tile.loading || tile.bitmap == null)) { + hasMissingTiles = true; + } + } + } + } + + // Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath. + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) { + for (Tile tile : tileMapEntry.getValue()) { + Rect vRect = convertRect(sourceToViewRect(tile.sRect)); + if (!tile.loading && tile.bitmap != null) { + canvas.drawBitmap(tile.bitmap, null, vRect, bitmapPaint); + if (debug) { + canvas.drawRect(vRect, debugPaint); + } + } else if (tile.loading && debug) { + canvas.drawText("LOADING", vRect.left + 5, vRect.top + 35, debugPaint); + } + if (tile.visible && debug) { + canvas.drawText("ISS " + tile.sampleSize + " RECT " + tile.sRect.top + "," + tile.sRect.left + "," + tile.sRect.bottom + "," + tile.sRect.right, vRect.left + 5, vRect.top + 15, debugPaint); + } + } + } + } + + if (debug) { + canvas.drawText("Scale: " + String.format("%.2f", scale), 5, 15, debugPaint); + canvas.drawText("Translate: " + String.format("%.2f", vTranslate.x) + ":" + String.format("%.2f", vTranslate.y), 5, 35, debugPaint); + PointF center = getCenter(); + canvas.drawText("Source center: " + String.format("%.2f", center.x) + ":" + String.format("%.2f", center.y), 5, 55, debugPaint); + + if (anim != null) { + PointF vCenterStart = sourceToViewCoord(anim.sCenterStart); + PointF vCenterEndRequested = sourceToViewCoord(anim.sCenterEndRequested); + PointF vCenterEnd = sourceToViewCoord(anim.sCenterEnd); + canvas.drawCircle(vCenterStart.x, vCenterStart.y, 10, debugPaint); + canvas.drawCircle(vCenterEndRequested.x, vCenterEndRequested.y, 20, debugPaint); + canvas.drawCircle(vCenterEnd.x, vCenterEnd.y, 25, debugPaint); + canvas.drawCircle(getWidth()/2, getHeight()/2, 30, debugPaint); + } + } + } + + /** + * Creates Paint objects once when first needed. + */ + private void createPaints() { + if (bitmapPaint == null) { + bitmapPaint = new Paint(); + bitmapPaint.setAntiAlias(true); + bitmapPaint.setFilterBitmap(true); + bitmapPaint.setDither(true); + } + if (debugPaint == null && debug) { + debugPaint = new Paint(); + debugPaint.setTextSize(18); + debugPaint.setColor(Color.MAGENTA); + debugPaint.setStyle(Style.STROKE); + } + } + + /** + * Called on first draw when the view has dimensions. Calculates the initial sample size and starts async loading of + * the base layer image - the whole source subsampled as necessary. + */ + private synchronized void initialiseBaseLayer(Point maxTileDimensions) { + + fitToBounds(true); + + // Load double resolution - next level will be split into four tiles and at the center all four are required, + // so don't bother with tiling until the next level 16 tiles are needed. + fullImageSampleSize = calculateInSampleSize(); + if (fullImageSampleSize > 1) { + fullImageSampleSize /= 2; + } + + initialiseTileMap(maxTileDimensions); + + List baseGrid = tileMap.get(fullImageSampleSize); + for (Tile baseTile : baseGrid) { + BitmapTileTask task = new BitmapTileTask(this, decoder, decoderLock, baseTile); + task.execute(); + } + + } + + /** + * Loads the optimum tiles for display at the current scale and translate, so the screen can be filled with tiles + * that are at least as high resolution as the screen. Frees up bitmaps that are now off the screen. + * @param load Whether to load the new tiles needed. Use false while scrolling/panning for performance. + */ + private void refreshRequiredTiles(boolean load) { + int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize()); + RectF vVisRect = new RectF(0, 0, getWidth(), getHeight()); + RectF sVisRect = viewToSourceRect(vVisRect); + + // Load tiles of the correct sample size that are on screen. Discard tiles off screen, and those that are higher + // resolution than required, or lower res than required but not the base layer, so the base layer is always present. + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + for (Tile tile : tileMapEntry.getValue()) { + if (tile.sampleSize < sampleSize || (tile.sampleSize > sampleSize && tile.sampleSize != fullImageSampleSize)) { + tile.visible = false; + if (tile.bitmap != null) { + tile.bitmap.recycle(); + tile.bitmap = null; + } + } + if (tile.sampleSize == sampleSize) { + if (RectF.intersects(sVisRect, convertRect(tile.sRect))) { + tile.visible = true; + if (!tile.loading && tile.bitmap == null && load) { + BitmapTileTask task = new BitmapTileTask(this, decoder, decoderLock, tile); + task.execute(); + } + } else if (tile.sampleSize != fullImageSampleSize) { + tile.visible = false; + if (tile.bitmap != null) { + tile.bitmap.recycle(); + tile.bitmap = null; + } + } + } else if (tile.sampleSize == fullImageSampleSize) { + tile.visible = true; + } + } + } + + } + + /** + * Calculates sample size to fit the source image in given bounds. + */ + private int calculateInSampleSize() { + float adjustedScale = scale; + if (minimumTileDpi > 0) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi)/2; + adjustedScale = (minimumTileDpi/averageDpi) * scale; + } + + int reqWidth = (int)(sWidth() * adjustedScale); + int reqHeight = (int)(sHeight() * adjustedScale); + + // Raw height and width of image + int inSampleSize = 1; + if (reqWidth == 0 || reqHeight == 0) { + return 32; + } + + if (sHeight() > reqHeight || sWidth() > reqWidth) { + + // Calculate ratios of height and width to requested height and width + final int heightRatio = Math.round((float) sHeight() / (float) reqHeight); + final int widthRatio = Math.round((float) sWidth() / (float) reqWidth); + + // Choose the smallest ratio as inSampleSize value, this will guarantee + // a final image with both dimensions larger than or equal to the + // requested height and width. + inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + } + + // We want the actual sample size that will be used, so round down to nearest power of 2. + int power = 1; + while (power * 2 < inSampleSize) { + power = power * 2; + } + + return power; + } + + /** + * Adjusts hypothetical future scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale + * is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an + * animation should be. + * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. + * @param scaleAndTranslate The scale we want and the translation we're aiming for. The values are adjusted to be valid. + */ + private void fitToBounds(boolean center, ScaleAndTranslate scaleAndTranslate) { + if (panLimit == PAN_LIMIT_OUTSIDE && isImageReady()) { + center = false; + } + + PointF vTranslate = scaleAndTranslate.translate; + float scale = limitedScale(scaleAndTranslate.scale); + float scaleWidth = scale * sWidth(); + float scaleHeight = scale * sHeight(); + + if (panLimit == PAN_LIMIT_CENTER && isImageReady()) { + vTranslate.x = Math.max(vTranslate.x, getWidth()/2 - scaleWidth); + vTranslate.y = Math.max(vTranslate.y, getHeight()/2 - scaleHeight); + } else if (center) { + vTranslate.x = Math.max(vTranslate.x, getWidth() - scaleWidth); + vTranslate.y = Math.max(vTranslate.y, getHeight() - scaleHeight); + } else { + vTranslate.x = Math.max(vTranslate.x, -scaleWidth); + vTranslate.y = Math.max(vTranslate.y, -scaleHeight); + } + + float maxTx; + float maxTy; + if (panLimit == PAN_LIMIT_CENTER && isImageReady()) { + maxTx = Math.max(0, getWidth()/2); + maxTy = Math.max(0, getHeight()/2); + } else if (center) { + maxTx = Math.max(0, (getWidth() - scaleWidth) / 2); + maxTy = Math.max(0, (getHeight() - scaleHeight) / 2); + } else { + maxTx = Math.max(0, getWidth()); + maxTy = Math.max(0, getHeight()); + } + + vTranslate.x = Math.min(vTranslate.x, maxTx); + vTranslate.y = Math.min(vTranslate.y, maxTy); + + scaleAndTranslate.scale = scale; + } + + /** + * Adjusts current scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale + * is set so one dimension fills the view and the image is centered on the other dimension. + * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. + */ + private void fitToBounds(boolean center) { + if (vTranslate == null) { + vTranslate = new PointF(0, 0); + } + ScaleAndTranslate input = new ScaleAndTranslate(scale, vTranslate); + fitToBounds(center, input); + scale = input.scale; + } + + /** + * Once source image and view dimensions are known, creates a map of sample size to tile grid. + */ + private void initialiseTileMap(Point maxTileDimensions) { + this.tileMap = new LinkedHashMap>(); + int sampleSize = fullImageSampleSize; + int xTiles = 1; + int yTiles = 1; + while (true) { + int sTileWidth = sWidth()/xTiles; + int sTileHeight = sHeight()/yTiles; + int subTileWidth = sTileWidth/sampleSize; + int subTileHeight = sTileHeight/sampleSize; + while (subTileWidth > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) { + xTiles += 1; + sTileWidth = sWidth()/xTiles; + subTileWidth = sTileWidth/sampleSize; + } + while (subTileHeight > maxTileDimensions.y || (subTileHeight > getHeight() * 1.25 && sampleSize < fullImageSampleSize)) { + yTiles += 1; + sTileHeight = sHeight()/yTiles; + subTileHeight = sTileHeight/sampleSize; + } + List tileGrid = new ArrayList(xTiles * yTiles); + for (int x = 0; x < xTiles; x++) { + for (int y = 0; y < yTiles; y++) { + Tile tile = new Tile(); + tile.sampleSize = sampleSize; + tile.visible = sampleSize == fullImageSampleSize; + tile.sRect = new Rect( + x * sTileWidth, + y * sTileHeight, + (x + 1) * sTileWidth, + (y + 1) * sTileHeight + ); + tileGrid.add(tile); + } + } + tileMap.put(sampleSize, tileGrid); + if (sampleSize == 1) { + break; + } else { + sampleSize /= 2; + } + } + } + + /** + * Called by worker task when decoder is ready and image size and EXIF orientation is known. + */ + private void onImageInited(BitmapRegionDecoder decoder, int sWidth, int sHeight, int sOrientation) { + this.decoder = decoder; + this.sWidth = sWidth; + this.sHeight = sHeight; + this.sOrientation = sOrientation; + requestLayout(); + invalidate(); + } + + /** + * Called by worker task when a tile has loaded. Redraws the view. + */ + private void onTileLoaded() { + invalidate(); + } + + /** + * Async task used to get image details without blocking the UI thread. + */ + private static class BitmapInitTask extends AsyncTask { + private final WeakReference viewRef; + private final WeakReference contextRef; + private final String source; + private final boolean sourceIsAsset; + private BitmapRegionDecoder decoder; + + public BitmapInitTask(SubsamplingScaleImageView view, Context context, String source, boolean sourceIsAsset) { + this.viewRef = new WeakReference(view); + this.contextRef = new WeakReference(context); + this.source = source; + this.sourceIsAsset = sourceIsAsset; + } + + @Override + protected int[] doInBackground(Void... params) { + try { + if (viewRef != null && contextRef != null) { + Context context = contextRef.get(); + if (context != null) { + int exifOrientation = ORIENTATION_0; + if (sourceIsAsset) { + decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(source, AssetManager.ACCESS_RANDOM), true); + } else { + decoder = BitmapRegionDecoder.newInstance(source, true); + try { + ExifInterface exifInterface = new ExifInterface(source); + int orientationAttr = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + if (orientationAttr == ExifInterface.ORIENTATION_NORMAL || orientationAttr == ExifInterface.ORIENTATION_UNDEFINED) { // added undefined + exifOrientation = ORIENTATION_0; + } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_90) { + exifOrientation = ORIENTATION_90; + } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_180) { + exifOrientation = ORIENTATION_180; + } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_270) { + exifOrientation = ORIENTATION_270; + } else { + Log.w(TAG, "Unsupported EXIF orientation: " + orientationAttr); + } + } catch (Exception e) { + Log.w(TAG, "Could not get EXIF orientation of image"); + } + + } + return new int[] { decoder.getWidth(), decoder.getHeight(), exifOrientation }; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to initialise bitmap decoder", e); + } + return null; + } + + @Override + protected void onPostExecute(int[] xyo) { + if (viewRef != null && decoder != null) { + final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get(); + if (subsamplingScaleImageView != null && decoder != null && xyo != null && xyo.length == 3) { + subsamplingScaleImageView.onImageInited(decoder, xyo[0], xyo[1], xyo[2]); + } + } + } + } + + /** + * Async task used to load images without blocking the UI thread. + */ + private static class BitmapTileTask extends AsyncTask { + private final WeakReference viewRef; + private final WeakReference decoderRef; + private final WeakReference decoderLockRef; + private final WeakReference tileRef; + + public BitmapTileTask(SubsamplingScaleImageView view, BitmapRegionDecoder decoder, Object decoderLock, Tile tile) { + this.viewRef = new WeakReference(view); + this.decoderRef = new WeakReference(decoder); + this.decoderLockRef = new WeakReference(decoderLock); + this.tileRef = new WeakReference(tile); + tile.loading = true; + } + + @Override + protected Bitmap doInBackground(Void... params) { + try { + if (decoderRef != null && tileRef != null && viewRef != null) { + final BitmapRegionDecoder decoder = decoderRef.get(); + final Object decoderLock = decoderLockRef.get(); + final Tile tile = tileRef.get(); + final SubsamplingScaleImageView view = viewRef.get(); + if (decoder != null && decoderLock != null && tile != null && view != null && !decoder.isRecycled()) { + synchronized (decoderLock) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = tile.sampleSize; + options.inPreferredConfig = Config.RGB_565; + options.inDither = true; + Bitmap bitmap = decoder.decodeRegion(view.fileSRect(tile.sRect), options); + int rotation = view.getRequiredRotation(); + if (rotation != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(rotation); + bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + return bitmap; + } + } else if (tile != null) { + tile.loading = false; + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to decode tile", e); + } + return null; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (viewRef != null && tileRef != null && bitmap != null) { + final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get(); + final Tile tile = tileRef.get(); + if (subsamplingScaleImageView != null && tile != null) { + tile.bitmap = bitmap; + tile.loading = false; + subsamplingScaleImageView.onTileLoaded(); + } + } + } + } + + private static class Tile { + + private Rect sRect; + private int sampleSize; + private Bitmap bitmap; + private boolean loading; + private boolean visible; + + } + + private static class Anim { + + private float scaleStart; // Scale at start of anim + private float scaleEnd; // Scale at end of anim (target) + private PointF sCenterStart; // Source center point at start + private PointF sCenterEnd; // Source center point at end, adjusted for pan limits + private PointF sCenterEndRequested; // Source center point that was requested, without adjustment + private PointF vFocusStart; // View point that was double tapped + private PointF vFocusEnd; // Where the view focal point should be moved to during the anim + private long duration = 500; // How long the anim takes + private boolean interruptible = true; // Whether the anim can be interrupted by a touch + private int easing = EASE_IN_OUT_QUAD; // Easing style + private long time = System.currentTimeMillis(); // Start time + + } + + private static class ScaleAndTranslate { + private ScaleAndTranslate(float scale, PointF translate) { + this.scale = scale; + this.translate = translate; + } + private float scale; + private PointF translate; + } + + /** + * Set scale, center and orientation from saved state. + */ + private void restoreState(ImageViewState state) { + if (state != null && state.getCenter() != null && VALID_ORIENTATIONS.contains(state.getOrientation())) { + this.orientation = state.getOrientation(); + this.pendingScale = state.getScale(); + this.sPendingCenter = state.getCenter(); + invalidate(); + } + } + + /** + * In SDK 14 and above, use canvas max bitmap width and height instead of the default 2048, to avoid redundant tiling. + */ + private Point getMaxBitmapDimensions(Canvas canvas) { + if (VERSION.SDK_INT >= 14) { + try { + int maxWidth = (Integer)Canvas.class.getMethod("getMaximumBitmapWidth").invoke(canvas); + int maxHeight = (Integer)Canvas.class.getMethod("getMaximumBitmapHeight").invoke(canvas); + return new Point(maxWidth, maxHeight); + } catch (Exception e) { + // Return default + } + } + return new Point(2048, 2048); + } + + /** + * Get source width taking rotation into account. + */ + private int sWidth() { + int rotation = getRequiredRotation(); + if (rotation == 90 || rotation == 270) { + return sHeight; + } else { + return sWidth; + } + } + + /** + * Get source height taking rotation into account. + */ + private int sHeight() { + int rotation = getRequiredRotation(); + if (rotation == 90 || rotation == 270) { + return sWidth; + } else { + return sHeight; + } + } + + /** + * Converts source rectangle from tile, which treats the image file as if it were in the correct orientation already, + * to the rectangle of the image that needs to be loaded. + */ + private Rect fileSRect(Rect sRect) { + if (getRequiredRotation() == 0) { + return sRect; + } else if (getRequiredRotation() == 90) { + return new Rect(sRect.top, sHeight - sRect.right, sRect.bottom, sHeight - sRect.left); + } else if (getRequiredRotation() == 180) { + return new Rect(sWidth - sRect.right, sHeight - sRect.bottom, sWidth - sRect.left, sHeight - sRect.top); + } else { + return new Rect(sWidth - sRect.bottom, sRect.left, sWidth - sRect.top, sRect.right); + } + } + + /** + * Determines the rotation to be applied to tiles, based on EXIF orientation or chosen setting. + */ + private int getRequiredRotation() { + if (orientation == ORIENTATION_USE_EXIF) { + return sOrientation; + } else { + return orientation; + } + } + + /** + * Pythagoras distance between two points. + */ + private float distance(float x0, float x1, float y0, float y1) { + float x = x0 - x1; + float y = y0 - y1; + return FloatMath.sqrt(x * x + y * y); + } + + /** + * Convert screen coordinate to source coordinate. + */ + public final PointF viewToSourceCoord(PointF vxy) { + return viewToSourceCoord(vxy.x, vxy.y); + } + + /** + * Convert screen coordinate to source coordinate. + */ + public final PointF viewToSourceCoord(float vx, float vy) { + if (vTranslate == null) { + return null; + } + float sx = (vx - vTranslate.x)/scale; + float sy = (vy - vTranslate.y)/scale; + return new PointF(sx, sy); + } + + /** + * Convert source coordinate to screen coordinate. + */ + public final PointF sourceToViewCoord(PointF sxy) { + return sourceToViewCoord(sxy.x, sxy.y); + } + + /** + * Convert source coordinate to screen coordinate. + */ + public final PointF sourceToViewCoord(float sx, float sy) { + if (vTranslate == null) { + return null; + } + float vx = (sx * scale) + vTranslate.x; + float vy = (sy * scale) + vTranslate.y; + return new PointF(vx, vy); + } + + /** + * Convert source rect to screen rect. + */ + private RectF sourceToViewRect(Rect sRect) { + return sourceToViewRect(convertRect(sRect)); + } + + /** + * Convert source rect to screen rect. + */ + private RectF sourceToViewRect(RectF sRect) { + PointF vLT = sourceToViewCoord(new PointF(sRect.left, sRect.top)); + PointF vRB = sourceToViewCoord(new PointF(sRect.right, sRect.bottom)); + return new RectF(vLT.x, vLT.y, vRB.x, vRB.y); + } + + /** + * Convert screen rect to source rect. + */ + private RectF viewToSourceRect(RectF vRect) { + PointF sLT = viewToSourceCoord(new PointF(vRect.left, vRect.top)); + PointF sRB = viewToSourceCoord(new PointF(vRect.right, vRect.bottom)); + return new RectF(sLT.x, sLT.y, sRB.x, sRB.y); + } + + /** + * Int to float rect conversion. + */ + private RectF convertRect(Rect rect) { + return new RectF(rect.left, rect.top, rect.right, rect.bottom); + } + + /** + * Float to int rect conversion. + */ + private Rect convertRect(RectF rect) { + return new Rect((int)rect.left, (int)rect.top, (int)rect.right, (int)rect.bottom); + } + + /** + * Get the translation required to place a given source coordinate at the center of the screen. Accepts the desired + * scale as an argument, so this is independent of current translate and scale. The result is fitted to bounds, putting + * the image point as near to the screen center as permitted. + */ + private PointF vTranslateForSCenter(PointF sCenter, float scale) { + PointF vTranslate = new PointF((getWidth()/2) - (sCenter.x * scale), (getHeight()/2) - (sCenter.y * scale)); + ScaleAndTranslate sat = new ScaleAndTranslate(scale, vTranslate); + fitToBounds(true, sat); + return vTranslate; + } + + /** + * Given a requested source center and scale, calculate what the actual center will have to be to keep the image in + * pan limits, keeping the requested center as near to the middle of the screen as allowed. + */ + private PointF limitedSCenter(PointF sCenter, float scale) { + PointF vTranslate = vTranslateForSCenter(sCenter, scale); + int mY = getHeight()/2; + float sx = ((getWidth()/2) - vTranslate.x)/scale; + float sy = ((getHeight()/2) - vTranslate.y)/scale; + return new PointF(sx, sy); + } + + /** + * Returns the minimum allowed scale. + */ + private float minScale() { + return Math.min(getWidth() / (float) sWidth(), getHeight() / (float) sHeight()); + } + + /** + * Adjust a requested scale to be within the allowed limits. + */ + private float limitedScale(float targetScale) { + targetScale = Math.max(minScale(), targetScale); + targetScale = Math.min(maxScale, targetScale); + return targetScale; + } + + /** + * Apply a selected type of easing. + * @param type Easing type, from static fields + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private float ease(int type, long time, float from, float change, long duration) { + switch (type) { + case EASE_IN_OUT_QUAD: + return easeInOutQuad(time, from, change, duration); + case EASE_OUT_QUAD: + return easeOutQuad(time, from, change, duration); + default: + throw new IllegalStateException("Unexpected easing type: " + type); + } + } + + /** + * Quadratic easing for fling. With thanks to Robert Penner - http://gizma.com/easing/ + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private float easeOutQuad(long time, float from, float change, long duration) { + float progress = (float)time/(float)duration; + return -change * progress*(progress-2) + from; + } + + /** + * Quadratic easing for scale and center animations. With thanks to Robert Penner - http://gizma.com/easing/ + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private float easeInOutQuad(long time, float from, float change, long duration) { + float timeF = time/(duration/2f); + if (timeF < 1) { + return (change/2f * timeF * timeF) + from; + } else { + timeF--; + return (-change/2f) * (timeF * (timeF - 2) - 1) + from; + } + } + + /** + * Set the pan limiting style. See static fields. Normally {@link #PAN_LIMIT_INSIDE} is best, for image galleries. + */ + public final void setPanLimit(int panLimit) { + if (!VALID_PAN_LIMITS.contains(panLimit)) { + throw new IllegalArgumentException("Invalid pan limit: " + panLimit); + } + this.panLimit = panLimit; + if (isImageReady()) { + fitToBounds(true); + invalidate(); + } + } + + /** + * Set the maximum scale allowed. A value of 1 means 1:1 pixels at maximum scale. You may wish to set this according + * to screen density - on a retina screen, 1:1 may still be too small. Consider using {@link #setMinimumDpi(int)}, + * which is density aware. + */ + public final void setMaxScale(float maxScale) { + this.maxScale = maxScale; + } + + /** + * Returns the maximum allowed scale. + */ + public float getMaxScale() { + return maxScale; + } + + /** + * This is a screen density aware alternative to {@link #setMaxScale(float)}; it allows you to express the maximum + * allowed scale in terms of the minimum pixel density. This avoids the problem of 1:1 scale still being + * too small on a high density screen. A sensible starting point is 160 - the default used by this view. + * @param dpi Source image pixel density at maximum zoom. + */ + public final void setMinimumDpi(int dpi) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi)/2; + setMaxScale(averageDpi/dpi); + } + + /** + * Returns the minimum allowed scale. + */ + public final float getMinScale() { + return minScale(); + } + + /** + * By default, image tiles are at least as high resolution as the screen. For a retina screen this may not be + * necessary, and may increase the likelihood of an OutOfMemoryError. This method sets a DPI at which higher + * resolution tiles should be loaded. Using a lower number will on average use less memory but result in a lower + * quality image. 160-240dpi will usually be enough. + * @param minimumTileDpi Tile loading threshold. + */ + public void setMinimumTileDpi(int minimumTileDpi) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi)/2; + this.minimumTileDpi = (int)Math.min(averageDpi, minimumTileDpi); + if (isImageReady()) { + reset(false); + invalidate(); + } + } + + /** + * Returns the source point at the center of the view. + */ + public final PointF getCenter() { + int mX = getWidth()/2; + int mY = getHeight()/2; + return viewToSourceCoord(mX, mY); + } + + /** + * Returns the current scale value. + */ + public final float getScale() { + return scale; + } + + /** + * Externally change the scale and translation of the source image. This may be used with getCenter() and getScale() + * to restore the scale and zoom after a screen rotate. + * @param scale New scale to set. + * @param sCenter New source image coordinate to center on the screen, subject to boundaries. + */ + public final void setScaleAndCenter(float scale, PointF sCenter) { + this.anim = null; + this.pendingScale = scale; + this.sPendingCenter = sCenter; + this.sRequestedCenter = sCenter; + invalidate(); + } + + /** + * Fully zoom out and return the image to the middle of the screen. This might be useful if you have a view pager + * and want images to be reset when the user has moved to another page. + */ + public final void resetScaleAndCenter() { + this.anim = null; + this.pendingScale = limitedScale(0); + if (isImageReady()) { + this.sPendingCenter = new PointF(sWidth()/2, sHeight()/2); + } else { + this.sPendingCenter = new PointF(0, 0); + } + invalidate(); + } + + /** + * Subclasses can override this method to be informed when the view is set up and ready for rendering, so they can + * skip their own rendering until the base layer (and its scale and translate) are known. + */ + protected void onImageReady() { + + } + + /** + * Call to find whether the view is initialised and ready for rendering tiles. + */ + public final boolean isImageReady() { + return readySent && vTranslate != null && tileMap != null && sWidth > 0 && sHeight > 0; + } + + /** + * Get source width, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSHeight()} + * for the apparent width. + */ + public final int getSWidth() { + return sWidth; + } + + /** + * Get source height, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSWidth()} + * for the apparent height. + */ + public final int getSHeight() { + return sHeight; + } + + /** + * Returns the orientation setting. This can return {@link #ORIENTATION_USE_EXIF}, in which case it doesn't tell you + * the applied orientation of the image. For that, use {@link #getAppliedOrientation()}. + */ + public final int getOrientation() { + return orientation; + } + + /** + * Returns the actual orientation of the image relative to the source file. This will be based on the source file's + * EXIF orientation if you're using ORIENTATION_USE_EXIF. Values are 0, 90, 180, 270. + */ + public final int getAppliedOrientation() { + return getRequiredRotation(); + } + + /** + * Get the current state of the view (scale, center, orientation) for restoration after rotate. Will return null if + * the view is not ready. + */ + public final ImageViewState getState() { + if (vTranslate != null && sWidth > 0 && sHeight > 0) { + return new ImageViewState(getScale(), getCenter(), getOrientation()); + } + return null; + } + + /** + * Returns true if zoom gesture detection is enabled. + */ + public final boolean isZoomEnabled() { + return zoomEnabled; + } + + /** + * Enable or disable zoom gesture detection. Disabling zoom locks the the current scale. + */ + public final void setZoomEnabled(boolean zoomEnabled) { + this.zoomEnabled = zoomEnabled; + } + + /** + * Returns true if pan gesture detection is enabled. + */ + public final boolean isPanEnabled() { + return panEnabled; + } + + /** + * Enable or disable pan gesture detection. Disabling pan causes the image to be centered. + */ + public final void setPanEnabled(boolean panEnabled) { + this.panEnabled = panEnabled; + if (!panEnabled && vTranslate != null) { + vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2)); + vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2)); + if (isImageReady()) { + refreshRequiredTiles(true); + invalidate(); + } + } + } + + /** + * Set the scale the image will zoom in to when double tapped. This also the scale point where a double tap is interpreted + * as a zoom out gesture - if the scale is greater than 90% of this value, a double tap zooms out. Avoid using values + * greater than the max zoom. + * @param doubleTapZoomScale New value for double tap gesture zoom scale. + */ + public final void setDoubleTapZoomScale(float doubleTapZoomScale) { + this.doubleTapZoomScale = doubleTapZoomScale; + } + + /** + * A density aware alternative to {@link #setDoubleTapZoomScale(float)}; this allows you to express the scale the + * image will zoom in to when double tapped in terms of the image pixel density. Values lower than the max scale will + * be ignored. A sensible starting point is 160 - the default used by this view. + * @param dpi New value for double tap gesture zoom scale. + */ + public final void setDoubleTapZoomDpi(int dpi) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi)/2; + setDoubleTapZoomScale(averageDpi/dpi); + } + + /** + * Set the type of zoom animation to be used for double taps. See static fields. + * @param doubleTapZoomStyle New value for zoom style. + */ + public final void setDoubleTapZoomStyle(int doubleTapZoomStyle) { + if (!VALID_ZOOM_STYLES.contains(doubleTapZoomStyle)) { + throw new IllegalArgumentException("Invalid zoom style: " + doubleTapZoomStyle); + } + this.doubleTapZoomStyle = doubleTapZoomStyle; + } + + /** + * Enables visual debugging, showing tile boundaries and sizes. + */ + public final void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * Creates a panning animation builder, that when started will animate the image to place the given coordinates of + * the image in the center of the screen. If doing this would move the image beyond the edges of the screen, the + * image is instead animated to move the center point as near to the center of the screen as is allowed - it's + * guaranteed to be on screen. + * @param sCenter Target center point + * @return {@link AnimationBuilder} instance. Call {@link com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim. + */ + public AnimationBuilder animateCenter(PointF sCenter) { + if (!isImageReady()) { + return null; + } + return new AnimationBuilder(sCenter); + } + + /** + * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image + * beyond the panning limits, the image is automatically panned during the animation. + * @param scale Target scale. + * @return {@link AnimationBuilder} instance. Call {@link com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim. + */ + public AnimationBuilder animateScale(float scale) { + if (!isImageReady()) { + return null; + } + return new AnimationBuilder(scale); + } + + /** + * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image + * beyond the panning limits, the image is automatically panned during the animation. + * @param scale Target scale. + * @return {@link AnimationBuilder} instance. Call {@link com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim. + */ + public AnimationBuilder animateScaleAndCenter(float scale, PointF sCenter) { + if (!isImageReady()) { + return null; + } + return new AnimationBuilder(scale, sCenter); + } + + /** + * Builder class used to set additional options for a scale animation. Create an instance using {@link #animateScale(float)}, + * then set your options and call {@link #start()}. + */ + public final class AnimationBuilder { + + private final float targetScale; + private final PointF targetSCenter; + private final PointF vFocus; + private long duration = 500; + private int easing = EASE_IN_OUT_QUAD; + private boolean interruptible = true; + private boolean panLimited = true; + + private AnimationBuilder(PointF sCenter) { + this.targetScale = scale; + this.targetSCenter = sCenter; + this.vFocus = null; + } + + private AnimationBuilder(float scale) { + this.targetScale = scale; + this.targetSCenter = getCenter(); + this.vFocus = null; + } + + private AnimationBuilder(float scale, PointF sCenter) { + this.targetScale = scale; + this.targetSCenter = sCenter; + this.vFocus = null; + } + + private AnimationBuilder(float scale, PointF sCenter, PointF vFocus) { + this.targetScale = scale; + this.targetSCenter = sCenter; + this.vFocus = vFocus; + } + + /** + * Desired duration of the anim in milliseconds. Default is 500. + * @param duration duration in milliseconds. + * @return this builder for method chaining. + */ + public AnimationBuilder withDuration(long duration) { + this.duration = duration; + return this; + } + + /** + * Whether the animation can be interrupted with a touch. Default is true. + * @param interruptible interruptible flag. + * @return this builder for method chaining. + */ + public AnimationBuilder withInterruptible(boolean interruptible) { + this.interruptible = interruptible; + return this; + } + + /** + * Set the easing style. See static fields. {@link #EASE_IN_OUT_QUAD} is recommended, and the default. + * @param easing easing style. + * @return this builder for method chaining. + */ + public AnimationBuilder withEasing(int easing) { + if (!VALID_EASING_STYLES.contains(easing)) { + throw new IllegalArgumentException("Unknown easing type: " + easing); + } + this.easing = easing; + return this; + } + + /** + * Only for internal use. When set to true, the animation proceeds towards the actual end point - the nearest + * point to the center allowed by pan limits. When false, animation is in the direction of the requested end + * point and is stopped when the limit for each axis is reached. The latter behaviour is used for flings but + * nothing else. + */ + private AnimationBuilder withPanLimited(boolean panLimited) { + this.panLimited = panLimited; + return this; + } + + /** + * Starts the animation. + */ + public void start() { + float targetScale = limitedScale(this.targetScale); + PointF targetSCenter = panLimited ? limitedSCenter(this.targetSCenter, targetScale) : this.targetSCenter; + anim = new Anim(); + anim.scaleStart = scale; + anim.scaleEnd = targetScale; + anim.time = System.currentTimeMillis(); + anim.sCenterEndRequested = targetSCenter; + anim.sCenterStart = getCenter(); + anim.sCenterEnd = targetSCenter; + anim.vFocusStart = sourceToViewCoord(targetSCenter); + anim.vFocusEnd = new PointF( + getWidth()/2, + getHeight()/2 + ); + anim.duration = duration; + anim.interruptible = interruptible; + anim.easing = easing; + anim.time = System.currentTimeMillis(); + + if (vFocus != null) { + // Calculate where translation will be at the end of the anim + float vTranslateXEnd = vFocus.x - (targetScale * anim.sCenterStart.x); + float vTranslateYEnd = vFocus.y - (targetScale * anim.sCenterStart.y); + ScaleAndTranslate satEnd = new ScaleAndTranslate(targetScale, new PointF(vTranslateXEnd, vTranslateYEnd)); + // Fit the end translation into bounds + fitToBounds(true, satEnd); + // Adjust the position of the focus point at end so image will be in bounds + anim.vFocusEnd = new PointF( + vFocus.x + (satEnd.translate.x - vTranslateXEnd), + vFocus.y + (satEnd.translate.y - vTranslateYEnd) + ); + } + + invalidate(); + } + + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java index 0427fd38..7fa26cca 100644 --- a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java +++ b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java @@ -19,19 +19,20 @@ package org.floens.chan; import android.app.Application; import android.content.SharedPreferences; -import android.os.StrictMode; import android.preference.PreferenceManager; import android.view.ViewConfiguration; import com.android.volley.RequestQueue; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.Volley; +import com.koushikdutta.ion.Ion; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.net.BitmapLruImageCache; import org.floens.chan.database.DatabaseManager; +import org.floens.chan.utils.FileCache; import org.floens.chan.utils.IconCache; import org.floens.chan.utils.Logger; @@ -43,13 +44,18 @@ import java.util.List; public class ChanApplication extends Application { private static final String TAG = "ChanApplication"; + private static final long FILE_CACHE_DISK_SIZE = 50 * 1024 * 1024; // 50mb + private static final String FILE_CACHE_NAME = "filecache"; + private static final int VOLLEY_LRU_CACHE_SIZE = 8 * 1024 * 1024; // 8mb + private static ChanApplication instance; private static RequestQueue volleyRequestQueue; - private static ImageLoader imageLoader; + private static com.android.volley.toolbox.ImageLoader imageLoader; private static BoardManager boardManager; private static WatchManager watchManager; private static ReplyManager replyManager; private static DatabaseManager databaseManager; + private static FileCache fileCache; private List foregroundChangedListeners = new ArrayList<>(); private int activityForegroundCounter = 0; @@ -66,7 +72,7 @@ public class ChanApplication extends Application { return volleyRequestQueue; } - public static ImageLoader getImageLoader() { + public static ImageLoader getVolleyImageLoader() { return imageLoader; } @@ -86,6 +92,10 @@ public class ChanApplication extends Application { return databaseManager; } + public static FileCache getFileCache() { + return fileCache; + } + public static SharedPreferences getPreferences() { return PreferenceManager.getDefaultSharedPreferences(instance); } @@ -113,9 +123,14 @@ public class ChanApplication extends Application { IconCache.createIcons(this); - File cacheDir = new File(getExternalCacheDir() != null ? getExternalCacheDir() : getCacheDir(), Volley.DEFAULT_CACHE_DIR); - volleyRequestQueue = Volley.newRequestQueue(this, null, cacheDir); - imageLoader = new ImageLoader(volleyRequestQueue, new BitmapLruImageCache(1024 * 1024 * 8)); + Ion.getDefault(this).getCache().setMaxSize(1 * 1024 * 1024); + + File cacheDir = getExternalCacheDir() != null ? getExternalCacheDir() : getCacheDir(); + + volleyRequestQueue = Volley.newRequestQueue(this, null, new File(cacheDir, Volley.DEFAULT_CACHE_DIR)); + imageLoader = new ImageLoader(volleyRequestQueue, new BitmapLruImageCache(VOLLEY_LRU_CACHE_SIZE)); + + fileCache = new FileCache(new File(cacheDir, FILE_CACHE_NAME), FILE_CACHE_DISK_SIZE); databaseManager = new DatabaseManager(this); boardManager = new BoardManager(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/GIFRequest.java b/Clover/app/src/main/java/org/floens/chan/core/net/GIFRequest.java deleted file mode 100644 index 7097ccf1..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/net/GIFRequest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.floens.chan.core.net; - -import android.content.Context; - -import com.android.volley.NetworkResponse; -import com.android.volley.ParseError; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.Response.ErrorListener; -import com.android.volley.Response.Listener; -import com.android.volley.toolbox.HttpHeaderParser; - -import org.floens.chan.ui.view.GIFView; - -public class GIFRequest extends Request { - protected final Listener listener; - private final Context context; - - public GIFRequest(String url, Listener listener, ErrorListener errorListener, Context context) { - super(Method.GET, url, errorListener); - - this.listener = listener; - this.context = context; - } - - @Override - protected void deliverResponse(GIFView response) { - listener.onResponse(response); - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - GIFView gifView = new GIFView(context); - boolean success = gifView.setData(response.data); - - if (success) { - return Response.success(gifView, HttpHeaderParser.parseCacheHeaders(response)); - } else { - return Response.error(new ParseError()); - } - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java index 0a57d848..d4595bfc 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImageViewActivity.java @@ -24,7 +24,10 @@ import android.support.v4.view.ViewPager; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; +import android.view.View; import android.view.Window; +import android.widget.FrameLayout; +import android.widget.ProgressBar; import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; @@ -51,6 +54,8 @@ public class ImageViewActivity extends Activity implements ViewPager.OnPageChang private ViewPager viewPager; private ImageViewAdapter adapter; + private ProgressBar progressBar; + private int currentPosition; /** @@ -72,36 +77,54 @@ public class ImageViewActivity extends Activity implements ViewPager.OnPageChang super.onCreate(savedInstanceState); + if (postAdapter == null) { + Logger.e(TAG, "Posts in ImageViewActivity was null"); + finish(); + return; + } + ThemeHelper.setTheme(this); - if (postAdapter != null) { - // Get the posts with images - ArrayList imagePosts = new ArrayList<>(); - for (Post post : postAdapter.getList()) { - if (post.hasImage) { - imagePosts.add(post); - } + progressBar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal); + progressBar.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); + progressBar.setProgressDrawable(getResources().getDrawable(R.drawable.progressbar_no_bg)); + progressBar.setIndeterminate(false); + progressBar.setMax(1000000); + + final FrameLayout decorView = (FrameLayout) getWindow().getDecorView(); + decorView.addView(progressBar); + + progressBar.post(new Runnable() { + @Override + public void run() { + View contentView = decorView.findViewById(android.R.id.content); + progressBar.setY(contentView.getY() - progressBar.getHeight() / 2); } + }); - // Setup our pages and adapter - setContentView(R.layout.image_pager); - viewPager = (ViewPager) findViewById(R.id.image_pager); - adapter = new ImageViewAdapter(getFragmentManager(), this); - adapter.setList(imagePosts); - viewPager.setAdapter(adapter); - viewPager.setOnPageChangeListener(this); - - // Select the right image - for (int i = 0; i < imagePosts.size(); i++) { - if (imagePosts.get(i).no == selectedId) { - viewPager.setCurrentItem(i); - onPageSelected(i); - break; - } + // Get the posts with images + ArrayList imagePosts = new ArrayList<>(); + for (Post post : postAdapter.getList()) { + if (post.hasImage) { + imagePosts.add(post); + } + } + + // Setup our pages and adapter + setContentView(R.layout.image_pager); + viewPager = (ViewPager) findViewById(R.id.image_pager); + adapter = new ImageViewAdapter(getFragmentManager(), this); + adapter.setList(imagePosts); + viewPager.setAdapter(adapter); + viewPager.setOnPageChangeListener(this); + + // Select the right image + for (int i = 0; i < imagePosts.size(); i++) { + if (imagePosts.get(i).no == selectedId) { + viewPager.setCurrentItem(i); + onPageSelected(i); + break; } - } else { - Logger.e(TAG, "Posts in ImageViewActivity was null"); - finish(); } } @@ -154,13 +177,22 @@ public class ImageViewActivity extends Activity implements ViewPager.OnPageChang invalidateOptionsMenu(); } - public void callOnSelect() { + public void updateActionBarIfSelected(ImageViewFragment targetFragment) { ImageViewFragment fragment = getFragment(currentPosition); - if (fragment != null) { + if (fragment != null && fragment == targetFragment) { fragment.onSelected(adapter, currentPosition); } } + public void setProgressBar(long current, long total, boolean done) { + if (done) { + progressBar.setVisibility(View.GONE); + } else { + progressBar.setVisibility(View.VISIBLE); + progressBar.setProgress((int) (((double) current / total) * progressBar.getMax())); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { @@ -206,7 +238,7 @@ public class ImageViewActivity extends Activity implements ViewPager.OnPageChang public boolean onPrepareOptionsMenu(Menu menu) { ImageViewFragment fragment = getFragment(currentPosition); if (fragment != null) { - fragment.onPrepareOptionsMenu(currentPosition, adapter, menu); + fragment.onPrepareOptionsMenu(menu); } return super.onPrepareOptionsMenu(menu); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java index cba6dc97..c087b4b0 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinnedAdapter.java @@ -25,7 +25,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.FrameLayout; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -120,7 +119,7 @@ public class PinnedAdapter extends BaseAdapter { imageView.setVisibility(View.VISIBLE); imageView.setFadeIn(100); if (imageView.getUrl() == null || !imageView.getUrl().equals(pin.opPost.thumbnailUrl)) { - imageView.setImageUrl(pin.opPost.thumbnailUrl, ChanApplication.getImageLoader()); + imageView.setImageUrl(pin.opPost.thumbnailUrl, ChanApplication.getVolleyImageLoader()); } } else { imageView.setVisibility(View.GONE); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java index ec29c22e..a76ad072 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ImageViewFragment.java @@ -41,15 +41,20 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal private Context context; private ImageViewActivity activity; + private ThumbnailImageView imageView; + private Post post; private boolean showProgressBar = true; - private ThumbnailImageView imageView; private boolean isVideo = false; private boolean videoVisible = false; private boolean videoSetIconToPause = false; private boolean tapToLoad = false; private boolean loaded = false; + private long progressCurrent; + private long progressTotal; + private boolean progressDone; + public static ImageViewFragment newInstance(Post post, ImageViewActivity activity, int index) { ImageViewFragment imageViewFragment = new ImageViewFragment(); imageViewFragment.post = post; @@ -68,9 +73,7 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal imageView = new ThumbnailImageView(context); imageView.setCallback(this); - imageView.setLayoutParams(Utils.MATCH_PARAMS); - - int padding = (int) context.getResources().getDimension(R.dimen.image_view_padding); + int padding = Utils.dp(8); imageView.setPadding(padding, padding, padding, padding); return imageView; @@ -180,6 +183,8 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal } } } + + activity.setProgressBar(progressCurrent, progressTotal, progressDone); } public void onDeselected() { @@ -188,7 +193,7 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal } } - public void onPrepareOptionsMenu(int position, ImageViewAdapter adapter, Menu menu) { + public void onPrepareOptionsMenu(Menu menu) { MenuItem item = menu.findItem(R.id.action_image_play_state); item.setVisible(isVideo); item.setEnabled(isVideo); @@ -237,7 +242,7 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal public void showProgressBar(boolean e) { showProgressBar = e; - activity.callOnSelect(); + activity.updateActionBarIfSelected(this); } @Override @@ -258,6 +263,14 @@ public class ImageViewFragment extends Fragment implements ThumbnailImageViewCal showProgressBar(progress); } + @Override + public void setLinearProgress(long current, long total, boolean done) { + progressCurrent = current; + progressTotal = total; + progressDone = done; + activity.updateActionBarIfSelected(this); + } + @Override public void onVideoLoaded() { videoSetIconToPause = true; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java index 31fbf64e..a8467a38 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java @@ -428,7 +428,7 @@ public class ReplyFragment extends DialogFragment { String imageUrl = ChanUrls.getCaptchaImageUrl(challenge); NetworkImageView captchaImage = new NetworkImageView(context); - captchaImage.setImageUrl(imageUrl, ChanApplication.getImageLoader()); + captchaImage.setImageUrl(imageUrl, ChanApplication.getVolleyImageLoader()); captchaContainer.setView(captchaImage); gettingCaptcha = false; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/GIFView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/GIFView.java index 739ac5a7..85f389ff 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/GIFView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/GIFView.java @@ -26,7 +26,14 @@ import android.os.SystemClock; import android.util.AttributeSet; import android.view.View; +import org.floens.chan.utils.Utils; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + public class GIFView extends View { + private static final ExecutorService executor = Executors.newFixedThreadPool(1); + private Movie movie; private long movieStart; @@ -51,20 +58,22 @@ public class GIFView extends View { setLayerType(LAYER_TYPE_SOFTWARE, paint); } - public boolean setData(byte[] array) { - Movie movie = Movie.decodeByteArray(array, 0, array.length); - - return onMovieLoaded(movie); - } - - private boolean onMovieLoaded(Movie movie) { - if (movie != null) { - this.movie = movie; - invalidate(); - return true; - } else { - return false; - } + public void setPath(final String path) { + executor.submit(new Runnable() { + @Override + public void run() { + final Movie movie = Movie.decodeFile(path); + if (movie != null) { + Utils.runOnUiThread(new Runnable() { + @Override + public void run() { + GIFView.this.movie = movie; + invalidate(); + } + }); + } + } + }); } @Override diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java index 74fde222..83eddf10 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java @@ -141,7 +141,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { if (post.hasImage) { imageView.setVisibility(View.VISIBLE); - imageView.setImageUrl(post.thumbnailUrl, ChanApplication.getImageLoader()); + imageView.setImageUrl(post.thumbnailUrl, ChanApplication.getVolleyImageLoader()); } else { imageView.setVisibility(View.GONE); imageView.setImageUrl(null, null); @@ -251,7 +251,7 @@ public class PostView extends LinearLayout implements View.OnClickListener { deletedView.setVisibility(showDeletedIcon ? View.VISIBLE : View.GONE); if (showCountryFlag) { countryView.setVisibility(View.VISIBLE); - countryView.setImageUrl(ChanUrls.getCountryFlagUrl(post.country), ChanApplication.getImageLoader()); + countryView.setImageUrl(ChanUrls.getCountryFlagUrl(post.country), ChanApplication.getVolleyImageLoader()); } else { countryView.setVisibility(View.GONE); countryView.setImageUrl(null, null); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java index 99fb4a2b..b095785f 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailImageView.java @@ -19,7 +19,6 @@ package org.floens.chan.ui.view; import android.content.Context; import android.media.MediaPlayer; -import android.media.MediaPlayer.OnPreparedListener; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; @@ -28,24 +27,23 @@ import android.widget.Toast; import android.widget.VideoView; import com.android.volley.Request; -import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageLoader.ImageContainer; -import com.android.volley.toolbox.ImageLoader.ImageListener; +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; +import com.koushikdutta.async.future.Future; +import com.koushikdutta.async.future.FutureCallback; +import com.koushikdutta.ion.Ion; +import com.koushikdutta.ion.ProgressCallback; import org.floens.chan.ChanApplication; import org.floens.chan.R; -import org.floens.chan.core.net.FileRequest; -import org.floens.chan.core.net.GIFRequest; import org.floens.chan.utils.Logger; import org.floens.chan.utils.Utils; import java.io.File; +import java.util.concurrent.CancellationException; -import uk.co.senab.photoview.PhotoViewAttacher; -import uk.co.senab.photoview.PhotoViewAttacher.OnViewTapListener; - -public class ThumbnailImageView extends LoadView implements OnViewTapListener, View.OnClickListener { +public class ThumbnailImageView extends LoadView implements View.OnClickListener { private static final String TAG = "ThumbnailImageView"; private ThumbnailImageViewCallback callback; @@ -57,8 +55,8 @@ public class ThumbnailImageView extends LoadView implements OnViewTapListener, V private boolean thumbnailNeeded = true; - private ImageContainer imageContainerRequest; private Request imageRequest; + private Future ionRequest; private VideoView videoView; public ThumbnailImageView(Context context) { @@ -90,7 +88,8 @@ public class ThumbnailImageView extends LoadView implements OnViewTapListener, V return; } - ChanApplication.getImageLoader().get(thumbnailUrl, new ImageListener() { + // Also use volley for the thumbnails + ChanApplication.getVolleyImageLoader().get(thumbnailUrl, new com.android.volley.toolbox.ImageLoader.ImageListener() { @Override public void onErrorResponse(VolleyError error) { onError(); @@ -116,34 +115,53 @@ public class ThumbnailImageView extends LoadView implements OnViewTapListener, V callback.setProgress(true); - // 4096 is the max GPU upload size - int maxWidth = Math.min((int) (getWidth() * maxScale), 4096); - int maxHeight = Math.min((int) (getHeight() * maxScale), 4096); - - imageContainerRequest = ChanApplication.getImageLoader().get(imageUrl, new ImageListener() { - @Override - public void onErrorResponse(VolleyError error) { - onError(); - } - - @Override - public void onResponse(ImageContainer response, boolean isImmediate) { - if (response.getBitmap() != null) { - CleanupImageView image = new CleanupImageView(getContext()); - image.setImageBitmap(response.getBitmap()); - - PhotoViewAttacher attacher = new PhotoViewAttacher(image); - attacher.setOnViewTapListener(ThumbnailImageView.this); - attacher.setMaximumScale(maxScale); + File file = ChanApplication.getFileCache().get(imageUrl); + if (file.exists()) { + onBigImage(file); + } else { + ionRequest = Ion.with(getContext()) + .load(imageUrl) + .progress(new ProgressCallback() { + @Override + public void onProgress(final long downloaded, final long total) { + Utils.runOnUiThread(new Runnable() { + @Override + public void run() { + callback.setLinearProgress(downloaded, total, false); + } + }); + } + }) + .write(file) + .setCallback(new FutureCallback() { + @Override + public void onCompleted(Exception e, File result) { + if (e != null || result == null || result.length() == 0) { + if (result != null) { + ChanApplication.getFileCache().delete(result); + } + if (e != null && !(e instanceof CancellationException)) { + onError(); + e.printStackTrace(); + } + } else { + ChanApplication.getFileCache().put(result); + onBigImage(result); + } + } + }); + } + } - image.setAttacher(attacher); + private void onBigImage(File file) { + SubsamplingScaleImageView image = new SubsamplingScaleImageView(getContext()); + image.setImageFile(file.getAbsolutePath()); + image.setOnClickListener(this); - setView(image, !isImmediate); - callback.setProgress(false); - thumbnailNeeded = false; - } - } - }, maxWidth, maxHeight); + setView(image, false); + callback.setProgress(false); + callback.setLinearProgress(0, 0, true); + thumbnailNeeded = false; } public void setGif(String gifUrl) { @@ -154,68 +172,118 @@ public class ThumbnailImageView extends LoadView implements OnViewTapListener, V callback.setProgress(true); - imageRequest = ChanApplication.getVolleyRequestQueue().add( - new GIFRequest(gifUrl, new Response.Listener() { - @Override - public void onResponse(GIFView view) { - view.setLayoutParams(Utils.MATCH_PARAMS); - - setView(view, false); - callback.setProgress(false); - thumbnailNeeded = false; - } - }, new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onError(); - } - }, getContext()) - ); + File file = ChanApplication.getFileCache().get(gifUrl); + if (file.exists()) { + onGif(file); + } else { + ionRequest = Ion.with(getContext()) + .load(gifUrl) + .progress(new ProgressCallback() { + @Override + public void onProgress(final long downloaded, final long total) { + Utils.runOnUiThread(new Runnable() { + @Override + public void run() { + callback.setLinearProgress(downloaded, total, false); + } + }); + } + }) + .write(file) + .setCallback(new FutureCallback() { + @Override + public void onCompleted(Exception e, File result) { + if (e != null || result == null || result.length() == 0) { + if (result != null) { + ChanApplication.getFileCache().delete(result); + } + if (e != null && !(e instanceof CancellationException)) { + onError(); + e.printStackTrace(); + } + } else { + ChanApplication.getFileCache().put(result); + onGif(result); + } + } + }); + } + } + + private void onGif(File file) { + GIFView view = new GIFView(getContext()); + view.setPath(file.getAbsolutePath()); + view.setLayoutParams(Utils.MATCH_PARAMS); + setView(view, false); + callback.setProgress(false); + callback.setLinearProgress(0, 0, true); } public void setVideo(String videoUrl) { callback.setProgress(true); - imageRequest = ChanApplication.getVolleyRequestQueue().add( - new FileRequest(videoUrl, new Response.Listener() { - @Override - public void onResponse(final File file) { - if (file != null) { - videoView = new VideoView(getContext()); - videoView.setZOrderOnTop(true); - videoView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - videoView.setLayoutParams(Utils.MATCH_PARAMS); - LayoutParams par = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - par.gravity = Gravity.CENTER; - videoView.setLayoutParams(par); - - videoView.setOnPreparedListener(new OnPreparedListener() { + File file = ChanApplication.getFileCache().get(videoUrl); + if (file.exists()) { + onVideo(file); + } else { + ionRequest = Ion.with(getContext()) + .load(videoUrl) + .progress(new ProgressCallback() { + @Override + public void onProgress(final long downloaded, final long total) { + Utils.runOnUiThread(new Runnable() { @Override - public void onPrepared(MediaPlayer mp) { - mp.setLooping(true); - callback.onVideoLoaded(); + public void run() { + callback.setLinearProgress(downloaded, total, false); } }); - videoView.setVideoPath(file.getAbsolutePath()); + } + }) + .write(file) + .setCallback(new FutureCallback() { + @Override + public void onCompleted(Exception e, File result) { + if (e != null || result == null || result.length() == 0) { + if (result != null) { + ChanApplication.getFileCache().delete(result); + } + if (e != null && !(e instanceof CancellationException)) { + onError(); + e.printStackTrace(); + } + } else { + ChanApplication.getFileCache().put(result); + onVideo(result); + } + } + }); + } + } + + private void onVideo(File file) { + videoView = new VideoView(getContext()); + videoView.setZOrderOnTop(true); + videoView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + videoView.setLayoutParams(Utils.MATCH_PARAMS); + LayoutParams par = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + par.gravity = Gravity.CENTER; + videoView.setLayoutParams(par); + + videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer mp) { + mp.setLooping(true); + callback.onVideoLoaded(); + } + }); + videoView.setVideoPath(file.getAbsolutePath()); - setView(videoView, false); - callback.setProgress(false); - thumbnailNeeded = false; + setView(videoView, false); + callback.setProgress(false); + thumbnailNeeded = false; - videoView.start(); - } else { - onError(); - } - } - }, new Response.ErrorListener() { - - @Override - public void onErrorResponse(VolleyError error) { - onError(); - } - }) - ); + videoView.start(); } @Override @@ -238,17 +306,11 @@ public class ThumbnailImageView extends LoadView implements OnViewTapListener, V imageRequest = null; } - if (imageContainerRequest != null) { - imageContainerRequest.cancelRequest(); - imageContainerRequest = null; + if (ionRequest != null) { + ionRequest.cancel(true); } } - @Override - public void onViewTap(View view, float x, float y) { - callback.onTap(); - } - @Override public void onClick(View v) { callback.onTap(); @@ -259,35 +321,8 @@ public class ThumbnailImageView extends LoadView implements OnViewTapListener, V public void setProgress(boolean progress); - public void onVideoLoaded(); - } - - private static class CleanupImageView extends ImageView { - private PhotoViewAttacher attacher; - - public CleanupImageView(Context context) { - super(context); - } - - public CleanupImageView(Context context, AttributeSet attrs) { - super(context, attrs); - } + public void setLinearProgress(long current, long total, boolean done); - public CleanupImageView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void setAttacher(PhotoViewAttacher attacher) { - this.attacher = attacher; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (attacher != null) { - attacher.cleanup(); - } - } + public void onVideoLoaded(); } } diff --git a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java new file mode 100644 index 00000000..e49a66c5 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java @@ -0,0 +1,86 @@ +package org.floens.chan.utils; + +import android.util.Log; + +import java.io.File; + +public class FileCache { + private static final String TAG = "FileCache"; + + private final File directory; + private final long maxSize; + + private long size; + + public FileCache(File directory, long maxSize) { + this.directory = directory; + this.maxSize = maxSize; + + if (!directory.exists() && !directory.mkdirs()) { + Logger.e(TAG, "Unable to create file cache dir " + directory.getAbsolutePath()); + } + + calculateSize(); + } + + public File get(String key) { + return new File(directory, Integer.toString(key.hashCode())); + } + + public void put(File file) { + size += file.length(); + + trim(); + } + + public boolean delete(File file) { + size -= file.length(); + + return file.delete(); + } + + private void trim() { + int tries = 0; + while (size > maxSize && tries++ < 10) { + File[] files = directory.listFiles(); + if (files == null) { + break; + } + long age = Long.MAX_VALUE; + long last; + File oldest = null; + for (File file : files) { + last = file.lastModified(); + if (last < age && last != 0L) { + age = last; + oldest = file; + } + } + + if (oldest == null) { + Log.e(TAG, "No files to trim"); + break; + } else { + Log.d(TAG, "Deleting " + oldest.getAbsolutePath()); + if (!delete(oldest)) { + Log.e(TAG, "Cannot delete cache file"); + calculateSize(); + break; + } + } + + calculateSize(); + } + } + + private void calculateSize() { + size = 0; + + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + size += file.length(); + } + } + } +} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/Compat.java b/Clover/app/src/main/java/uk/co/senab/photoview/Compat.java deleted file mode 100644 index 6faccdf8..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/Compat.java +++ /dev/null @@ -1,60 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview; - -import android.annotation.TargetApi; -import android.os.Build; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.view.MotionEvent; -import android.view.View; - -public class Compat { - - private static final int SIXTY_FPS_INTERVAL = 1000 / 60; - - public static void postOnAnimation(View view, Runnable runnable) { - if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { - postOnAnimationJellyBean(view, runnable); - } else { - view.postDelayed(runnable, SIXTY_FPS_INTERVAL); - } - } - - @TargetApi(16) - private static void postOnAnimationJellyBean(View view, Runnable runnable) { - view.postOnAnimation(runnable); - } - - public static int getPointerIndex(int action) { - if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) - return getPointerIndexHoneyComb(action); - else - return getPointerIndexEclair(action); - } - - @SuppressWarnings("deprecation") - @TargetApi(Build.VERSION_CODES.ECLAIR) - private static int getPointerIndexEclair(int action) { - return (action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT; - } - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - private static int getPointerIndexHoneyComb(int action) { - return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; - } - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/DefaultOnDoubleTapListener.java b/Clover/app/src/main/java/uk/co/senab/photoview/DefaultOnDoubleTapListener.java deleted file mode 100644 index d0ba1ef0..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/DefaultOnDoubleTapListener.java +++ /dev/null @@ -1,98 +0,0 @@ -package uk.co.senab.photoview; - -import android.graphics.RectF; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.widget.ImageView; - -/** - * Provided default implementation of GestureDetector.OnDoubleTapListener, to be overriden with custom behavior, if needed - *

 

- * To be used via {@link uk.co.senab.photoview.PhotoViewAttacher#setOnDoubleTapListener(android.view.GestureDetector.OnDoubleTapListener)} - */ -public class DefaultOnDoubleTapListener implements GestureDetector.OnDoubleTapListener { - - private PhotoViewAttacher photoViewAttacher; - - /** - * Default constructor - * - * @param photoViewAttacher PhotoViewAttacher to bind to - */ - public DefaultOnDoubleTapListener(PhotoViewAttacher photoViewAttacher) { - setPhotoViewAttacher(photoViewAttacher); - } - - /** - * Allows to change PhotoViewAttacher within range of single instance - * - * @param newPhotoViewAttacher PhotoViewAttacher to bind to - */ - public void setPhotoViewAttacher(PhotoViewAttacher newPhotoViewAttacher) { - this.photoViewAttacher = newPhotoViewAttacher; - } - - @Override - public boolean onSingleTapConfirmed(MotionEvent e) { - if (this.photoViewAttacher == null) - return false; - - ImageView imageView = photoViewAttacher.getImageView(); - - if (null != photoViewAttacher.getOnPhotoTapListener()) { - final RectF displayRect = photoViewAttacher.getDisplayRect(); - - if (null != displayRect) { - final float x = e.getX(), y = e.getY(); - - // Check to see if the user tapped on the photo - if (displayRect.contains(x, y)) { - - float xResult = (x - displayRect.left) - / displayRect.width(); - float yResult = (y - displayRect.top) - / displayRect.height(); - - photoViewAttacher.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult); - return true; - } - } - } - if (null != photoViewAttacher.getOnViewTapListener()) { - photoViewAttacher.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY()); - } - - return false; - } - - @Override - public boolean onDoubleTap(MotionEvent ev) { - if (photoViewAttacher == null) - return false; - - try { - float scale = photoViewAttacher.getScale(); - float x = ev.getX(); - float y = ev.getY(); - - if (scale < photoViewAttacher.getMediumScale()) { - photoViewAttacher.setScale(photoViewAttacher.getMediumScale(), x, y, true); - } else if (scale >= photoViewAttacher.getMediumScale() && scale < photoViewAttacher.getMaximumScale()) { - photoViewAttacher.setScale(photoViewAttacher.getMaximumScale(), x, y, true); - } else { - photoViewAttacher.setScale(photoViewAttacher.getMinimumScale(), x, y, true); - } - } catch (ArrayIndexOutOfBoundsException e) { - // Can sometimes happen when getX() and getY() is called - } - - return true; - } - - @Override - public boolean onDoubleTapEvent(MotionEvent e) { - // Wait for the confirmed onDoubleTap() instead - return false; - } - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/IPhotoView.java b/Clover/app/src/main/java/uk/co/senab/photoview/IPhotoView.java deleted file mode 100644 index 739b6f5e..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/IPhotoView.java +++ /dev/null @@ -1,260 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview; - -import android.graphics.Bitmap; -import android.graphics.Matrix; -import android.graphics.RectF; -import android.view.View; -import android.widget.ImageView; - - -public interface IPhotoView { - /** - * Returns true if the PhotoView is set to allow zooming of Photos. - * - * @return true if the PhotoView allows zooming. - */ - boolean canZoom(); - - /** - * Gets the Display Rectangle of the currently displayed Drawable. The - * Rectangle is relative to this View and includes all scaling and - * translations. - * - * @return - RectF of Displayed Drawable - */ - RectF getDisplayRect(); - - /** - * Sets the Display Matrix of the currently displayed Drawable. The - * Rectangle is considered relative to this View and includes all scaling and - * translations. - * - * @return - true if rectangle was applied successfully - */ - boolean setDisplayMatrix(Matrix finalMatrix); - - /** - * Gets the Display Matrix of the currently displayed Drawable. The - * Rectangle is considered relative to this View and includes all scaling and - * translations. - * - * @return - true if rectangle was applied successfully - */ - Matrix getDisplayMatrix(); - - /** - * Use {@link #getMinimumScale()} instead, this will be removed in future release - * - * @return The current minimum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - @Deprecated - float getMinScale(); - - /** - * @return The current minimum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - float getMinimumScale(); - - /** - * Use {@link #getMediumScale()} instead, this will be removed in future release - * - * @return The current middle scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - @Deprecated - float getMidScale(); - - /** - * @return The current medium scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - float getMediumScale(); - - /** - * Use {@link #getMaximumScale()} instead, this will be removed in future release - * - * @return The current maximum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - @Deprecated - float getMaxScale(); - - /** - * @return The current maximum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - float getMaximumScale(); - - /** - * Returns the current scale value - * - * @return float - current scale value - */ - float getScale(); - - /** - * Return the current scale type in use by the ImageView. - */ - ImageView.ScaleType getScaleType(); - - /** - * Whether to allow the ImageView's parent to intercept the touch event when the photo is scroll to it's horizontal edge. - */ - void setAllowParentInterceptOnEdge(boolean allow); - - /** - * Use {@link #setMinimumScale(float minimumScale)} instead, this will be removed in future release - *

- * Sets the minimum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - @Deprecated - void setMinScale(float minScale); - - /** - * Sets the minimum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - void setMinimumScale(float minimumScale); - - /** - * Use {@link #setMediumScale(float mediumScale)} instead, this will be removed in future release - *

- * Sets the middle scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - @Deprecated - void setMidScale(float midScale); - - /* - * Sets the medium scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - void setMediumScale(float mediumScale); - - /** - * Use {@link #setMaximumScale(float maximumScale)} instead, this will be removed in future release - *

- * Sets the maximum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - @Deprecated - void setMaxScale(float maxScale); - - /** - * Sets the maximum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. - */ - void setMaximumScale(float maximumScale); - - /** - * Register a callback to be invoked when the Photo displayed by this view is long-pressed. - * - * @param listener - Listener to be registered. - */ - void setOnLongClickListener(View.OnLongClickListener listener); - - /** - * Register a callback to be invoked when the Matrix has changed for this - * View. An example would be the user panning or scaling the Photo. - * - * @param listener - Listener to be registered. - */ - void setOnMatrixChangeListener(PhotoViewAttacher.OnMatrixChangedListener listener); - - /** - * Register a callback to be invoked when the Photo displayed by this View - * is tapped with a single tap. - * - * @param listener - Listener to be registered. - */ - void setOnPhotoTapListener(PhotoViewAttacher.OnPhotoTapListener listener); - - /** - * Returns a listener to be invoked when the Photo displayed by this View - * is tapped with a single tap. - * - * @return PhotoViewAttacher.OnPhotoTapListener currently set, may be null - */ - PhotoViewAttacher.OnPhotoTapListener getOnPhotoTapListener(); - - /** - * Register a callback to be invoked when the View is tapped with a single - * tap. - * - * @param listener - Listener to be registered. - */ - void setOnViewTapListener(PhotoViewAttacher.OnViewTapListener listener); - - /** - * Returns a callback listener to be invoked when the View is tapped with a single - * tap. - * - * @return PhotoViewAttacher.OnViewTapListener currently set, may be null - */ - PhotoViewAttacher.OnViewTapListener getOnViewTapListener(); - - /** - * Changes the current scale to the specified value. - * - * @param scale - Value to scale to - */ - void setScale(float scale); - - /** - * Changes the current scale to the specified value. - * - * @param scale - Value to scale to - * @param animate - Whether to animate the scale - */ - void setScale(float scale, boolean animate); - - /** - * Changes the current scale to the specified value, around the given focal point. - * - * @param scale - Value to scale to - * @param focalX - X Focus Point - * @param focalY - Y Focus Point - * @param animate - Whether to animate the scale - */ - void setScale(float scale, float focalX, float focalY, boolean animate); - - /** - * Controls how the image should be resized or moved to match the size of - * the ImageView. Any scaling or panning will happen within the confines of - * this {@link android.widget.ImageView.ScaleType}. - * - * @param scaleType - The desired scaling mode. - */ - void setScaleType(ImageView.ScaleType scaleType); - - /** - * Allows you to enable/disable the zoom functionality on the ImageView. - * When disable the ImageView reverts to using the FIT_CENTER matrix. - * - * @param zoomable - Whether the zoom functionality is enabled. - */ - void setZoomable(boolean zoomable); - - /** - * Enables rotation via PhotoView internal functions. - * Name is chosen so it won't collide with View.setRotation(float) in API since 11 - * - * @param rotationDegree - Degree to rotate PhotoView by, should be in range 0 to 360 - */ - void setPhotoViewRotation(float rotationDegree); - - /** - * Extracts currently visible area to Bitmap object, if there is no image loaded yet or the - * ImageView is already destroyed, returns {@code null} - * - * @return currently visible area as bitmap or null - */ - public Bitmap getVisibleRectangleBitmap(); - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/PhotoView.java b/Clover/app/src/main/java/uk/co/senab/photoview/PhotoView.java deleted file mode 100644 index a331b255..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/PhotoView.java +++ /dev/null @@ -1,256 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview; - -import uk.co.senab.photoview.PhotoViewAttacher.OnMatrixChangedListener; -import uk.co.senab.photoview.PhotoViewAttacher.OnPhotoTapListener; -import uk.co.senab.photoview.PhotoViewAttacher.OnViewTapListener; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Matrix; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.util.AttributeSet; -import android.widget.ImageView; - -public class PhotoView extends ImageView implements IPhotoView { - - private final PhotoViewAttacher mAttacher; - - private ScaleType mPendingScaleType; - - public PhotoView(Context context) { - this(context, null); - } - - public PhotoView(Context context, AttributeSet attr) { - this(context, attr, 0); - } - - public PhotoView(Context context, AttributeSet attr, int defStyle) { - super(context, attr, defStyle); - super.setScaleType(ScaleType.MATRIX); - mAttacher = new PhotoViewAttacher(this); - - if (null != mPendingScaleType) { - setScaleType(mPendingScaleType); - mPendingScaleType = null; - } - } - - @Override - public void setPhotoViewRotation(float rotationDegree) { - mAttacher.setPhotoViewRotation(rotationDegree); - } - - @Override - public boolean canZoom() { - return mAttacher.canZoom(); - } - - @Override - public RectF getDisplayRect() { - return mAttacher.getDisplayRect(); - } - - @Override - public Matrix getDisplayMatrix() { - return mAttacher.getDrawMatrix(); - } - - @Override - public boolean setDisplayMatrix(Matrix finalRectangle) { - return mAttacher.setDisplayMatrix(finalRectangle); - } - - @Override - @Deprecated - public float getMinScale() { - return getMinimumScale(); - } - - @Override - public float getMinimumScale() { - return mAttacher.getMinimumScale(); - } - - @Override - @Deprecated - public float getMidScale() { - return getMediumScale(); - } - - @Override - public float getMediumScale() { - return mAttacher.getMediumScale(); - } - - @Override - @Deprecated - public float getMaxScale() { - return getMaximumScale(); - } - - @Override - public float getMaximumScale() { - return mAttacher.getMaximumScale(); - } - - @Override - public float getScale() { - return mAttacher.getScale(); - } - - @Override - public ScaleType getScaleType() { - return mAttacher.getScaleType(); - } - - @Override - public void setAllowParentInterceptOnEdge(boolean allow) { - mAttacher.setAllowParentInterceptOnEdge(allow); - } - - @Override - @Deprecated - public void setMinScale(float minScale) { - setMinimumScale(minScale); - } - - @Override - public void setMinimumScale(float minimumScale) { - mAttacher.setMinimumScale(minimumScale); - } - - @Override - @Deprecated - public void setMidScale(float midScale) { - setMediumScale(midScale); - } - - @Override - public void setMediumScale(float mediumScale) { - mAttacher.setMediumScale(mediumScale); - } - - @Override - @Deprecated - public void setMaxScale(float maxScale) { - setMaximumScale(maxScale); - } - - @Override - public void setMaximumScale(float maximumScale) { - mAttacher.setMaximumScale(maximumScale); - } - - @Override - // setImageBitmap calls through to this method - public void setImageDrawable(Drawable drawable) { - super.setImageDrawable(drawable); - if (null != mAttacher) { - mAttacher.update(); - } - } - - @Override - public void setImageResource(int resId) { - super.setImageResource(resId); - if (null != mAttacher) { - mAttacher.update(); - } - } - - @Override - public void setImageURI(Uri uri) { - super.setImageURI(uri); - if (null != mAttacher) { - mAttacher.update(); - } - } - - @Override - public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { - mAttacher.setOnMatrixChangeListener(listener); - } - - @Override - public void setOnLongClickListener(OnLongClickListener l) { - mAttacher.setOnLongClickListener(l); - } - - @Override - public void setOnPhotoTapListener(OnPhotoTapListener listener) { - mAttacher.setOnPhotoTapListener(listener); - } - - @Override - public OnPhotoTapListener getOnPhotoTapListener() { - return mAttacher.getOnPhotoTapListener(); - } - - @Override - public void setOnViewTapListener(OnViewTapListener listener) { - mAttacher.setOnViewTapListener(listener); - } - - @Override - public OnViewTapListener getOnViewTapListener() { - return mAttacher.getOnViewTapListener(); - } - - @Override - public void setScale(float scale) { - mAttacher.setScale(scale); - } - - @Override - public void setScale(float scale, boolean animate) { - mAttacher.setScale(scale, animate); - } - - @Override - public void setScale(float scale, float focalX, float focalY, boolean animate) { - mAttacher.setScale(scale, focalX, focalY, animate); - } - - @Override - public void setScaleType(ScaleType scaleType) { - if (null != mAttacher) { - mAttacher.setScaleType(scaleType); - } else { - mPendingScaleType = scaleType; - } - } - - @Override - public void setZoomable(boolean zoomable) { - mAttacher.setZoomable(zoomable); - } - - @Override - public Bitmap getVisibleRectangleBitmap() { - return mAttacher.getVisibleRectangleBitmap(); - } - - @Override - protected void onDetachedFromWindow() { - mAttacher.cleanup(); - super.onDetachedFromWindow(); - } - -} \ No newline at end of file diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/PhotoViewAttacher.java b/Clover/app/src/main/java/uk/co/senab/photoview/PhotoViewAttacher.java deleted file mode 100644 index 3875b2a8..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/PhotoViewAttacher.java +++ /dev/null @@ -1,1085 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview; - -import static android.view.MotionEvent.ACTION_CANCEL; -import static android.view.MotionEvent.ACTION_DOWN; -import static android.view.MotionEvent.ACTION_UP; - -import java.lang.ref.WeakReference; - -import uk.co.senab.photoview.gestures.OnGestureListener; -import uk.co.senab.photoview.gestures.VersionedGestureDetector; -import uk.co.senab.photoview.log.LogManager; -import uk.co.senab.photoview.scrollerproxy.ScrollerProxy; -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Matrix; -import android.graphics.Matrix.ScaleToFit; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.util.FloatMath; -import android.util.Log; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnLongClickListener; -import android.view.ViewParent; -import android.view.ViewTreeObserver; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.Interpolator; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; - -@SuppressLint("FloatMath") -public class PhotoViewAttacher implements IPhotoView, View.OnTouchListener, - OnGestureListener, - ViewTreeObserver.OnGlobalLayoutListener { - - private static final String LOG_TAG = "PhotoViewAttacher"; - - // let debug flag be dynamic, but still Proguard can be used to remove from - // release builds - private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); - - static final Interpolator sInterpolator = new AccelerateDecelerateInterpolator(); - static final int ZOOM_DURATION = 200; - - static final int EDGE_NONE = -1; - static final int EDGE_LEFT = 0; - static final int EDGE_RIGHT = 1; - static final int EDGE_BOTH = 2; - - public static final float DEFAULT_MAX_SCALE = 3.0f; - public static final float DEFAULT_MID_SCALE = 1.75f; - public static final float DEFAULT_MIN_SCALE = 1.0f; - - private float mMinScale = DEFAULT_MIN_SCALE; - private float mMidScale = DEFAULT_MID_SCALE; - private float mMaxScale = DEFAULT_MAX_SCALE; - - private boolean mAllowParentInterceptOnEdge = true; - - private static void checkZoomLevels(float minZoom, float midZoom, - float maxZoom) { - if (minZoom >= midZoom) { - throw new IllegalArgumentException( - "MinZoom has to be less than MidZoom"); - } else if (midZoom >= maxZoom) { - throw new IllegalArgumentException( - "MidZoom has to be less than MaxZoom"); - } - } - - /** - * @return true if the ImageView exists, and it's Drawable existss - */ - private static boolean hasDrawable(ImageView imageView) { - return null != imageView && null != imageView.getDrawable(); - } - - /** - * @return true if the ScaleType is supported. - */ - private static boolean isSupportedScaleType(final ScaleType scaleType) { - if (null == scaleType) { - return false; - } - - switch (scaleType) { - case MATRIX: - throw new IllegalArgumentException(scaleType.name() - + " is not supported in PhotoView"); - - default: - return true; - } - } - - /** - * Set's the ImageView's ScaleType to Matrix. - */ - private static void setImageViewScaleTypeMatrix(ImageView imageView) { - /** - * PhotoView sets it's own ScaleType to Matrix, then diverts all calls - * setScaleType to this.setScaleType automatically. - */ - if (null != imageView && !(imageView instanceof PhotoView)) { - if (!ScaleType.MATRIX.equals(imageView.getScaleType())) { - imageView.setScaleType(ScaleType.MATRIX); - } - } - } - - private WeakReference mImageView; - - // Gesture Detectors - private GestureDetector mGestureDetector; - private uk.co.senab.photoview.gestures.GestureDetector mScaleDragDetector; - - // These are set so we don't keep allocating them on the heap - private final Matrix mBaseMatrix = new Matrix(); - private final Matrix mDrawMatrix = new Matrix(); - private final Matrix mSuppMatrix = new Matrix(); - private final RectF mDisplayRect = new RectF(); - private final float[] mMatrixValues = new float[9]; - - // Listeners - private OnMatrixChangedListener mMatrixChangeListener; - private OnPhotoTapListener mPhotoTapListener; - private OnViewTapListener mViewTapListener; - private OnLongClickListener mLongClickListener; - - private int mIvTop, mIvRight, mIvBottom, mIvLeft; - private FlingRunnable mCurrentFlingRunnable; - private int mScrollEdge = EDGE_BOTH; - - @SuppressWarnings("unused") - private final boolean mRotationDetectionEnabled = false; - private boolean mZoomEnabled; - private ScaleType mScaleType = ScaleType.FIT_CENTER; - - public PhotoViewAttacher(ImageView imageView) { - mImageView = new WeakReference(imageView); - - imageView.setDrawingCacheEnabled(true); - imageView.setOnTouchListener(this); - - ViewTreeObserver observer = imageView.getViewTreeObserver(); - if (null != observer) - observer.addOnGlobalLayoutListener(this); - - // Make sure we using MATRIX Scale Type - setImageViewScaleTypeMatrix(imageView); - - if (imageView.isInEditMode()) { - return; - } - // Create Gesture Detectors... - mScaleDragDetector = VersionedGestureDetector.newInstance( - imageView.getContext(), this); - - mGestureDetector = new GestureDetector(imageView.getContext(), - new GestureDetector.SimpleOnGestureListener() { - - // forward long click listener - @Override - public void onLongPress(MotionEvent e) { - if (null != mLongClickListener) { - mLongClickListener.onLongClick(getImageView()); - } - } - }); - - mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); - - // Finally, update the UI so that we're zoomable - setZoomable(true); - } - - /** - * Sets custom double tap listener, to intercept default given functions. - * To reset behavior to default, you can just pass in "null" or public field of PhotoViewAttacher.defaultOnDoubleTapListener - * - * @param newOnDoubleTapListener custom OnDoubleTapListener to be set on ImageView - */ - public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { - if (newOnDoubleTapListener != null) - this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); - else - this.mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); - } - - @Override - public final boolean canZoom() { - return mZoomEnabled; - } - - /** - * Clean-up the resources attached to this object. This needs to be called - * when the ImageView is no longer used. A good example is from - * {@link android.view.View#onDetachedFromWindow()} or from - * {@link android.app.Activity#onDestroy()}. This is automatically called if - * you are using {@link uk.co.senab.photoview.PhotoView}. - */ - @SuppressWarnings("deprecation") - public final void cleanup() { - if (null == mImageView) { - return; // cleanup already done - } - - final ImageView imageView = mImageView.get(); - - if (null != imageView) { - // Remove this as a global layout listener - ViewTreeObserver observer = imageView.getViewTreeObserver(); - if (null != observer && observer.isAlive()) { - observer.removeGlobalOnLayoutListener(this); - } - - // Remove the ImageView's reference to this - imageView.setOnTouchListener(null); - - // make sure a pending fling runnable won't be run - cancelFling(); - } - - if (null != mGestureDetector) { - mGestureDetector.setOnDoubleTapListener(null); - } - - // Clear listeners too - mMatrixChangeListener = null; - mPhotoTapListener = null; - mViewTapListener = null; - - // Finally, clear ImageView - mImageView = null; - } - - @Override - public final RectF getDisplayRect() { - checkMatrixBounds(); - return getDisplayRect(getDrawMatrix()); - } - - @Override - public boolean setDisplayMatrix(Matrix finalMatrix) { - if (finalMatrix == null) - throw new IllegalArgumentException("Matrix cannot be null"); - - ImageView imageView = getImageView(); - if (null == imageView) - return false; - - if (null == imageView.getDrawable()) - return false; - - mSuppMatrix.set(finalMatrix); - setImageViewMatrix(getDrawMatrix()); - checkMatrixBounds(); - - return true; - } - - private float mLastRotation = 0; - - @Override - public void setPhotoViewRotation(float degrees) { - degrees %= 360; - mSuppMatrix.postRotate(mLastRotation - degrees); - mLastRotation = degrees; - checkAndDisplayMatrix(); - } - - public final ImageView getImageView() { - ImageView imageView = null; - - if (null != mImageView) { - imageView = mImageView.get(); - } - - // If we don't have an ImageView, call cleanup() - if (null == imageView) { - cleanup(); - Log.i(LOG_TAG, - "ImageView no longer exists. You should not use this PhotoViewAttacher any more."); - } - - return imageView; - } - - @Override - @Deprecated - public float getMinScale() { - return getMinimumScale(); - } - - @Override - public float getMinimumScale() { - return mMinScale; - } - - @Override - @Deprecated - public float getMidScale() { - return getMediumScale(); - } - - @Override - public float getMediumScale() { - return mMidScale; - } - - @Override - @Deprecated - public float getMaxScale() { - return getMaximumScale(); - } - - @Override - public float getMaximumScale() { - return mMaxScale; - } - - @Override - public final float getScale() { - return FloatMath.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); - } - - @Override - public final ScaleType getScaleType() { - return mScaleType; - } - - @Override - public final void onDrag(float dx, float dy) { - if (mScaleDragDetector.isScaling()) { - return; // Do not drag if we are already scaling - } - - if (DEBUG) { - LogManager.getLogger().d(LOG_TAG, - String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy)); - } - - ImageView imageView = getImageView(); - mSuppMatrix.postTranslate(dx, dy); - checkAndDisplayMatrix(); - - /** - * Here we decide whether to let the ImageView's parent to start taking - * over the touch event. - * - * First we check whether this function is enabled. We never want the - * parent to take over if we're scaling. We then check the edge we're - * on, and the direction of the scroll (i.e. if we're pulling against - * the edge, aka 'overscrolling', let the parent take over). - */ - if (mAllowParentInterceptOnEdge) { - if (mScrollEdge == EDGE_BOTH - || (mScrollEdge == EDGE_LEFT && dx >= 1f) - || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { - ViewParent parent = imageView.getParent(); - if (null != parent) - parent.requestDisallowInterceptTouchEvent(false); - } - } - } - - @Override - public final void onFling(float startX, float startY, float velocityX, - float velocityY) { - if (DEBUG) { - LogManager.getLogger().d( - LOG_TAG, - "onFling. sX: " + startX + " sY: " + startY + " Vx: " - + velocityX + " Vy: " + velocityY); - } - ImageView imageView = getImageView(); - mCurrentFlingRunnable = new FlingRunnable(imageView.getContext()); - mCurrentFlingRunnable.fling(getImageViewWidth(imageView), - getImageViewHeight(imageView), (int) velocityX, (int) velocityY); - imageView.post(mCurrentFlingRunnable); - } - - @Override - public final void onGlobalLayout() { - ImageView imageView = getImageView(); - - if (null != imageView && mZoomEnabled) { - final int top = imageView.getTop(); - final int right = imageView.getRight(); - final int bottom = imageView.getBottom(); - final int left = imageView.getLeft(); - - /** - * We need to check whether the ImageView's bounds have changed. - * This would be easier if we targeted API 11+ as we could just use - * View.OnLayoutChangeListener. Instead we have to replicate the - * work, keeping track of the ImageView's bounds and then checking - * if the values change. - */ - if (top != mIvTop || bottom != mIvBottom || left != mIvLeft - || right != mIvRight) { - // Update our base matrix, as the bounds have changed - updateBaseMatrix(imageView.getDrawable()); - - // Update values as something has changed - mIvTop = top; - mIvRight = right; - mIvBottom = bottom; - mIvLeft = left; - } - } - } - - @Override - public final void onScale(float scaleFactor, float focusX, float focusY) { - if (DEBUG) { - LogManager.getLogger().d( - LOG_TAG, - String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f", - scaleFactor, focusX, focusY)); - } - - if (getScale() < mMaxScale || scaleFactor < 1f) { - mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); - checkAndDisplayMatrix(); - } - } - - @Override - public final boolean onTouch(View v, MotionEvent ev) { - boolean handled = false; - - if (mZoomEnabled && hasDrawable((ImageView) v)) { - ViewParent parent = v.getParent(); - switch (ev.getAction()) { - case ACTION_DOWN: - // First, disable the Parent from intercepting the touch - // event - if (null != parent) - parent.requestDisallowInterceptTouchEvent(true); - else - Log.i(LOG_TAG, "onTouch getParent() returned null"); - - // If we're flinging, and the user presses down, cancel - // fling - cancelFling(); - break; - - case ACTION_CANCEL: - case ACTION_UP: - // If the user has zoomed less than min scale, zoom back - // to min scale - if (getScale() < mMinScale) { - RectF rect = getDisplayRect(); - if (null != rect) { - v.post(new AnimatedZoomRunnable(getScale(), mMinScale, - rect.centerX(), rect.centerY())); - handled = true; - } - } - break; - } - - // Finally, try the Scale/Drag detector - if (null != mScaleDragDetector - && mScaleDragDetector.onTouchEvent(ev)) { - handled = true; - } - - if (!handled && null != parent && !mAllowParentInterceptOnEdge) { - parent.requestDisallowInterceptTouchEvent(false); - } - - // Check to see if the user double tapped - if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) { - handled = true; - } - } - - return handled; - } - - @Override - public void setAllowParentInterceptOnEdge(boolean allow) { - mAllowParentInterceptOnEdge = allow; - } - - @Override - @Deprecated - public void setMinScale(float minScale) { - setMinimumScale(minScale); - } - - @Override - public void setMinimumScale(float minimumScale) { - checkZoomLevels(minimumScale, mMidScale, mMaxScale); - mMinScale = minimumScale; - } - - @Override - @Deprecated - public void setMidScale(float midScale) { - setMediumScale(midScale); - } - - @Override - public void setMediumScale(float mediumScale) { - checkZoomLevels(mMinScale, mediumScale, mMaxScale); - mMidScale = mediumScale; - } - - @Override - @Deprecated - public void setMaxScale(float maxScale) { - setMaximumScale(maxScale); - } - - @Override - public void setMaximumScale(float maximumScale) { - checkZoomLevels(mMinScale, mMidScale, maximumScale); - mMaxScale = maximumScale; - } - - @Override - public final void setOnLongClickListener(OnLongClickListener listener) { - mLongClickListener = listener; - } - - @Override - public final void setOnMatrixChangeListener(OnMatrixChangedListener listener) { - mMatrixChangeListener = listener; - } - - @Override - public final void setOnPhotoTapListener(OnPhotoTapListener listener) { - mPhotoTapListener = listener; - } - - @Override - public final OnPhotoTapListener getOnPhotoTapListener() { - return mPhotoTapListener; - } - - @Override - public final void setOnViewTapListener(OnViewTapListener listener) { - mViewTapListener = listener; - } - - @Override - public final OnViewTapListener getOnViewTapListener() { - return mViewTapListener; - } - - @Override - public void setScale(float scale) { - setScale(scale, false); - } - - @Override - public void setScale(float scale, boolean animate) { - ImageView imageView = getImageView(); - - if (null != imageView) { - setScale(scale, - (imageView.getRight()) / 2, - (imageView.getBottom()) / 2, - animate); - } - } - - @Override - public void setScale(float scale, float focalX, float focalY, - boolean animate) { - ImageView imageView = getImageView(); - - if (null != imageView) { - // Check to see if the scale is within bounds - if (scale < mMinScale || scale > mMaxScale) { - LogManager - .getLogger() - .i(LOG_TAG, - "Scale must be within the range of minScale and maxScale"); - return; - } - - if (animate) { - imageView.post(new AnimatedZoomRunnable(getScale(), scale, - focalX, focalY)); - } else { - mSuppMatrix.setScale(scale, scale, focalX, focalY); - checkAndDisplayMatrix(); - } - } - } - - @Override - public final void setScaleType(ScaleType scaleType) { - if (isSupportedScaleType(scaleType) && scaleType != mScaleType) { - mScaleType = scaleType; - - // Finally update - update(); - } - } - - @Override - public final void setZoomable(boolean zoomable) { - mZoomEnabled = zoomable; - update(); - } - - public final void update() { - ImageView imageView = getImageView(); - - if (null != imageView) { - if (mZoomEnabled) { - // Make sure we using MATRIX Scale Type - setImageViewScaleTypeMatrix(imageView); - - // Update the base matrix using the current drawable - updateBaseMatrix(imageView.getDrawable()); - } else { - // Reset the Matrix... - resetMatrix(); - } - } - } - - @Override - public Matrix getDisplayMatrix() { - return new Matrix(mSuppMatrix); - } - - protected Matrix getDrawMatrix() { - mDrawMatrix.set(mBaseMatrix); - mDrawMatrix.postConcat(mSuppMatrix); - return mDrawMatrix; - } - - private void cancelFling() { - if (null != mCurrentFlingRunnable) { - mCurrentFlingRunnable.cancelFling(); - mCurrentFlingRunnable = null; - } - } - - /** - * Helper method that simply checks the Matrix, and then displays the result - */ - private void checkAndDisplayMatrix() { - if (checkMatrixBounds()) { - setImageViewMatrix(getDrawMatrix()); - } - } - - private void checkImageViewScaleType() { - ImageView imageView = getImageView(); - - /** - * PhotoView's getScaleType() will just divert to this.getScaleType() so - * only call if we're not attached to a PhotoView. - */ - if (null != imageView && !(imageView instanceof PhotoView)) { - if (!ScaleType.MATRIX.equals(imageView.getScaleType())) { - throw new IllegalStateException( - "The ImageView's ScaleType has been changed since attaching a PhotoViewAttacher"); - } - } - } - - private boolean checkMatrixBounds() { - final ImageView imageView = getImageView(); - if (null == imageView) { - return false; - } - - final RectF rect = getDisplayRect(getDrawMatrix()); - if (null == rect) { - return false; - } - - final float height = rect.height(), width = rect.width(); - float deltaX = 0, deltaY = 0; - - final int viewHeight = getImageViewHeight(imageView); - if (height <= viewHeight) { - switch (mScaleType) { - case FIT_START: - deltaY = -rect.top; - break; - case FIT_END: - deltaY = viewHeight - height - rect.top; - break; - default: - deltaY = (viewHeight - height) / 2 - rect.top; - break; - } - } else if (rect.top > 0) { - deltaY = -rect.top; - } else if (rect.bottom < viewHeight) { - deltaY = viewHeight - rect.bottom; - } - - final int viewWidth = getImageViewWidth(imageView); - if (width <= viewWidth) { - switch (mScaleType) { - case FIT_START: - deltaX = -rect.left; - break; - case FIT_END: - deltaX = viewWidth - width - rect.left; - break; - default: - deltaX = (viewWidth - width) / 2 - rect.left; - break; - } - mScrollEdge = EDGE_BOTH; - } else if (rect.left > 0) { - mScrollEdge = EDGE_LEFT; - deltaX = -rect.left; - } else if (rect.right < viewWidth) { - deltaX = viewWidth - rect.right; - mScrollEdge = EDGE_RIGHT; - } else { - mScrollEdge = EDGE_NONE; - } - - // Finally actually translate the matrix - mSuppMatrix.postTranslate(deltaX, deltaY); - return true; - } - - /** - * Helper method that maps the supplied Matrix to the current Drawable - * - * @param matrix - Matrix to map Drawable against - * @return RectF - Displayed Rectangle - */ - private RectF getDisplayRect(Matrix matrix) { - ImageView imageView = getImageView(); - - if (null != imageView) { - Drawable d = imageView.getDrawable(); - if (null != d) { - mDisplayRect.set(0, 0, d.getIntrinsicWidth(), - d.getIntrinsicHeight()); - matrix.mapRect(mDisplayRect); - return mDisplayRect; - } - } - return null; - } - - @Override - public Bitmap getVisibleRectangleBitmap() { - ImageView imageView = getImageView(); - return imageView == null ? null : imageView.getDrawingCache(); - } - - /** - * Helper method that 'unpacks' a Matrix and returns the required value - * - * @param matrix - Matrix to unpack - * @param whichValue - Which value from Matrix.M* to return - * @return float - returned value - */ - private float getValue(Matrix matrix, int whichValue) { - matrix.getValues(mMatrixValues); - return mMatrixValues[whichValue]; - } - - /** - * Resets the Matrix back to FIT_CENTER, and then displays it.s - */ - private void resetMatrix() { - mSuppMatrix.reset(); - setImageViewMatrix(getDrawMatrix()); - checkMatrixBounds(); - } - - private void setImageViewMatrix(Matrix matrix) { - ImageView imageView = getImageView(); - if (null != imageView) { - - checkImageViewScaleType(); - imageView.setImageMatrix(matrix); - - // Call MatrixChangedListener if needed - if (null != mMatrixChangeListener) { - RectF displayRect = getDisplayRect(matrix); - if (null != displayRect) { - mMatrixChangeListener.onMatrixChanged(displayRect); - } - } - } - } - - /** - * Calculate Matrix for FIT_CENTER - * - * @param d - Drawable being displayed - */ - private void updateBaseMatrix(Drawable d) { - ImageView imageView = getImageView(); - if (null == imageView || null == d) { - return; - } - - final float viewWidth = getImageViewWidth(imageView); - final float viewHeight = getImageViewHeight(imageView); - final int drawableWidth = d.getIntrinsicWidth(); - final int drawableHeight = d.getIntrinsicHeight(); - - mBaseMatrix.reset(); - - final float widthScale = viewWidth / drawableWidth; - final float heightScale = viewHeight / drawableHeight; - - if (mScaleType == ScaleType.CENTER) { - mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, - (viewHeight - drawableHeight) / 2F); - - } else if (mScaleType == ScaleType.CENTER_CROP) { - float scale = Math.max(widthScale, heightScale); - mBaseMatrix.postScale(scale, scale); - mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, - (viewHeight - drawableHeight * scale) / 2F); - - } else if (mScaleType == ScaleType.CENTER_INSIDE) { - float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); - mBaseMatrix.postScale(scale, scale); - mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, - (viewHeight - drawableHeight * scale) / 2F); - - } else { - RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); - RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); - - switch (mScaleType) { - case FIT_CENTER: - mBaseMatrix - .setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); - break; - - case FIT_START: - mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); - break; - - case FIT_END: - mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); - break; - - case FIT_XY: - mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); - break; - - default: - break; - } - } - - resetMatrix(); - } - - private int getImageViewWidth(ImageView imageView) { - if (null == imageView) - return 0; - return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); - } - - private int getImageViewHeight(ImageView imageView) { - if (null == imageView) - return 0; - return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); - } - - /** - * Interface definition for a callback to be invoked when the internal - * Matrix has changed for this View. - * - * @author Chris Banes - */ - public static interface OnMatrixChangedListener { - /** - * Callback for when the Matrix displaying the Drawable has changed. - * This could be because the View's bounds have changed, or the user has - * zoomed. - * - * @param rect - Rectangle displaying the Drawable's new bounds. - */ - void onMatrixChanged(RectF rect); - } - - /** - * Interface definition for a callback to be invoked when the Photo is - * tapped with a single tap. - * - * @author Chris Banes - */ - public static interface OnPhotoTapListener { - - /** - * A callback to receive where the user taps on a photo. You will only - * receive a callback if the user taps on the actual photo, tapping on - * 'whitespace' will be ignored. - * - * @param view - View the user tapped. - * @param x - where the user tapped from the of the Drawable, as - * percentage of the Drawable width. - * @param y - where the user tapped from the top of the Drawable, as - * percentage of the Drawable height. - */ - void onPhotoTap(View view, float x, float y); - } - - /** - * Interface definition for a callback to be invoked when the ImageView is - * tapped with a single tap. - * - * @author Chris Banes - */ - public static interface OnViewTapListener { - - /** - * A callback to receive where the user taps on a ImageView. You will - * receive a callback if the user taps anywhere on the view, tapping on - * 'whitespace' will not be ignored. - * - * @param view - View the user tapped. - * @param x - where the user tapped from the left of the View. - * @param y - where the user tapped from the top of the View. - */ - void onViewTap(View view, float x, float y); - } - - private class AnimatedZoomRunnable implements Runnable { - - private final float mFocalX, mFocalY; - private final long mStartTime; - private final float mZoomStart, mZoomEnd; - - public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, - final float focalX, final float focalY) { - mFocalX = focalX; - mFocalY = focalY; - mStartTime = System.currentTimeMillis(); - mZoomStart = currentZoom; - mZoomEnd = targetZoom; - } - - @Override - public void run() { - ImageView imageView = getImageView(); - if (imageView == null) { - return; - } - - float t = interpolate(); - float scale = mZoomStart + t * (mZoomEnd - mZoomStart); - float deltaScale = scale / getScale(); - - mSuppMatrix.postScale(deltaScale, deltaScale, mFocalX, mFocalY); - checkAndDisplayMatrix(); - - // We haven't hit our target scale yet, so post ourselves again - if (t < 1f) { - Compat.postOnAnimation(imageView, this); - } - } - - private float interpolate() { - float t = 1f * (System.currentTimeMillis() - mStartTime) / ZOOM_DURATION; - t = Math.min(1f, t); - t = sInterpolator.getInterpolation(t); - return t; - } - } - - private class FlingRunnable implements Runnable { - - private final ScrollerProxy mScroller; - private int mCurrentX, mCurrentY; - - public FlingRunnable(Context context) { - mScroller = ScrollerProxy.getScroller(context); - } - - public void cancelFling() { - if (DEBUG) { - LogManager.getLogger().d(LOG_TAG, "Cancel Fling"); - } - mScroller.forceFinished(true); - } - - public void fling(int viewWidth, int viewHeight, int velocityX, - int velocityY) { - final RectF rect = getDisplayRect(); - if (null == rect) { - return; - } - - final int startX = Math.round(-rect.left); - final int minX, maxX, minY, maxY; - - if (viewWidth < rect.width()) { - minX = 0; - maxX = Math.round(rect.width() - viewWidth); - } else { - minX = maxX = startX; - } - - final int startY = Math.round(-rect.top); - if (viewHeight < rect.height()) { - minY = 0; - maxY = Math.round(rect.height() - viewHeight); - } else { - minY = maxY = startY; - } - - mCurrentX = startX; - mCurrentY = startY; - - if (DEBUG) { - LogManager.getLogger().d( - LOG_TAG, - "fling. StartX:" + startX + " StartY:" + startY - + " MaxX:" + maxX + " MaxY:" + maxY); - } - - // If we actually can move, fling the scroller - if (startX != maxX || startY != maxY) { - mScroller.fling(startX, startY, velocityX, velocityY, minX, - maxX, minY, maxY, 0, 0); - } - } - - @Override - public void run() { - if (mScroller.isFinished()) { - return; // remaining post that should not be handled - } - - ImageView imageView = getImageView(); - if (null != imageView && mScroller.computeScrollOffset()) { - - final int newX = mScroller.getCurrX(); - final int newY = mScroller.getCurrY(); - - if (DEBUG) { - LogManager.getLogger().d( - LOG_TAG, - "fling run(). CurrentX:" + mCurrentX + " CurrentY:" - + mCurrentY + " NewX:" + newX + " NewY:" - + newY); - } - - mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); - setImageViewMatrix(getDrawMatrix()); - - mCurrentX = newX; - mCurrentY = newY; - - // Post On animation - Compat.postOnAnimation(imageView, this); - } - } - } -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/CupcakeGestureDetector.java b/Clover/app/src/main/java/uk/co/senab/photoview/gestures/CupcakeGestureDetector.java deleted file mode 100644 index d677cdb2..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/CupcakeGestureDetector.java +++ /dev/null @@ -1,143 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.gestures; - -import android.content.Context; -import android.util.Log; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.ViewConfiguration; - -public class CupcakeGestureDetector implements GestureDetector { - - protected OnGestureListener mListener; - private static final String LOG_TAG = "CupcakeGestureDetector"; - float mLastTouchX; - float mLastTouchY; - final float mTouchSlop; - final float mMinimumVelocity; - - @Override - public void setOnGestureListener(OnGestureListener listener) { - this.mListener = listener; - } - - public CupcakeGestureDetector(Context context) { - final ViewConfiguration configuration = ViewConfiguration - .get(context); - mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); - mTouchSlop = configuration.getScaledTouchSlop(); - } - - private VelocityTracker mVelocityTracker; - private boolean mIsDragging; - - float getActiveX(MotionEvent ev) { - return ev.getX(); - } - - float getActiveY(MotionEvent ev) { - return ev.getY(); - } - - @Override - public boolean isScaling() { - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - switch (ev.getAction()) { - case MotionEvent.ACTION_DOWN: { - mVelocityTracker = VelocityTracker.obtain(); - if (null != mVelocityTracker) { - mVelocityTracker.addMovement(ev); - } else { - Log.i(LOG_TAG, "Velocity tracker is null"); - } - - mLastTouchX = getActiveX(ev); - mLastTouchY = getActiveY(ev); - mIsDragging = false; - break; - } - - case MotionEvent.ACTION_MOVE: { - final float x = getActiveX(ev); - final float y = getActiveY(ev); - final float dx = x - mLastTouchX, dy = y - mLastTouchY; - - if (!mIsDragging) { - // Use Pythagoras to see if drag length is larger than - // touch slop - mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; - } - - if (mIsDragging) { - mListener.onDrag(dx, dy); - mLastTouchX = x; - mLastTouchY = y; - - if (null != mVelocityTracker) { - mVelocityTracker.addMovement(ev); - } - } - break; - } - - case MotionEvent.ACTION_CANCEL: { - // Recycle Velocity Tracker - if (null != mVelocityTracker) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - break; - } - - case MotionEvent.ACTION_UP: { - if (mIsDragging) { - if (null != mVelocityTracker) { - mLastTouchX = getActiveX(ev); - mLastTouchY = getActiveY(ev); - - // Compute velocity within the last 1000ms - mVelocityTracker.addMovement(ev); - mVelocityTracker.computeCurrentVelocity(1000); - - final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker - .getYVelocity(); - - // If the velocity is greater than minVelocity, call - // listener - if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { - mListener.onFling(mLastTouchX, mLastTouchY, -vX, - -vY); - } - } - } - - // Recycle Velocity Tracker - if (null != mVelocityTracker) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - break; - } - } - - return true; - } -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/EclairGestureDetector.java b/Clover/app/src/main/java/uk/co/senab/photoview/gestures/EclairGestureDetector.java deleted file mode 100644 index 11439c91..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/EclairGestureDetector.java +++ /dev/null @@ -1,85 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.gestures; - -import uk.co.senab.photoview.Compat; -import android.annotation.TargetApi; -import android.content.Context; -import android.view.MotionEvent; - -@TargetApi(5) -public class EclairGestureDetector extends CupcakeGestureDetector { - - private static final int INVALID_POINTER_ID = -1; - private int mActivePointerId = INVALID_POINTER_ID; - private int mActivePointerIndex = 0; - - public EclairGestureDetector(Context context) { - super(context); - } - - @Override - float getActiveX(MotionEvent ev) { - try { - return ev.getX(mActivePointerIndex); - } catch (Exception e) { - return ev.getX(); - } - } - - @Override - float getActiveY(MotionEvent ev) { - try { - return ev.getY(mActivePointerIndex); - } catch (Exception e) { - return ev.getY(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - final int action = ev.getAction(); - switch (action & MotionEvent.ACTION_MASK) { - case MotionEvent.ACTION_DOWN: - mActivePointerId = ev.getPointerId(0); - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - mActivePointerId = INVALID_POINTER_ID; - break; - case MotionEvent.ACTION_POINTER_UP: - // Ignore deprecation, ACTION_POINTER_ID_MASK and - // ACTION_POINTER_ID_SHIFT has same value and are deprecated - // You can have either deprecation or lint target api warning - final int pointerIndex = Compat.getPointerIndex(ev.getAction()); - final int pointerId = ev.getPointerId(pointerIndex); - if (pointerId == mActivePointerId) { - // This was our active pointer going up. Choose a new - // active pointer and adjust accordingly. - final int newPointerIndex = pointerIndex == 0 ? 1 : 0; - mActivePointerId = ev.getPointerId(newPointerIndex); - mLastTouchX = ev.getX(newPointerIndex); - mLastTouchY = ev.getY(newPointerIndex); - } - break; - } - - mActivePointerIndex = ev - .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId - : 0); - return super.onTouchEvent(ev); - } -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/FroyoGestureDetector.java b/Clover/app/src/main/java/uk/co/senab/photoview/gestures/FroyoGestureDetector.java deleted file mode 100644 index e9b5acb0..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/FroyoGestureDetector.java +++ /dev/null @@ -1,63 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.gestures; - -import android.annotation.TargetApi; -import android.content.Context; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; - -@TargetApi(8) -public class FroyoGestureDetector extends EclairGestureDetector { - - protected final ScaleGestureDetector mDetector; - - public FroyoGestureDetector(Context context) { - super(context); - ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { - - @Override - public boolean onScale(ScaleGestureDetector detector) { - mListener.onScale(detector.getScaleFactor(), - detector.getFocusX(), detector.getFocusY()); - return true; - } - - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - return true; - } - - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - // NO-OP - } - }; - mDetector = new ScaleGestureDetector(context, mScaleListener); - } - - @Override - public boolean isScaling() { - return mDetector.isInProgress(); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - mDetector.onTouchEvent(ev); - return super.onTouchEvent(ev); - } - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/GestureDetector.java b/Clover/app/src/main/java/uk/co/senab/photoview/gestures/GestureDetector.java deleted file mode 100644 index 5433fe64..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/GestureDetector.java +++ /dev/null @@ -1,28 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.gestures; - -import android.view.MotionEvent; - -public interface GestureDetector { - - public boolean onTouchEvent(MotionEvent ev); - - public boolean isScaling(); - - public void setOnGestureListener(OnGestureListener listener); - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/OnGestureListener.java b/Clover/app/src/main/java/uk/co/senab/photoview/gestures/OnGestureListener.java deleted file mode 100644 index d0555ced..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/OnGestureListener.java +++ /dev/null @@ -1,27 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.gestures; - -public interface OnGestureListener { - - public void onDrag(float dx, float dy); - - public void onFling(float startX, float startY, float velocityX, - float velocityY); - - public void onScale(float scaleFactor, float focusX, float focusY); - -} \ No newline at end of file diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/VersionedGestureDetector.java b/Clover/app/src/main/java/uk/co/senab/photoview/gestures/VersionedGestureDetector.java deleted file mode 100644 index c1550ce3..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/gestures/VersionedGestureDetector.java +++ /dev/null @@ -1,42 +0,0 @@ -package uk.co.senab.photoview.gestures; - -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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. - *******************************************************************************/ - -import android.content.Context; -import android.os.Build; - -public final class VersionedGestureDetector { - - public static GestureDetector newInstance(Context context, - OnGestureListener listener) { - final int sdkVersion = Build.VERSION.SDK_INT; - GestureDetector detector; - - if (sdkVersion < Build.VERSION_CODES.ECLAIR) { - detector = new CupcakeGestureDetector(context); - } else if (sdkVersion < Build.VERSION_CODES.FROYO) { - detector = new EclairGestureDetector(context); - } else { - detector = new FroyoGestureDetector(context); - } - - detector.setOnGestureListener(listener); - - return detector; - } - -} \ No newline at end of file diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/log/LogManager.java b/Clover/app/src/main/java/uk/co/senab/photoview/log/LogManager.java deleted file mode 100644 index eefdde39..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/log/LogManager.java +++ /dev/null @@ -1,35 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.log; - -import android.util.Log; - -/** - * class that holds the {@link Logger} for this library, defaults to {@link LoggerDefault} to send logs to android {@link Log} - */ -public final class LogManager { - - private static Logger logger = new LoggerDefault(); - - public static void setLogger(Logger newLogger) { - logger = newLogger; - } - - public static Logger getLogger() { - return logger; - } - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/log/Logger.java b/Clover/app/src/main/java/uk/co/senab/photoview/log/Logger.java deleted file mode 100644 index 5324d75f..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/log/Logger.java +++ /dev/null @@ -1,116 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.log; - -/** - * interface for a logger class to replace the static calls to {@link android.util.Log} - */ -public interface Logger { - /** - * Send a {@link android.util.Log#VERBOSE} log message. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - int v(String tag, String msg); - - /** - * Send a {@link android.util.Log#VERBOSE} log message and log the exception. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - int v(String tag, String msg, Throwable tr); - - /** - * Send a {@link android.util.Log#DEBUG} log message. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - int d(String tag, String msg); - - /** - * Send a {@link android.util.Log#DEBUG} log message and log the exception. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - int d(String tag, String msg, Throwable tr); - - /** - * Send an {@link android.util.Log#INFO} log message. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - int i(String tag, String msg); - - /** - * Send a {@link android.util.Log#INFO} log message and log the exception. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - int i(String tag, String msg, Throwable tr); - - /** - * Send a {@link android.util.Log#WARN} log message. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - int w(String tag, String msg); - - /** - * Send a {@link android.util.Log#WARN} log message and log the exception. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - int w(String tag, String msg, Throwable tr); - - /** - * Send an {@link android.util.Log#ERROR} log message. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - */ - int e(String tag, String msg); - - /** - * Send a {@link android.util.Log#ERROR} log message and log the exception. - * - * @param tag Used to identify the source of a log message. It usually identifies - * the class or activity where the log call occurs. - * @param msg The message you would like logged. - * @param tr An exception to log - */ - int e(String tag, String msg, Throwable tr); -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/log/LoggerDefault.java b/Clover/app/src/main/java/uk/co/senab/photoview/log/LoggerDefault.java deleted file mode 100644 index f827f4a8..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/log/LoggerDefault.java +++ /dev/null @@ -1,76 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.log; - -import android.util.Log; - -/** - * Helper class to redirect {@link LogManager#logger} to {@link Log} - */ -public class LoggerDefault implements Logger { - - @Override - public int v(String tag, String msg) { - return Log.v(tag, msg); - } - - @Override - public int v(String tag, String msg, Throwable tr) { - return Log.v(tag, msg, tr); - } - - @Override - public int d(String tag, String msg) { - return Log.d(tag, msg); - } - - @Override - public int d(String tag, String msg, Throwable tr) { - return Log.d(tag, msg, tr); - } - - @Override - public int i(String tag, String msg) { - return Log.i(tag, msg); - } - - @Override - public int i(String tag, String msg, Throwable tr) { - return Log.i(tag, msg, tr); - } - - @Override - public int w(String tag, String msg) { - return Log.w(tag, msg); - } - - @Override - public int w(String tag, String msg, Throwable tr) { - return Log.w(tag, msg, tr); - } - - @Override - public int e(String tag, String msg) { - return Log.e(tag, msg); - } - - @Override - public int e(String tag, String msg, Throwable tr) { - return Log.e(tag, msg, tr); - } - - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/GingerScroller.java b/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/GingerScroller.java deleted file mode 100644 index c58cdb96..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/GingerScroller.java +++ /dev/null @@ -1,68 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.scrollerproxy; - -import android.annotation.TargetApi; -import android.content.Context; -import android.widget.OverScroller; - -@TargetApi(9) -public class GingerScroller extends ScrollerProxy { - - protected final OverScroller mScroller; - private boolean mFirstScroll = false; - - public GingerScroller(Context context) { - mScroller = new OverScroller(context); - } - - @Override - public boolean computeScrollOffset() { - // Workaround for first scroll returning 0 for the direction of the edge it hits. - // Simply recompute values. - if (mFirstScroll) { - mScroller.computeScrollOffset(); - mFirstScroll = false; - } - return mScroller.computeScrollOffset(); - } - - @Override - public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, - int overX, int overY) { - mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY); - } - - @Override - public void forceFinished(boolean finished) { - mScroller.forceFinished(finished); - } - - @Override - public boolean isFinished() { - return mScroller.isFinished(); - } - - @Override - public int getCurrX() { - return mScroller.getCurrX(); - } - - @Override - public int getCurrY() { - return mScroller.getCurrY(); - } -} \ No newline at end of file diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/IcsScroller.java b/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/IcsScroller.java deleted file mode 100644 index 10f9eab8..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/IcsScroller.java +++ /dev/null @@ -1,33 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.scrollerproxy; - -import android.annotation.TargetApi; -import android.content.Context; - -@TargetApi(14) -public class IcsScroller extends GingerScroller { - - public IcsScroller(Context context) { - super(context); - } - - @Override - public boolean computeScrollOffset() { - return mScroller.computeScrollOffset(); - } - -} diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/PreGingerScroller.java b/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/PreGingerScroller.java deleted file mode 100644 index 2cd91b3f..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/PreGingerScroller.java +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.scrollerproxy; - -import android.content.Context; -import android.widget.Scroller; - -public class PreGingerScroller extends ScrollerProxy { - - private final Scroller mScroller; - - public PreGingerScroller(Context context) { - mScroller = new Scroller(context); - } - - @Override - public boolean computeScrollOffset() { - return mScroller.computeScrollOffset(); - } - - @Override - public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, - int overX, int overY) { - mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); - } - - @Override - public void forceFinished(boolean finished) { - mScroller.forceFinished(finished); - } - - public boolean isFinished() { - return mScroller.isFinished(); - } - - @Override - public int getCurrX() { - return mScroller.getCurrX(); - } - - @Override - public int getCurrY() { - return mScroller.getCurrY(); - } -} \ No newline at end of file diff --git a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/ScrollerProxy.java b/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/ScrollerProxy.java deleted file mode 100644 index ce39ef19..00000000 --- a/Clover/app/src/main/java/uk/co/senab/photoview/scrollerproxy/ScrollerProxy.java +++ /dev/null @@ -1,48 +0,0 @@ -/******************************************************************************* - * Copyright 2011, 2012 Chris Banes. - * - * 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 uk.co.senab.photoview.scrollerproxy; - -import android.content.Context; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; - -public abstract class ScrollerProxy { - - public static ScrollerProxy getScroller(Context context) { - if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) { - return new PreGingerScroller(context); - } else if (VERSION.SDK_INT < VERSION_CODES.ICE_CREAM_SANDWICH) { - return new GingerScroller(context); - } else { - return new IcsScroller(context); - } - } - - public abstract boolean computeScrollOffset(); - - public abstract void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, - int maxY, int overX, int overY); - - public abstract void forceFinished(boolean finished); - - public abstract boolean isFinished(); - - public abstract int getCurrX(); - - public abstract int getCurrY(); - - -} diff --git a/Clover/app/src/main/res/drawable-hdpi/progress_primary_holo_light.9.png b/Clover/app/src/main/res/drawable-hdpi/progress_primary_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..2f76a22648d1e1a49b6462736994deebb6a35899 GIT binary patch literal 873 zcmV-v1D5=WP)DaJY4;Tc>`U@6@Fr2>`Sdx1t5d?E(BnvfCzDRGF6gF4csf$in%H4Xw827`PE6p zqeuCAn1O%?;7&EP$#lkYy<|0Z);ThuFh}CN%NA<4lv+~3Q;3EH14|d9S(1s_O@-$m zPs-+yQzn`vx4i-WpD*9O)t?U?7nkCBV z^b8Bia#|S-T}U!VKp;FoIgoT0gV72Q1e9fVM&YDb^%1b>nGe4|_-SF;A`S_FM}mb2 z0`P>EDZ&t84p;(=gek)W8W7Fs1|$<`Mi~SGFad@v5->?i`;Zx6PVt9Jm>Woo1p1fA z|FF)M<}Sv--+T3=i;sJwzQDA=l}+C)p6BHgc-~~NrMP%^fCS|fP3J#O2jl?H$An36 z0tUh%89={Ue_4bGU=H#zfbXt3+5)}>A8x>=ue`9jh-^;iDa@_ry2a31?eH#lUI^Yr zy2D)|dlrMiJRv(^0hAMRWCoFeLJ;J%oRBynY!u6I{Oc#1r`I$vfqm)mD)N1MQ)|yg zJbUA1UQ1+pP4rpC({^TE67QC<*_1ou@2!z(A7OqsVE< zaq_!Xnk!pws@>m+-5p>Xzz{_B9NX)Twj47)E7;5`ZrD2O=_zGow-VW=72cg@&db)K z)5^9Zib_?bbS!nVJ#`0PZDj!ZZ!kC2V3Btlz$Uo!1e~X*zjls#*dQ4ocnnYs5(Ho`86 zCc27}|9XYy`m~5+%ukhBcc__T#ZfXox%C31axTjJ6sxguHmyGJ_SSf)K5>aisqkD^7-ItXY*uv(G^APB_+!~MbG zVC&iKbjHE@E0@iO2R8oRSCxhtJ*~$Y;;Mw@dKh{tv1AQac}ya)aw3!}h(^V$f)uj% z)~i-$3kU!Y-t2q;^n|;T5~hej(mcW-8-ispa8ng?+{95%Vk(X~D7XNc@Li@2<{fAb z%Vf1v{+XYy{Th?0|Awi^tk~(?tXp}Rm00000NkvXXu0mjf_HqHy literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable-xhdpi/progress_primary_holo_light.9.png b/Clover/app/src/main/res/drawable-xhdpi/progress_primary_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..e62123c4d45d4de95b73db947f09cbb7edc4113c GIT binary patch literal 1285 zcmV+g1^W7lP)w-zSTJ4{Ou>Y1deZ-!OP+`@z;1hy!P?u zgkjs@C*+1<$HFIaO`2)AU`o z3QxzAVcgE^t-TNH-X_WQ>)AXv;T_->Vi#Sy_zoA}*;3!YFx!U*CDuBTtBM@W>6#)> z4>>G-r0z4DSn7|YQB#J34u_e7LE}`U5TTH&6dBH5jY3LWQJh4!-E1y1Y_T=Z!{8pz za+Ph1U%|UKQM36kWmltUkn6gJS*P*2Mq`Eg>eAFuUx&yN8oQM0R5lc+!tM%^N<>LV z7(%KctFdd4Q5+$P8`}|bktHY=)TB;>oT;+!n9M);#peFgPy8;t{Q+Eg6YY;TbpPN$ ztLM+DxQWTAQH)ZkM&A)`i8_xGNPT1h2SEYCb^(R$Mjj%GLJ&ZMA|x8ilGR{G5UoMT zcG}3rL=&Nok^*P|-uu%he?6h9$K_K2+%>@$XtgatXI18^AdU$@1;V6ak)>oWGc!^umr$V$hO713sb-fBTa73*a=W#p9jii|RaCBObAfhxnj_=3W9YulCR=ajQmk=!ly0UgM+P67|`xa*EUD>Nh9zfB&&A|F^IkKPJ{#z{~b zLe&tpmuB+Ne>Q67cbYA8UHOV_tzjGZ!_vWrYrl^!ufWDG9CwHf$OrrMS3<+*()SOF zI(6}*OT1Gjg9k4`tV z4nGFwXyf0gA5Y0+!FSdf))syF{FB~wH1(Yy{UG_{t1lXO_5ZBk4@}xo!OqnrV0ATB6K?00000NkvXXu0mjfl%{@_ literal 0 HcmV?d00001 diff --git a/Clover/app/src/main/res/drawable-xxhdpi/progress_primary_holo_light.9.png b/Clover/app/src/main/res/drawable-xxhdpi/progress_primary_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..add4d383807610148174bfc3d99ac9a72e281f62 GIT binary patch literal 3029 zcmbVOc|25m8=taoWxJxpH15TvF>}V4F_Wb+Lo>1^YbwUf!Ne@)42H5t6j>8eTuWJ7 zl(Kb&db9Kta^Lrl`?-I-=lPuT+n(?DS$@y+`J7Z2ryWZ28|7gz zm=cXjaTSjg@r#j_7Jpv{MRUZ1mcZ6au$vnoh@kU97>UXC2jMg}JrHyS>CDK`ZqNb- zlL%nBc?rB69f%Ar+l0QVV5d z^2lHiiyFlTcSkw7F`|MQ1SZ1T3T`1JiV4`DfDRY3LpTsoXo>jBOBBynw^0cAR~139 zCE}Y?UXCtsGM5j+%}mfp1{#Nk;{X%D6pP2>x56=K0F6RpPyhxA;EARfA{q_<{vgEG z_{;#JE5+`+E%C|{5hM`sh$vJ0h06$IgB$^kVu=u+ zm@rvPqAfr+$70ENJOzWsnFD}19# zad~j^ZZo(eox$R)u2-+;TeK9A&k6^bc6=@y{x!cu*5BACknC_2tPS2A{XK4TGLAsN z;KU^b*pkoy;yaf4|KtoMb_TUt9RE`+KeogbxH|o-^u>$6ng`^FJBBZAjiwFtw_q?? zDH?_3CVcYFrXub6vvf&o(e&}wS%W@^}>K;qwH;k#(_gHr=q#a7c zd4qNPa4MPU*U^&XHIfpTvNxpEyYy9MadKD()IUKR#gdO>`*K<>V^Ua{X5($b~~kjnoTvf4Zv2FLqBB zW{#$FL~p>#W&_;w$12bTdSO#|{Ub~;tTUM=8mSS))^X|dH<(4mY zmf5{Ebm>r5e_NT;!A>1ehBJRs^u#H4z2qnDt`9Q9iE;%>!Xvj0Z~GamM(rDkNzs2I zjaaz4_U%*27k^|Pv%C3PxsoLx^Xvuh^zl9~-?d9U@+W2%6Y@@0Z3a$}iXFEO1$a74 zg*Rq&U+}{D(33mN4Nip7{A*R&IW+Lh&jk~01YpsaTW25@_wKY@0la<3(?p#LC?W$N zy>Y!yTcWIlpx@~;9=0hCQT@7#8D&?ls&d}+jjr#~sj#%rDAnz!sJun{_igsS?$N1~ ze4}leC*h>9Zc$_0tG!5bOULgwRO6y+j@~nnzs!zPe0n1d%Enx>-xgf=aH`QIQP@y! z;TaiT?c3k{o31y?p-YBRtHuql8E+IlmU*8$(Gq6#CGy6CW>s2l%w+qy6b#Iou%T8N zRN^hnZl#zbmFauV*0-u#HQUQ&Ntc*Ak9Q=p{e2JKKuC2Jg&O0|sykr`HZ!ABjqUgA zWtr5rV)S-})btU}JL^SoM17tD+0IC#*+Fsq7p0?Tqg{s(k75@;)U*%#w!j$*%)@`o z2&xsk^5!G)BO`+F)!Y3ke-6sGEV;TACL0=ADBz5|cN!P& zTnB}uxNhi>;2ciD=Od&YlE(GbG|)C0R1Hk#%hYTb=kOL~1S4-9BZT++396j-<-udN z_Ox4@-YaM@zLmt4uU?WQ#AzzvhC3MRbc>Rv!@I>8RZ z2w!*X{N}dsH48I%?JE;85A*YhF&`}eMp|H+D6otWyLdHvPsz%WzU5`zm`^7cJ*qiO zYo}qMhb=%1nDLfvu7VF$we8pq{iX#$*8MB%o)%jNOiYhCyt_&h-I}YAI=?SBH0K$zOwzgFY}3{1ITn51=hgme=_vaLEg2-)zQ~nulEdY`kYn@(&yr%|zceyA zWpjhoB&DX@utqn_-07(jpX^4}pp6yVcIAvv534@c)F8F_iGH=oFnSO~k2ZU<-RzkC z;QVC6y)O#~iWor~QE^>wH{~u>`jt&4868@7aw7lh1nz#*U~W@|pu|1z`a-9tK}77- zw9|=c5viGc4`fmFFn8wVdidQFnQ5wsyJr)2rr@MLRl7-_cE89jne>3e+`}j2`!B*b z6#wR|=@~Dr)e|P;s`<#KZFJ1=RvjgE-9@Q5eI11T5;YlPJzM@c_8#_yoTOGNV{3ZC znReqxr3*VwOjBxyYz({Z8za=Z^O|xPIFA-`#oM}~Yv&dbz6EYg&OVR4{qGv<>KX1; z9#~UZx;i!{E)<0rto; z$;WgFl~SV2gV5I%!iLcsXWFXNXd1zHhdyja-J1U};T|d3a4>d$#^)ley087SR-x7N z6-^&lx}o;^fb~?P_nIno*{OX(ElQ-Qb8qSk_7#g=1=rR{Bccxj%4V|!MV*S0xZWaT zV_3PAkKdd4l8iJ}y^dC-@_II`G{02_9-4d%C}Dax*9RHiJ?W7>V)(W?QP6j9PE>P5 zi!zY+A_|l^ekePO_G)nn8DH7+)4=T`YB6$_btaw$aGCN6g|XY6wV0FRAtOI2KOBEp za`cLpmfab6Y|@n z2OmCxrq0hNMPk)8g05uJtNLm$(B_T{|81Rcj(kBqyA$i2-@I@3W+Xok6&<_ZJH=|$ zA~YjqPr)_yOR^Qu?OMx!#$CO+C#(1XG$k-kRqhd8fna2d~FRHzd^#wZGfH zpsToia_^j!LP4>~lkk8dsIzzYQ2Li!yv>`gFXNJLrvHBYvp#giddbKi8c22tr7A_} zWfq^w%ozEzPcJ0Bb*v** zEM1AR1#_3DiR4KSHP^2)%hTdLjfF4VFI;h-i + + + + + diff --git a/Clover/app/src/main/res/values/dimens.xml b/Clover/app/src/main/res/values/dimens.xml index c5dfda6d..6345d959 100644 --- a/Clover/app/src/main/res/values/dimens.xml +++ b/Clover/app/src/main/res/values/dimens.xml @@ -17,5 +17,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . --> - 8dp