├── .gitignore
├── LICENSE
├── README.md
├── app
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ └── com
│ │ └── gtp
│ │ └── showapicturetoyourfriend
│ │ ├── TouchImageView.java
│ │ └── receiverpictureactivity.java
│ └── res
│ ├── drawable
│ └── ic_launcher.png
│ ├── layout
│ ├── activity_receivemultiple.xml
│ ├── activity_receiverpictureactivity.xml
│ ├── adapterimage.xml
│ └── adaptervideo.xml
│ ├── values-de
│ └── strings.xml
│ ├── values-pt-rBR
│ └── strings.xml
│ └── values
│ ├── colors.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ └── gradle-wrapper.properties
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Somethingweirdhere
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Secure Photo Viewer
2 | This app is here to protect you from you friends and enemies. Have you ever handed over your phone to show someone the latest meme? Or a cool pic you took? How often did they scroll too far and have seen something embarrassing in your gallery or image browser?
3 |
4 | Now we have a solution: Select the pictures you want them to see in your gallery, press share and select the Secure Photo Viewer! Now they will only see what you want them to see. It works with any amount of photos or videos you share from you gallery! It's that easy to protect your privacy from nosy friends.
5 |
6 |
7 |
8 |
9 |
10 |
11 | # For developers:
12 |
13 | Clone this repo via Android Studio and let Gradle install all dependencies. Then build the project, and run it.
14 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | defaultConfig {
6 | applicationId "com.gtp.showapicturetoyourfriend"
7 | minSdkVersion 15
8 | targetSdkVersion 28
9 | versionCode 5
10 | versionName "1.4"
11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
12 | }
13 | buildTypes {
14 | release {
15 | minifyEnabled true
16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17 | }
18 | }
19 | }
20 |
21 | repositories {
22 | mavenCentral()
23 | maven { url 'https://maven.google.com' }
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
29 | exclude group: 'com.android.support', module: 'support-annotations'
30 | })
31 | implementation 'com.android.support:appcompat-v7:28.0.0'
32 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
33 | testImplementation 'junit:junit:4.12'
34 | implementation 'com.android.support:design:28.0.0'
35 |
36 | implementation 'com.github.bumptech.glide:glide:3.8.0'
37 | implementation 'io.github.kobakei:ratethisapp:1.2.0'
38 | }
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\Gregor\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gregordr/Secure-Photo-Viewer/ed2e6238753bba4aa15d4eb735cb1dfe573fa2a6/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/com/gtp/showapicturetoyourfriend/TouchImageView.java:
--------------------------------------------------------------------------------
1 | package com.gtp.showapicturetoyourfriend;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.Context;
5 | import android.content.res.Configuration;
6 | import android.graphics.Bitmap;
7 | import android.graphics.Canvas;
8 | import android.graphics.Matrix;
9 | import android.graphics.PointF;
10 | import android.graphics.RectF;
11 | import android.graphics.drawable.Drawable;
12 | import android.net.Uri;
13 | import android.os.Build;
14 | import android.os.Build.VERSION;
15 | import android.os.Build.VERSION_CODES;
16 | import android.os.Bundle;
17 | import android.os.Parcelable;
18 | import android.util.AttributeSet;
19 | import android.util.Log;
20 | import android.view.GestureDetector;
21 | import android.view.MotionEvent;
22 | import android.view.ScaleGestureDetector;
23 | import android.view.View;
24 | import android.view.animation.AccelerateDecelerateInterpolator;
25 | import android.widget.ImageView;
26 | import android.widget.OverScroller;
27 | import android.widget.Scroller;
28 |
29 | public class TouchImageView extends ImageView {
30 |
31 | private static final String DEBUG = "DEBUG";
32 |
33 | //
34 | // SuperMin and SuperMax multipliers. Determine how much the image can be
35 | // zoomed below or above the zoom boundaries, before animating back to the
36 | // min/max zoom boundary.
37 | //
38 | private static final float SUPER_MIN_MULTIPLIER = .75f;
39 | private static final float SUPER_MAX_MULTIPLIER = 1.25f;
40 |
41 | //
42 | // Scale of image ranges from minScale to maxScale, where minScale == 1
43 | // when the image is stretched to fit view.
44 | //
45 | private float normalizedScale;
46 |
47 | //
48 | // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
49 | // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
50 | // saved prior to the screen rotating.
51 | //
52 | private Matrix matrix, prevMatrix;
53 |
54 | private static enum State { NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM };
55 | private State state;
56 |
57 | private float minScale;
58 | private float maxScale;
59 | private float superMinScale;
60 | private float superMaxScale;
61 | private float[] m;
62 |
63 | private Context context;
64 | private Fling fling;
65 |
66 | private ScaleType mScaleType;
67 |
68 | private boolean imageRenderedAtLeastOnce;
69 | private boolean onDrawReady;
70 |
71 | private ZoomVariables delayedZoomVariables;
72 |
73 | //
74 | // Size of view and previous view size (ie before rotation)
75 | //
76 | private int viewWidth, viewHeight, prevViewWidth, prevViewHeight;
77 |
78 | //
79 | // Size of image when it is stretched to fit view. Before and After rotation.
80 | //
81 | private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight;
82 |
83 | private ScaleGestureDetector mScaleDetector;
84 | private GestureDetector mGestureDetector;
85 | private GestureDetector.OnDoubleTapListener doubleTapListener = null;
86 | private OnTouchListener userTouchListener = null;
87 | private OnTouchImageViewListener touchImageViewListener = null;
88 |
89 | public TouchImageView(Context context) {
90 | super(context);
91 | sharedConstructing(context);
92 | }
93 |
94 | public TouchImageView(Context context, AttributeSet attrs) {
95 | super(context, attrs);
96 | sharedConstructing(context);
97 | }
98 |
99 | public TouchImageView(Context context, AttributeSet attrs, int defStyle) {
100 | super(context, attrs, defStyle);
101 | sharedConstructing(context);
102 | }
103 |
104 | private void sharedConstructing(Context context) {
105 | super.setClickable(true);
106 | this.context = context;
107 | mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
108 | mGestureDetector = new GestureDetector(context, new GestureListener());
109 | matrix = new Matrix();
110 | prevMatrix = new Matrix();
111 | m = new float[9];
112 | normalizedScale = 1;
113 | if (mScaleType == null) {
114 | mScaleType = ScaleType.FIT_CENTER;
115 | }
116 | minScale = 1;
117 | maxScale = 3;
118 | superMinScale = SUPER_MIN_MULTIPLIER * minScale;
119 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
120 | setImageMatrix(matrix);
121 | setScaleType(ScaleType.MATRIX);
122 | setState(State.NONE);
123 | onDrawReady = false;
124 | super.setOnTouchListener(new PrivateOnTouchListener());
125 | }
126 |
127 | @Override
128 | public void setOnTouchListener(View.OnTouchListener l) {
129 | userTouchListener = l;
130 | }
131 |
132 | public void setOnTouchImageViewListener(OnTouchImageViewListener l) {
133 | touchImageViewListener = l;
134 | }
135 |
136 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener l) {
137 | doubleTapListener = l;
138 | }
139 |
140 | @Override
141 | public void setImageResource(int resId) {
142 | super.setImageResource(resId);
143 | savePreviousImageValues();
144 | fitImageToView();
145 | }
146 |
147 | @Override
148 | public void setImageBitmap(Bitmap bm) {
149 | super.setImageBitmap(bm);
150 | savePreviousImageValues();
151 | fitImageToView();
152 | }
153 |
154 | @Override
155 | public void setImageDrawable(Drawable drawable) {
156 | super.setImageDrawable(drawable);
157 | savePreviousImageValues();
158 | fitImageToView();
159 | }
160 |
161 | @Override
162 | public void setImageURI(Uri uri) {
163 | super.setImageURI(uri);
164 | savePreviousImageValues();
165 | fitImageToView();
166 | }
167 |
168 | @Override
169 | public void setScaleType(ScaleType type) {
170 | if (type == ScaleType.FIT_START || type == ScaleType.FIT_END) {
171 | throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
172 | }
173 | if (type == ScaleType.MATRIX) {
174 | super.setScaleType(ScaleType.MATRIX);
175 |
176 | } else {
177 | mScaleType = type;
178 | if (onDrawReady) {
179 | //
180 | // If the image is already rendered, scaleType has been called programmatically
181 | // and the TouchImageView should be updated with the new scaleType.
182 | //
183 | setZoom(this);
184 | }
185 | }
186 | }
187 |
188 | @Override
189 | public ScaleType getScaleType() {
190 | return mScaleType;
191 | }
192 |
193 | /**
194 | * Returns false if image is in initial, unzoomed state. False, otherwise.
195 | * @return true if image is zoomed
196 | */
197 | public boolean isZoomed() {
198 | return normalizedScale != 1;
199 | }
200 |
201 | /**
202 | * Return a Rect representing the zoomed image.
203 | * @return rect representing zoomed image
204 | */
205 | public RectF getZoomedRect() {
206 | if (mScaleType == ScaleType.FIT_XY) {
207 | throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY");
208 | }
209 | PointF topLeft = transformCoordTouchToBitmap(0, 0, true);
210 | PointF bottomRight = transformCoordTouchToBitmap(viewWidth, viewHeight, true);
211 |
212 | if(getDrawable() == null) return new RectF(0,0,0,0);
213 |
214 | float w = getDrawable().getIntrinsicWidth();
215 | float h = getDrawable().getIntrinsicHeight();
216 | return new RectF(topLeft.x / w, topLeft.y / h, bottomRight.x / w, bottomRight.y / h);
217 | }
218 |
219 | /**
220 | * Save the current matrix and view dimensions
221 | * in the prevMatrix and prevView variables.
222 | */
223 | private void savePreviousImageValues() {
224 | if (matrix != null && viewHeight != 0 && viewWidth != 0) {
225 | matrix.getValues(m);
226 | prevMatrix.setValues(m);
227 | prevMatchViewHeight = matchViewHeight;
228 | prevMatchViewWidth = matchViewWidth;
229 | prevViewHeight = viewHeight;
230 | prevViewWidth = viewWidth;
231 | }
232 | }
233 |
234 | @Override
235 | public Parcelable onSaveInstanceState() {
236 | Bundle bundle = new Bundle();
237 | bundle.putParcelable("instanceState", super.onSaveInstanceState());
238 | bundle.putFloat("saveScale", normalizedScale);
239 | bundle.putFloat("matchViewHeight", matchViewHeight);
240 | bundle.putFloat("matchViewWidth", matchViewWidth);
241 | bundle.putInt("viewWidth", viewWidth);
242 | bundle.putInt("viewHeight", viewHeight);
243 | matrix.getValues(m);
244 | bundle.putFloatArray("matrix", m);
245 | bundle.putBoolean("imageRendered", imageRenderedAtLeastOnce);
246 | return bundle;
247 | }
248 |
249 | @Override
250 | public void onRestoreInstanceState(Parcelable state) {
251 | if (state instanceof Bundle) {
252 | Bundle bundle = (Bundle) state;
253 | normalizedScale = bundle.getFloat("saveScale");
254 | m = bundle.getFloatArray("matrix");
255 | prevMatrix.setValues(m);
256 | prevMatchViewHeight = bundle.getFloat("matchViewHeight");
257 | prevMatchViewWidth = bundle.getFloat("matchViewWidth");
258 | prevViewHeight = bundle.getInt("viewHeight");
259 | prevViewWidth = bundle.getInt("viewWidth");
260 | imageRenderedAtLeastOnce = bundle.getBoolean("imageRendered");
261 | super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
262 | return;
263 | }
264 |
265 | super.onRestoreInstanceState(state);
266 | }
267 |
268 | @Override
269 | protected void onDraw(Canvas canvas) {
270 | onDrawReady = true;
271 | imageRenderedAtLeastOnce = true;
272 | if (delayedZoomVariables != null) {
273 | setZoom(delayedZoomVariables.scale, delayedZoomVariables.focusX, delayedZoomVariables.focusY, delayedZoomVariables.scaleType);
274 | delayedZoomVariables = null;
275 | }
276 | super.onDraw(canvas);
277 | }
278 |
279 | @Override
280 | public void onConfigurationChanged(Configuration newConfig) {
281 | super.onConfigurationChanged(newConfig);
282 | savePreviousImageValues();
283 | }
284 |
285 | /**
286 | * Get the max zoom multiplier.
287 | * @return max zoom multiplier.
288 | */
289 | public float getMaxZoom() {
290 | return maxScale;
291 | }
292 |
293 | /**
294 | * Set the max zoom multiplier. Default value: 3.
295 | * @param max max zoom multiplier.
296 | */
297 | public void setMaxZoom(float max) {
298 | maxScale = max;
299 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
300 | }
301 |
302 | /**
303 | * Get the min zoom multiplier.
304 | * @return min zoom multiplier.
305 | */
306 | public float getMinZoom() {
307 | return minScale;
308 | }
309 |
310 | /**
311 | * Get the current zoom. This is the zoom relative to the initial
312 | * scale, not the original resource.
313 | * @return current zoom multiplier.
314 | */
315 | public float getCurrentZoom() {
316 | return normalizedScale;
317 | }
318 |
319 | /**
320 | * Set the min zoom multiplier. Default value: 1.
321 | * @param min min zoom multiplier.
322 | */
323 | public void setMinZoom(float min) {
324 | minScale = min;
325 | superMinScale = SUPER_MIN_MULTIPLIER * minScale;
326 | }
327 |
328 | /**
329 | * Reset zoom and translation to initial state.
330 | */
331 | public void resetZoom() {
332 | normalizedScale = 1;
333 | fitImageToView();
334 | }
335 |
336 | /**
337 | * Set zoom to the specified scale. Image will be centered by default.
338 | * @param scale
339 | */
340 | public void setZoom(float scale) {
341 | setZoom(scale, 0.5f, 0.5f);
342 | }
343 |
344 | /**
345 | * Set zoom to the specified scale. Image will be centered around the point
346 | * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
347 | * as a fraction from the left and top of the view. For example, the top left
348 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
349 | * @param scale
350 | * @param focusX
351 | * @param focusY
352 | */
353 | public void setZoom(float scale, float focusX, float focusY) {
354 | setZoom(scale, focusX, focusY, mScaleType);
355 | }
356 |
357 | /**
358 | * Set zoom to the specified scale. Image will be centered around the point
359 | * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
360 | * as a fraction from the left and top of the view. For example, the top left
361 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
362 | * @param scale
363 | * @param focusX
364 | * @param focusY
365 | * @param scaleType
366 | */
367 | public void setZoom(float scale, float focusX, float focusY, ScaleType scaleType) {
368 | //
369 | // setZoom can be called before the image is on the screen, but at this point,
370 | // image and view sizes have not yet been calculated in onMeasure. Thus, we should
371 | // delay calling setZoom until the view has been measured.
372 | //
373 | if (!onDrawReady) {
374 | delayedZoomVariables = new ZoomVariables(scale, focusX, focusY, scaleType);
375 | return;
376 | }
377 |
378 | if (scaleType != mScaleType) {
379 | setScaleType(scaleType);
380 | }
381 | resetZoom();
382 | scaleImage(scale, viewWidth / 2, viewHeight / 2, true);
383 | matrix.getValues(m);
384 | m[Matrix.MTRANS_X] = -((focusX * getImageWidth()) - (viewWidth * 0.5f));
385 | m[Matrix.MTRANS_Y] = -((focusY * getImageHeight()) - (viewHeight * 0.5f));
386 | matrix.setValues(m);
387 | fixTrans();
388 | setImageMatrix(matrix);
389 | }
390 |
391 | /**
392 | * Set zoom parameters equal to another TouchImageView. Including scale, position,
393 | * and ScaleType.
394 | * @param TouchImageView
395 | */
396 | public void setZoom(TouchImageView img) {
397 | PointF center = img.getScrollPosition();
398 | setZoom(img.getCurrentZoom(), center.x, center.y, img.getScaleType());
399 | }
400 |
401 | /**
402 | * Return the point at the center of the zoomed image. The PointF coordinates range
403 | * in value between 0 and 1 and the focus point is denoted as a fraction from the left
404 | * and top of the view. For example, the top left corner of the image would be (0, 0).
405 | * And the bottom right corner would be (1, 1).
406 | * @return PointF representing the scroll position of the zoomed image.
407 | */
408 | public PointF getScrollPosition() {
409 | Drawable drawable = getDrawable();
410 | if (drawable == null) {
411 | return null;
412 | }
413 | int drawableWidth = drawable.getIntrinsicWidth();
414 | int drawableHeight = drawable.getIntrinsicHeight();
415 |
416 | PointF point = transformCoordTouchToBitmap(viewWidth / 2, viewHeight / 2, true);
417 | point.x /= drawableWidth;
418 | point.y /= drawableHeight;
419 | return point;
420 | }
421 |
422 | /**
423 | * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the
424 | * left and top of the view. The focus points can range in value between 0 and 1.
425 | * @param focusX
426 | * @param focusY
427 | */
428 | public void setScrollPosition(float focusX, float focusY) {
429 | setZoom(normalizedScale, focusX, focusY);
430 | }
431 |
432 | /**
433 | * Performs boundary checking and fixes the image matrix if it
434 | * is out of bounds.
435 | */
436 | private void fixTrans() {
437 | matrix.getValues(m);
438 | float transX = m[Matrix.MTRANS_X];
439 | float transY = m[Matrix.MTRANS_Y];
440 |
441 | float fixTransX = getFixTrans(transX, viewWidth, getImageWidth());
442 | float fixTransY = getFixTrans(transY, viewHeight, getImageHeight());
443 |
444 | if (fixTransX != 0 || fixTransY != 0) {
445 | matrix.postTranslate(fixTransX, fixTransY);
446 | }
447 | }
448 |
449 | /**
450 | * When transitioning from zooming from focus to zoom from center (or vice versa)
451 | * the image can become unaligned within the view. This is apparent when zooming
452 | * quickly. When the content size is less than the view size, the content will often
453 | * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
454 | * then makes sure the image is centered correctly within the view.
455 | */
456 | private void fixScaleTrans() {
457 | fixTrans();
458 | matrix.getValues(m);
459 | if (getImageWidth() < viewWidth) {
460 | m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2;
461 | }
462 |
463 | if (getImageHeight() < viewHeight) {
464 | m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2;
465 | }
466 | matrix.setValues(m);
467 | }
468 |
469 | private float getFixTrans(float trans, float viewSize, float contentSize) {
470 | float minTrans, maxTrans;
471 |
472 | if (contentSize <= viewSize) {
473 | minTrans = 0;
474 | maxTrans = viewSize - contentSize;
475 |
476 | } else {
477 | minTrans = viewSize - contentSize;
478 | maxTrans = 0;
479 | }
480 |
481 | if (trans < minTrans)
482 | return -trans + minTrans;
483 | if (trans > maxTrans)
484 | return -trans + maxTrans;
485 | return 0;
486 | }
487 |
488 | private float getFixDragTrans(float delta, float viewSize, float contentSize) {
489 | if (contentSize <= viewSize) {
490 | return 0;
491 | }
492 | return delta;
493 | }
494 |
495 | private float getImageWidth() {
496 | return matchViewWidth * normalizedScale;
497 | }
498 |
499 | private float getImageHeight() {
500 | return matchViewHeight * normalizedScale;
501 | }
502 |
503 | @Override
504 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
505 | Drawable drawable = getDrawable();
506 | if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
507 | setMeasuredDimension(0, 0);
508 | return;
509 | }
510 |
511 | int drawableWidth = drawable.getIntrinsicWidth();
512 | int drawableHeight = drawable.getIntrinsicHeight();
513 | int widthSize = MeasureSpec.getSize(widthMeasureSpec);
514 | int widthMode = MeasureSpec.getMode(widthMeasureSpec);
515 | int heightSize = MeasureSpec.getSize(heightMeasureSpec);
516 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
517 | viewWidth = setViewSize(widthMode, widthSize, drawableWidth);
518 | viewHeight = setViewSize(heightMode, heightSize, drawableHeight);
519 |
520 | //
521 | // Set view dimensions
522 | //
523 | setMeasuredDimension(viewWidth, viewHeight);
524 |
525 | //
526 | // Fit content within view
527 | //
528 | fitImageToView();
529 | }
530 |
531 | /**
532 | * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
533 | * it is made to fit the screen according to the dimensions of the previous image matrix. This
534 | * allows the image to maintain its zoom after rotation.
535 | */
536 | private void fitImageToView() {
537 | Drawable drawable = getDrawable();
538 | if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
539 | return;
540 | }
541 | if (matrix == null || prevMatrix == null) {
542 | return;
543 | }
544 |
545 | int drawableWidth = drawable.getIntrinsicWidth();
546 | int drawableHeight = drawable.getIntrinsicHeight();
547 |
548 | //
549 | // Scale image for view
550 | //
551 | float scaleX = (float) viewWidth / drawableWidth;
552 | float scaleY = (float) viewHeight / drawableHeight;
553 |
554 | switch (mScaleType) {
555 | case CENTER:
556 | scaleX = scaleY = 1;
557 | break;
558 |
559 | case CENTER_CROP:
560 | scaleX = scaleY = Math.max(scaleX, scaleY);
561 | break;
562 |
563 | case CENTER_INSIDE:
564 | scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY));
565 |
566 | case FIT_CENTER:
567 | scaleX = scaleY = Math.min(scaleX, scaleY);
568 | break;
569 |
570 | case FIT_XY:
571 | break;
572 |
573 | default:
574 | //
575 | // FIT_START and FIT_END not supported
576 | //
577 | throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
578 |
579 | }
580 |
581 | //
582 | // Center the image
583 | //
584 | float redundantXSpace = viewWidth - (scaleX * drawableWidth);
585 | float redundantYSpace = viewHeight - (scaleY * drawableHeight);
586 | matchViewWidth = viewWidth - redundantXSpace;
587 | matchViewHeight = viewHeight - redundantYSpace;
588 | if (!isZoomed() && !imageRenderedAtLeastOnce) {
589 | //
590 | // Stretch and center image to fit view
591 | //
592 | matrix.setScale(scaleX, scaleY);
593 | matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2);
594 | normalizedScale = 1;
595 |
596 | } else {
597 | //
598 | // These values should never be 0 or we will set viewWidth and viewHeight
599 | // to NaN in translateMatrixAfterRotate. To avoid this, call savePreviousImageValues
600 | // to set them equal to the current values.
601 | //
602 | if (prevMatchViewWidth == 0 || prevMatchViewHeight == 0) {
603 | savePreviousImageValues();
604 | }
605 |
606 | prevMatrix.getValues(m);
607 |
608 | //
609 | // Rescale Matrix after rotation
610 | //
611 | m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale;
612 | m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale;
613 |
614 | //
615 | // TransX and TransY from previous matrix
616 | //
617 | float transX = m[Matrix.MTRANS_X];
618 | float transY = m[Matrix.MTRANS_Y];
619 |
620 | //
621 | // Width
622 | //
623 | float prevActualWidth = prevMatchViewWidth * normalizedScale;
624 | float actualWidth = getImageWidth();
625 | translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth);
626 |
627 | //
628 | // Height
629 | //
630 | float prevActualHeight = prevMatchViewHeight * normalizedScale;
631 | float actualHeight = getImageHeight();
632 | translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight);
633 |
634 | //
635 | // Set the matrix to the adjusted scale and translate values.
636 | //
637 | matrix.setValues(m);
638 | }
639 | fixTrans();
640 | setImageMatrix(matrix);
641 | }
642 |
643 | /**
644 | * Set view dimensions based on layout params
645 | *
646 | * @param mode
647 | * @param size
648 | * @param drawableWidth
649 | * @return
650 | */
651 | private int setViewSize(int mode, int size, int drawableWidth) {
652 | int viewSize;
653 | switch (mode) {
654 | case MeasureSpec.EXACTLY:
655 | viewSize = size;
656 | break;
657 |
658 | case MeasureSpec.AT_MOST:
659 | viewSize = Math.min(drawableWidth, size);
660 | break;
661 |
662 | case MeasureSpec.UNSPECIFIED:
663 | viewSize = drawableWidth;
664 | break;
665 |
666 | default:
667 | viewSize = size;
668 | break;
669 | }
670 | return viewSize;
671 | }
672 |
673 | /**
674 | * After rotating, the matrix needs to be translated. This function finds the area of image
675 | * which was previously centered and adjusts translations so that is again the center, post-rotation.
676 | *
677 | * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y
678 | * @param trans the value of trans in that axis before the rotation
679 | * @param prevImageSize the width/height of the image before the rotation
680 | * @param imageSize width/height of the image after rotation
681 | * @param prevViewSize width/height of view before rotation
682 | * @param viewSize width/height of view after rotation
683 | * @param drawableSize width/height of drawable
684 | */
685 | private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) {
686 | if (imageSize < viewSize) {
687 | //
688 | // The width/height of image is less than the view's width/height. Center it.
689 | //
690 | m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f;
691 |
692 | } else if (trans > 0) {
693 | //
694 | // The image is larger than the view, but was not before rotation. Center it.
695 | //
696 | m[axis] = -((imageSize - viewSize) * 0.5f);
697 |
698 | } else {
699 | //
700 | // Find the area of the image which was previously centered in the view. Determine its distance
701 | // from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage
702 | // to calculate the trans in the new view width/height.
703 | //
704 | float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize;
705 | m[axis] = -((percentage * imageSize) - (viewSize * 0.5f));
706 | }
707 | }
708 |
709 | private void setState(State state) {
710 | this.state = state;
711 | }
712 |
713 | public boolean canScrollHorizontallyFroyo(int direction) {
714 | return canScrollHorizontally(direction);
715 | }
716 |
717 | @Override
718 | public boolean canScrollHorizontally(int direction) {
719 | matrix.getValues(m);
720 | float x = m[Matrix.MTRANS_X];
721 |
722 | if (getImageWidth() < viewWidth) {
723 | return false;
724 |
725 | } else if (x >= -1 && direction < 0) {
726 | return false;
727 |
728 | } else if (Math.abs(x) + viewWidth + 1 >= getImageWidth() && direction > 0) {
729 | return false;
730 | }
731 |
732 | return true;
733 | }
734 |
735 | /**
736 | * Gesture Listener detects a single click or long click and passes that on
737 | * to the view's listener.
738 | * @author Ortiz
739 | *
740 | */
741 | private class GestureListener extends GestureDetector.SimpleOnGestureListener {
742 |
743 | @Override
744 | public boolean onSingleTapConfirmed(MotionEvent e)
745 | {
746 | if(doubleTapListener != null) {
747 | return doubleTapListener.onSingleTapConfirmed(e);
748 | }
749 | return performClick();
750 | }
751 |
752 | @Override
753 | public void onLongPress(MotionEvent e)
754 | {
755 | performLongClick();
756 | }
757 |
758 | @Override
759 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
760 | {
761 | if (fling != null) {
762 | //
763 | // If a previous fling is still active, it should be cancelled so that two flings
764 | // are not run simultaenously.
765 | //
766 | fling.cancelFling();
767 | }
768 | fling = new Fling((int) velocityX, (int) velocityY);
769 | compatPostOnAnimation(fling);
770 | return super.onFling(e1, e2, velocityX, velocityY);
771 | }
772 |
773 | @Override
774 | public boolean onDoubleTap(MotionEvent e) {
775 | boolean consumed = false;
776 | if(doubleTapListener != null) {
777 | consumed = doubleTapListener.onDoubleTap(e);
778 | }
779 | if (state == State.NONE) {
780 | float targetZoom = (normalizedScale == minScale) ? maxScale/9 : minScale;
781 | DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false);
782 | compatPostOnAnimation(doubleTap);
783 | consumed = true;
784 | }
785 | return consumed;
786 | }
787 |
788 | @Override
789 | public boolean onDoubleTapEvent(MotionEvent e) {
790 | if(doubleTapListener != null) {
791 | return doubleTapListener.onDoubleTapEvent(e);
792 | }
793 | return false;
794 | }
795 | }
796 |
797 | public interface OnTouchImageViewListener {
798 | public void onMove();
799 | }
800 |
801 | /**
802 | * Responsible for all touch events. Handles the heavy lifting of drag and also sends
803 | * touch events to Scale Detector and Gesture Detector.
804 | * @author Ortiz
805 | *
806 | */
807 | private class PrivateOnTouchListener implements OnTouchListener {
808 |
809 | //
810 | // Remember last point position for dragging
811 | //
812 | private PointF last = new PointF();
813 |
814 | @Override
815 | public boolean onTouch(View v, MotionEvent event) {
816 | mScaleDetector.onTouchEvent(event);
817 | mGestureDetector.onTouchEvent(event);
818 | PointF curr = new PointF(event.getX(), event.getY());
819 |
820 | if (state == State.NONE || state == State.DRAG || state == State.FLING) {
821 | switch (event.getAction()) {
822 | case MotionEvent.ACTION_DOWN:
823 | last.set(curr);
824 | if (fling != null)
825 | fling.cancelFling();
826 | setState(State.DRAG);
827 | break;
828 |
829 | case MotionEvent.ACTION_MOVE:
830 | if (state == State.DRAG) {
831 | float deltaX = curr.x - last.x;
832 | float deltaY = curr.y - last.y;
833 | float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth());
834 | float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight());
835 | matrix.postTranslate(fixTransX, fixTransY);
836 | fixTrans();
837 | last.set(curr.x, curr.y);
838 | }
839 | break;
840 |
841 | case MotionEvent.ACTION_UP:
842 | case MotionEvent.ACTION_POINTER_UP:
843 | setState(State.NONE);
844 | break;
845 | }
846 | }
847 |
848 | setImageMatrix(matrix);
849 |
850 | //
851 | // User-defined OnTouchListener
852 | //
853 | if(userTouchListener != null) {
854 | userTouchListener.onTouch(v, event);
855 | }
856 |
857 | //
858 | // OnTouchImageViewListener is set: TouchImageView dragged by user.
859 | //
860 | if (touchImageViewListener != null) {
861 | touchImageViewListener.onMove();
862 | }
863 |
864 | //
865 | // indicate event was handled
866 | //
867 | return true;
868 | }
869 | }
870 |
871 | /**
872 | * ScaleListener detects user two finger scaling and scales image.
873 | * @author Ortiz
874 | *
875 | */
876 | private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
877 | @Override
878 | public boolean onScaleBegin(ScaleGestureDetector detector) {
879 | setState(State.ZOOM);
880 | return true;
881 | }
882 |
883 | @Override
884 | public boolean onScale(ScaleGestureDetector detector) {
885 | scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true);
886 |
887 | //
888 | // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
889 | //
890 | if (touchImageViewListener != null) {
891 | touchImageViewListener.onMove();
892 | }
893 | return true;
894 | }
895 |
896 | @Override
897 | public void onScaleEnd(ScaleGestureDetector detector) {
898 | super.onScaleEnd(detector);
899 | setState(State.NONE);
900 | boolean animateToZoomBoundary = false;
901 | float targetZoom = normalizedScale;
902 | if (normalizedScale > maxScale) {
903 | targetZoom = maxScale;
904 | animateToZoomBoundary = true;
905 |
906 | } else if (normalizedScale < minScale) {
907 | targetZoom = minScale;
908 | animateToZoomBoundary = true;
909 | }
910 |
911 | if (animateToZoomBoundary) {
912 | DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true);
913 | compatPostOnAnimation(doubleTap);
914 | }
915 | }
916 | }
917 |
918 | private void scaleImage(double deltaScale, float focusX, float focusY, boolean stretchImageToSuper) {
919 |
920 | float lowerScale, upperScale;
921 | if (stretchImageToSuper) {
922 | lowerScale = superMinScale;
923 | upperScale = superMaxScale;
924 |
925 | } else {
926 | lowerScale = minScale;
927 | upperScale = maxScale;
928 | }
929 |
930 | float origScale = normalizedScale;
931 | normalizedScale *= deltaScale;
932 | if (normalizedScale > upperScale) {
933 | normalizedScale = upperScale;
934 | deltaScale = upperScale / origScale;
935 | } else if (normalizedScale < lowerScale) {
936 | normalizedScale = lowerScale;
937 | deltaScale = lowerScale / origScale;
938 | }
939 |
940 | matrix.postScale((float) deltaScale, (float) deltaScale, focusX, focusY);
941 | fixScaleTrans();
942 | }
943 |
944 | /**
945 | * DoubleTapZoom calls a series of runnables which apply
946 | * an animated zoom in/out graphic to the image.
947 | * @author Ortiz
948 | *
949 | */
950 | private class DoubleTapZoom implements Runnable {
951 |
952 | private long startTime;
953 | private static final float ZOOM_TIME = 500;
954 | private float startZoom, targetZoom;
955 | private float bitmapX, bitmapY;
956 | private boolean stretchImageToSuper;
957 | private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
958 | private PointF startTouch;
959 | private PointF endTouch;
960 |
961 | DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) {
962 | setState(State.ANIMATE_ZOOM);
963 | startTime = System.currentTimeMillis();
964 | this.startZoom = normalizedScale;
965 | this.targetZoom = targetZoom;
966 | this.stretchImageToSuper = stretchImageToSuper;
967 | PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false);
968 | this.bitmapX = bitmapPoint.x;
969 | this.bitmapY = bitmapPoint.y;
970 |
971 | //
972 | // Used for translating image during scaling
973 | //
974 | startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY);
975 | endTouch = new PointF(viewWidth / 2, viewHeight / 2);
976 | }
977 |
978 | @Override
979 | public void run() {
980 | float t = interpolate();
981 | double deltaScale = calculateDeltaScale(t);
982 | scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper);
983 | translateImageToCenterTouchPosition(t);
984 | fixScaleTrans();
985 | setImageMatrix(matrix);
986 |
987 | //
988 | // OnTouchImageViewListener is set: double tap runnable updates listener
989 | // with every frame.
990 | //
991 | if (touchImageViewListener != null) {
992 | touchImageViewListener.onMove();
993 | }
994 |
995 | if (t < 1f) {
996 | //
997 | // We haven't finished zooming
998 | //
999 | compatPostOnAnimation(this);
1000 |
1001 | } else {
1002 | //
1003 | // Finished zooming
1004 | //
1005 | setState(State.NONE);
1006 | }
1007 | }
1008 |
1009 | /**
1010 | * Interpolate between where the image should start and end in order to translate
1011 | * the image so that the point that is touched is what ends up centered at the end
1012 | * of the zoom.
1013 | * @param t
1014 | */
1015 | private void translateImageToCenterTouchPosition(float t) {
1016 | float targetX = startTouch.x + t * (endTouch.x - startTouch.x);
1017 | float targetY = startTouch.y + t * (endTouch.y - startTouch.y);
1018 | PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY);
1019 | matrix.postTranslate(targetX - curr.x, targetY - curr.y);
1020 | }
1021 |
1022 | /**
1023 | * Use interpolator to get t
1024 | * @return
1025 | */
1026 | private float interpolate() {
1027 | long currTime = System.currentTimeMillis();
1028 | float elapsed = (currTime - startTime) / ZOOM_TIME;
1029 | elapsed = Math.min(1f, elapsed);
1030 | return interpolator.getInterpolation(elapsed);
1031 | }
1032 |
1033 | /**
1034 | * Interpolate the current targeted zoom and get the delta
1035 | * from the current zoom.
1036 | * @param t
1037 | * @return
1038 | */
1039 | private double calculateDeltaScale(float t) {
1040 | double zoom = startZoom + t * (targetZoom - startZoom);
1041 | return zoom / normalizedScale;
1042 | }
1043 | }
1044 |
1045 | /**
1046 | * This function will transform the coordinates in the touch event to the coordinate
1047 | * system of the drawable that the imageview contain
1048 | * @param x x-coordinate of touch event
1049 | * @param y y-coordinate of touch event
1050 | * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
1051 | * to the bounds of the bitmap size.
1052 | * @return Coordinates of the point touched, in the coordinate system of the original drawable.
1053 | */
1054 | private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) {
1055 | if(getDrawable() == null) return new PointF(0,0);
1056 | matrix.getValues(m);
1057 | float origW = getDrawable().getIntrinsicWidth();
1058 | float origH = getDrawable().getIntrinsicHeight();
1059 | float transX = m[Matrix.MTRANS_X];
1060 | float transY = m[Matrix.MTRANS_Y];
1061 | float finalX = ((x - transX) * origW) / getImageWidth();
1062 | float finalY = ((y - transY) * origH) / getImageHeight();
1063 |
1064 | if (clipToBitmap) {
1065 | finalX = Math.min(Math.max(finalX, 0), origW);
1066 | finalY = Math.min(Math.max(finalY, 0), origH);
1067 | }
1068 |
1069 | return new PointF(finalX , finalY);
1070 | }
1071 |
1072 | /**
1073 | * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
1074 | * drawable's coordinate system to the view's coordinate system.
1075 | * @param bx x-coordinate in original bitmap coordinate system
1076 | * @param by y-coordinate in original bitmap coordinate system
1077 | * @return Coordinates of the point in the view's coordinate system.
1078 | */
1079 | private PointF transformCoordBitmapToTouch(float bx, float by) {
1080 | if(getDrawable() == null) return new PointF(0,0);
1081 | matrix.getValues(m);
1082 | float origW = getDrawable().getIntrinsicWidth();
1083 | float origH = getDrawable().getIntrinsicHeight();
1084 | float px = bx / origW;
1085 | float py = by / origH;
1086 | float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px;
1087 | float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py;
1088 | return new PointF(finalX , finalY);
1089 | }
1090 |
1091 | /**
1092 | * Fling launches sequential runnables which apply
1093 | * the fling graphic to the image. The values for the translation
1094 | * are interpolated by the Scroller.
1095 | * @author Ortiz
1096 | *
1097 | */
1098 | private class Fling implements Runnable {
1099 |
1100 | CompatScroller scroller;
1101 | int currX, currY;
1102 |
1103 | Fling(int velocityX, int velocityY) {
1104 | setState(State.FLING);
1105 | scroller = new CompatScroller(context);
1106 | matrix.getValues(m);
1107 |
1108 | int startX = (int) m[Matrix.MTRANS_X];
1109 | int startY = (int) m[Matrix.MTRANS_Y];
1110 | int minX, maxX, minY, maxY;
1111 |
1112 | if (getImageWidth() > viewWidth) {
1113 | minX = viewWidth - (int) getImageWidth();
1114 | maxX = 0;
1115 |
1116 | } else {
1117 | minX = maxX = startX;
1118 | }
1119 |
1120 | if (getImageHeight() > viewHeight) {
1121 | minY = viewHeight - (int) getImageHeight();
1122 | maxY = 0;
1123 |
1124 | } else {
1125 | minY = maxY = startY;
1126 | }
1127 |
1128 | scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX,
1129 | maxX, minY, maxY);
1130 | currX = startX;
1131 | currY = startY;
1132 | }
1133 |
1134 | public void cancelFling() {
1135 | if (scroller != null) {
1136 | setState(State.NONE);
1137 | scroller.forceFinished(true);
1138 | }
1139 | }
1140 |
1141 | @Override
1142 | public void run() {
1143 |
1144 | //
1145 | // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
1146 | // Listener runnable updated with each frame of fling animation.
1147 | //
1148 | if (touchImageViewListener != null) {
1149 | touchImageViewListener.onMove();
1150 | }
1151 |
1152 | if (scroller.isFinished()) {
1153 | scroller = null;
1154 | return;
1155 | }
1156 |
1157 | if (scroller.computeScrollOffset()) {
1158 | int newX = scroller.getCurrX();
1159 | int newY = scroller.getCurrY();
1160 | int transX = newX - currX;
1161 | int transY = newY - currY;
1162 | currX = newX;
1163 | currY = newY;
1164 | matrix.postTranslate(transX, transY);
1165 | fixTrans();
1166 | setImageMatrix(matrix);
1167 | compatPostOnAnimation(this);
1168 | }
1169 | }
1170 | }
1171 |
1172 | @TargetApi(Build.VERSION_CODES.GINGERBREAD)
1173 | private class CompatScroller {
1174 | Scroller scroller;
1175 | OverScroller overScroller;
1176 | boolean isPreGingerbread;
1177 |
1178 | public CompatScroller(Context context) {
1179 | if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
1180 | isPreGingerbread = true;
1181 | scroller = new Scroller(context);
1182 |
1183 | } else {
1184 | isPreGingerbread = false;
1185 | overScroller = new OverScroller(context);
1186 | }
1187 | }
1188 |
1189 | public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
1190 | if (isPreGingerbread) {
1191 | scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
1192 | } else {
1193 | overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
1194 | }
1195 | }
1196 |
1197 | public void forceFinished(boolean finished) {
1198 | if (isPreGingerbread) {
1199 | scroller.forceFinished(finished);
1200 | } else {
1201 | overScroller.forceFinished(finished);
1202 | }
1203 | }
1204 |
1205 | public boolean isFinished() {
1206 | if (isPreGingerbread) {
1207 | return scroller.isFinished();
1208 | } else {
1209 | return overScroller.isFinished();
1210 | }
1211 | }
1212 |
1213 | public boolean computeScrollOffset() {
1214 | if (isPreGingerbread) {
1215 | return scroller.computeScrollOffset();
1216 | } else {
1217 | overScroller.computeScrollOffset();
1218 | return overScroller.computeScrollOffset();
1219 | }
1220 | }
1221 |
1222 | public int getCurrX() {
1223 | if (isPreGingerbread) {
1224 | return scroller.getCurrX();
1225 | } else {
1226 | return overScroller.getCurrX();
1227 | }
1228 | }
1229 |
1230 | public int getCurrY() {
1231 | if (isPreGingerbread) {
1232 | return scroller.getCurrY();
1233 | } else {
1234 | return overScroller.getCurrY();
1235 | }
1236 | }
1237 | }
1238 |
1239 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
1240 | private void compatPostOnAnimation(Runnable runnable) {
1241 | if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
1242 | postOnAnimation(runnable);
1243 |
1244 | } else {
1245 | postDelayed(runnable, 1000/60);
1246 | }
1247 | }
1248 |
1249 | private class ZoomVariables {
1250 | public float scale;
1251 | public float focusX;
1252 | public float focusY;
1253 | public ScaleType scaleType;
1254 |
1255 | public ZoomVariables(float scale, float focusX, float focusY, ScaleType scaleType) {
1256 | this.scale = scale;
1257 | this.focusX = focusX;
1258 | this.focusY = focusY;
1259 | this.scaleType = scaleType;
1260 | }
1261 | }
1262 |
1263 | private void printMatrixInfo() {
1264 | float[] n = new float[9];
1265 | matrix.getValues(n);
1266 | Log.d(DEBUG, "Scale: " + n[Matrix.MSCALE_X] + " TransX: " + n[Matrix.MTRANS_X] + " TransY: " + n[Matrix.MTRANS_Y]);
1267 | }
1268 | }
1269 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gtp/showapicturetoyourfriend/receiverpictureactivity.java:
--------------------------------------------------------------------------------
1 | package com.gtp.showapicturetoyourfriend;
2 |
3 | import android.Manifest;
4 | import android.app.KeyguardManager;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.content.pm.PackageManager;
8 | import android.net.Uri;
9 | import android.os.Build;
10 | import android.os.Bundle;
11 | import android.os.Handler;
12 | import android.os.PowerManager;
13 | import android.support.v4.app.ActivityCompat;
14 | import android.support.v4.app.Fragment;
15 | import android.support.v4.app.FragmentManager;
16 | import android.support.v4.app.FragmentStatePagerAdapter;
17 | import android.support.v4.view.PagerAdapter;
18 | import android.support.v4.view.ViewPager;
19 | import android.support.v7.app.AppCompatActivity;
20 | import android.util.Log;
21 | import android.view.LayoutInflater;
22 | import android.view.View;
23 | import android.view.ViewGroup;
24 | import android.view.Window;
25 | import android.view.WindowManager;
26 | import android.webkit.MimeTypeMap;
27 | import android.widget.MediaController;
28 | import android.widget.Toast;
29 | import android.widget.VideoView;
30 |
31 | import com.bumptech.glide.Glide;
32 | import com.bumptech.glide.load.resource.drawable.GlideDrawable;
33 | import com.bumptech.glide.request.animation.GlideAnimation;
34 | import com.bumptech.glide.request.target.GlideDrawableImageViewTarget;
35 | import com.kobakei.ratethisapp.RateThisApp;
36 |
37 | import java.util.ArrayList;
38 |
39 | public class receiverpictureactivity extends AppCompatActivity {
40 |
41 | //to make sure back button doesn't open old images
42 | @Override
43 | protected void onNewIntent(Intent intent) {
44 | finish();
45 | startActivity(intent);
46 | }
47 |
48 | Handler handly;
49 | Runnable goahead;
50 | int page = 0;
51 | ViewPager mViewPager;
52 |
53 | @Override
54 | protected void onCreate(Bundle savedInstanceState) {
55 | super.onCreate(savedInstanceState);
56 | //makes Window Fullscreen and show ontop of the Lockscreen
57 | getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
58 | getSupportActionBar().hide();
59 | setContentView(R.layout.activity_receiverpictureactivity);
60 | Window wind = this.getWindow();
61 | wind.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
62 | wind.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
63 |
64 | if (savedInstanceState != null) {
65 | if (savedInstanceState.getBoolean("ready", false)) {
66 | page = savedInstanceState.getInt("pageItem", 0);
67 | screenislocked();
68 | }
69 | }
70 |
71 | RateThisApp.onCreate(this);
72 | RateThisApp.showRateDialogIfNeeded(this);
73 |
74 | //periodically checks if the screen is locked, if it is calls screenislocked()
75 | handly = new Handler();
76 | goahead = new Runnable() {
77 | @Override
78 | public void run() {
79 | KeyguardManager myKM = (KeyguardManager) getApplication().getSystemService(Context.KEYGUARD_SERVICE);
80 | if (myKM != null) {
81 | if( myKM.inKeyguardRestrictedInputMode()) {
82 | screenislocked();
83 | } else {
84 | handly.postDelayed(goahead, 40);
85 | }
86 | } else {
87 | handly.postDelayed(goahead, 40);
88 | }
89 | }
90 | };
91 | goahead.run();
92 |
93 | }
94 |
95 | public void buttonpressed(View view) { //called when button is pressed
96 | screenislocked();
97 | }
98 |
99 | @Override
100 | protected void onSaveInstanceState(Bundle outState) {
101 | super.onSaveInstanceState(outState);
102 | if (mViewPager != null) {
103 | outState.putInt("pageItem", mViewPager.getCurrentItem());
104 | outState.putBoolean("ready", true);
105 | } else {
106 | outState.putBoolean("ready", false);
107 | }
108 | }
109 |
110 | public void screenislocked() {
111 |
112 | if (handly != null) {
113 | handly.removeCallbacks(goahead);
114 | }
115 |
116 | PowerManager.WakeLock screenLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(
117 | PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "TAG");
118 | screenLock.acquire(1);
119 |
120 | screenLock.release();
121 | //removes handler, wakes up screen and realeases Wakelock immediately
122 |
123 | Intent intent = getIntent();
124 | String action = intent.getAction();
125 | String type = intent.getType();
126 |
127 | setContentView(R.layout.activity_receivemultiple);
128 |
129 | ArrayList imageUris = null;
130 |
131 | if(Intent.ACTION_SEND.equals(action)) { //puts Uris into an array, whether there is one or multiple
132 | Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
133 | imageUris = new ArrayList<>();
134 | imageUris.add(imageUri);
135 | } else if(Intent.ACTION_SEND_MULTIPLE.equals(action)) {
136 | imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
137 | } //puts Uris into an array, whether there is one or multiple
138 |
139 | DemoCollectionPagerAdapter.setCounts(imageUris.size());
140 | DemoCollectionPagerAdapter.setUris(imageUris, this);
141 |
142 | PagerAdapter mDemoCollectionPagerAdapter = new DemoCollectionPagerAdapter(getSupportFragmentManager());
143 | DemoCollectionPagerAdapter.setAdapter(mDemoCollectionPagerAdapter);
144 | mViewPager = findViewById(R.id.pager);
145 | mViewPager.setOffscreenPageLimit(2);
146 | mViewPager.setAdapter(mDemoCollectionPagerAdapter);
147 |
148 | mViewPager.setCurrentItem(page);
149 | }
150 |
151 | @Override
152 | protected void onDestroy() {
153 | handly.removeCallbacks(goahead);
154 | super.onDestroy();
155 | }
156 |
157 | public static class DemoCollectionPagerAdapter extends FragmentStatePagerAdapter {
158 | public DemoCollectionPagerAdapter(FragmentManager fm) {
159 | super(fm);
160 | }
161 |
162 | static ArrayList uris;
163 | static Context context;
164 |
165 | public static void recreateafterpermission() { //this is called when the user gives permission to view file
166 | atp.notifyDataSetChanged();
167 | }
168 |
169 | public static void setUris(ArrayList muri, Context c) {
170 | uris=muri;
171 | context = c;
172 | }
173 |
174 | public static void setAdapter(PagerAdapter adapter) {
175 | atp = adapter;
176 | }
177 | static PagerAdapter atp;
178 |
179 | @Override
180 | public Fragment getItem(int i) {
181 | Fragment fragment = new DemoObjectFragment();
182 | Bundle args = new Bundle();
183 |
184 | Uri uri = uris.get(i);
185 | String stringuri = "";
186 | if(uri != null) {
187 | stringuri = uri.toString();
188 | } else {
189 | Toast.makeText(context, R.string.invalid, Toast.LENGTH_LONG).show();
190 | }
191 |
192 | args.putString("Uri",stringuri);
193 | fragment.setArguments(args);
194 | return fragment;
195 | }
196 |
197 | static int count;
198 |
199 | @Override
200 | public int getCount() {
201 | return count;
202 | }
203 |
204 | public static void setCounts(int mcount) {
205 | count = mcount;
206 | }
207 |
208 | @Override
209 | public CharSequence getPageTitle(int position) {
210 | return "OBJECT " + (position + 1);
211 | }
212 | }
213 |
214 | public static class DemoObjectFragment extends Fragment {
215 |
216 | @Override
217 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
218 |
219 | View rootView = null;
220 |
221 | Bundle args = getArguments();
222 | String forUri = args.getString("Uri");
223 | Uri urinormal = Uri.parse(forUri);
224 |
225 | String type = null;
226 | String extension = MimeTypeMap.getFileExtensionFromUrl(forUri.replace("~",""));
227 | if (extension != null) {
228 | type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
229 | }
230 |
231 | String startwith = getActivity().getContentResolver().getType(urinormal);
232 |
233 | if(startwith!=null) {
234 | if (startwith.startsWith("image/")) {
235 | rootView = inflater.inflate(R.layout.adapterimage, container, false);
236 | pictureSet((TouchImageView) rootView.findViewById(R.id.touchImageView), urinormal);
237 | } else if (startwith.startsWith("video/")) {
238 | rootView = inflater.inflate(R.layout.adaptervideo, container, false);
239 | videoSet((VideoView) rootView.findViewById(R.id.videoview), urinormal);
240 | }
241 | } else {
242 | if(type!=null) {
243 | if (Build.VERSION.SDK_INT >= 23) {
244 | if (getActivity().checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
245 | rootView=typeMethod(rootView,urinormal,container,type,inflater);
246 | } else {
247 | ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
248 | Toast.makeText(getActivity(), R.string.permission, Toast.LENGTH_LONG).show();
249 | }
250 | } else {
251 | rootView=typeMethod(rootView,urinormal,container,type,inflater);
252 | }
253 | }
254 | }
255 |
256 | if(viewvisibleinoncreate) {
257 | viewnowvisible(true);
258 | }
259 |
260 | return rootView;
261 |
262 | }
263 |
264 | private View typeMethod(View rootView, Uri urinormal, ViewGroup container, String type,LayoutInflater inflater) {
265 | if (type.startsWith("image/")) {
266 | rootView = inflater.inflate(R.layout.adapterimage, container, false);
267 | pictureSetFile((TouchImageView) rootView.findViewById(R.id.touchImageView), urinormal);
268 | } else if (type.startsWith("video/")) {
269 | rootView = inflater.inflate(R.layout.adaptervideo, container, false);
270 | videoSet((VideoView) rootView.findViewById(R.id.videoview), urinormal);
271 | }
272 | return rootView;
273 | }
274 |
275 | private void pictureSet(final TouchImageView imageset, Uri urinormal) {
276 |
277 | imageset.setMaxZoom(30);
278 |
279 | Glide.with(this)
280 | .load(urinormal)
281 | .override(2000, 2000)
282 | //.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
283 | .into(new GlideDrawableImageViewTarget(imageset) {
284 | @Override
285 | public void onResourceReady(GlideDrawable resource, GlideAnimation super GlideDrawable> animation) {
286 | super.onResourceReady(resource, animation);
287 | imageset.setZoom(1);
288 | }
289 | })
290 | ;
291 | }
292 |
293 | private void pictureSetFile(final TouchImageView imageset, Uri urinormal) {
294 | imageset.setMaxZoom(30);
295 |
296 | Glide.with(this)
297 | .load(urinormal)
298 | .override(2000, 2000)
299 | //.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
300 | .into(new GlideDrawableImageViewTarget(imageset) {
301 | @Override
302 | public void onResourceReady(GlideDrawable resource, GlideAnimation super GlideDrawable> animation) {
303 | super.onResourceReady(resource, animation);
304 | imageset.setZoom(1);
305 | }
306 | })
307 | ;
308 | }
309 |
310 | private void videoSet(VideoView video, Uri urinormal) {
311 | video.setVideoURI(urinormal);
312 | video.seekTo(1);
313 | controller = new MediaController(getActivity());
314 | videow = video;
315 | isvideo = true;
316 | }
317 |
318 | VideoView videow;
319 | MediaController controller;
320 | Boolean viewvisibleinoncreate = false;
321 | Boolean isvideo = false;
322 | Boolean iscontrollershowing = false;
323 |
324 | @Override
325 | public void setUserVisibleHint(boolean isVisibleToUser) {
326 | super.setUserVisibleHint(isVisibleToUser);
327 | if (getView() != null) {
328 | viewnowvisible(isVisibleToUser);
329 | } else {
330 | viewvisibleinoncreate = isVisibleToUser;
331 | }
332 | }
333 |
334 | public void viewnowvisible(boolean isVisibleToUser) {
335 | if (isvideo) {
336 | if(isVisibleToUser) {
337 | Log.d("r","VIDEO ON");
338 | if(iscontrollershowing) {
339 | controller.show();
340 | } else {
341 | controller.setAnchorView(videow);
342 | controller.setMediaPlayer(videow);
343 | videow.setMediaController(controller);
344 | iscontrollershowing = true;
345 | }
346 | videow.start();
347 | } else {
348 | Log.d("r","VIDEO OFF");
349 | videow.pause();
350 | controller.hide();
351 | }
352 | }
353 | }
354 |
355 | }
356 | }
357 |
358 |
359 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gregordr/Secure-Photo-Viewer/ed2e6238753bba4aa15d4eb735cb1dfe573fa2a6/app/src/main/res/drawable/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_receivemultiple.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_receiverpictureactivity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
25 |
26 |
40 |
41 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/adapterimage.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/adaptervideo.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Sichere Fotoanzeige
3 |
4 | Sperre dein Handy
5 | oder pinne diese App an und drücke hier:
6 | Anzeigen
7 |
8 | Um dieses Foto anzuzeigen, wird die Berechtigung für den Speicherzugriff benötigt
9 | Eine Datei ist beschädigt und kann nicht angezeigt werden
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Visualizador Seguro de Fotos
4 | Bloqueie seu aparelho
5 | ou fixe esse App e aperte aqui:
6 | Ver
7 | Por favor, ative o armazenamento para ver essa foto
8 | Arquivo inválido e não pode ser exibido.
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #1a237e
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #26C6DA
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Secure photo viewer
3 |
4 | Lock your device
5 | or pin this app and tap here:
6 | View
7 |
8 | Please enable storage permission for this photo
9 | A file is invalid and can not be displayed.
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | google()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.2.1'
10 |
11 | // NOTE: Do not place your application dependencies here; they belong
12 | // in the individual module build.gradle files
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | jcenter()
19 | }
20 | }
21 |
22 |
23 | task clean(type: Delete) {
24 | delete rootProject.buildDir
25 | }
26 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Oct 07 15:31:29 CEST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
7 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------