18 | * Inspired by {@link RecyclerView.Adapter}.
19 | */
20 | public abstract class RecyclePagerAdapter
21 | extends PagerAdapter {
22 |
23 | private final Queue cache = new LinkedList<>();
24 | private final SparseArray attached = new SparseArray<>();
25 |
26 | public abstract VH onCreateViewHolder(@NonNull ViewGroup container);
27 |
28 | public abstract void onBindViewHolder(@NonNull VH holder, int position);
29 |
30 | public void onRecycleViewHolder(@NonNull VH holder) {
31 | }
32 |
33 | /**
34 | * Returns ViewHolder for given position if it exists within ViewPager, or null otherwise.
35 | *
36 | * @param position Item position
37 | * @return View holder for given position
38 | */
39 | @Nullable
40 | public VH getViewHolder(int position) {
41 | return attached.get(position);
42 | }
43 |
44 | @NonNull
45 | @Override
46 | public Object instantiateItem(@NonNull ViewGroup container, int position) {
47 | VH holder = cache.poll();
48 | if (holder == null) {
49 | holder = onCreateViewHolder(container);
50 | }
51 | attached.put(position, holder);
52 |
53 | // We should not use previous layout params, since ViewPager stores
54 | // important information there which cannot be reused
55 | container.addView(holder.itemView, null);
56 |
57 | onBindViewHolder(holder, position);
58 | return holder;
59 | }
60 |
61 | @SuppressWarnings("unchecked")
62 | @Override
63 | public void destroyItem(ViewGroup container, int position, @NonNull Object object) {
64 | VH holder = (VH) object;
65 | attached.remove(position);
66 | container.removeView(holder.itemView);
67 | cache.offer(holder);
68 | onRecycleViewHolder(holder);
69 | }
70 |
71 | @Override
72 | public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
73 | ViewHolder holder = (ViewHolder) object;
74 | return holder.itemView == view;
75 | }
76 |
77 | @Override
78 | public int getItemPosition(@NonNull Object object) {
79 | // Forcing all views reinitialization when data set changed.
80 | // It should be safe because we're using views recycling logic.
81 | return POSITION_NONE;
82 | }
83 |
84 | public static class ViewHolder {
85 | public final View itemView;
86 |
87 | public ViewHolder(@NonNull View itemView) {
88 | this.itemView = itemView;
89 | }
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/commons/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This package contains common classes which are not directly related to library but still can be
3 | * useful.
4 | *
5 | * Keep in mind, that classes in this package can have breaking changes every time minor
6 | * part of library's version is changed (i.e. from 2.0.3 to 2.1.0).
7 | */
8 | package com.alexvasilkov.gestures.commons;
9 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/internal/AnimationEngine.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.internal;
2 |
3 | import android.os.Build;
4 | import android.view.View;
5 |
6 | import androidx.annotation.NonNull;
7 |
8 | public abstract class AnimationEngine implements Runnable {
9 |
10 | private static final long FRAME_TIME = 10L;
11 |
12 | private final View view;
13 | private final Fps fps;
14 |
15 | public AnimationEngine(@NonNull View view) {
16 | this.view = view;
17 | this.fps = GestureDebug.isDebugFps() ? new Fps() : null;
18 | }
19 |
20 | @Override
21 | public final void run() {
22 | boolean continueAnimation = onStep();
23 |
24 | if (fps != null) {
25 | fps.step();
26 | if (!continueAnimation) {
27 | fps.stop();
28 | }
29 | }
30 |
31 | if (continueAnimation) {
32 | scheduleNextStep();
33 | }
34 | }
35 |
36 | public abstract boolean onStep();
37 |
38 | private void scheduleNextStep() {
39 | view.removeCallbacks(this);
40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
41 | view.postOnAnimationDelayed(this, FRAME_TIME);
42 | } else {
43 | view.postDelayed(this, FRAME_TIME);
44 | }
45 | }
46 |
47 | public void start() {
48 | if (fps != null) {
49 | fps.start();
50 | }
51 |
52 | scheduleNextStep();
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/internal/Fps.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.internal;
2 |
3 | import android.os.SystemClock;
4 | import android.util.Log;
5 |
6 | class Fps {
7 |
8 | private static final String TAG = "GestureFps";
9 |
10 | private static final long WARNING_TIME = 20L; // Dropping less than 60 fps in average
11 | private static final long ERROR_TIME = 40L; // Dropping less than 30 fps in average
12 |
13 | private long frameStart;
14 | private long animationStart;
15 | private int framesCount;
16 |
17 | void start() {
18 | if (GestureDebug.isDebugFps()) {
19 | animationStart = frameStart = SystemClock.uptimeMillis();
20 | framesCount = 0;
21 | }
22 | }
23 |
24 | void stop() {
25 | if (GestureDebug.isDebugFps() && framesCount > 0) {
26 | int time = (int) (SystemClock.uptimeMillis() - animationStart);
27 | Log.d(TAG, "Average FPS: " + Math.round(1000f * framesCount / time));
28 | }
29 | }
30 |
31 | void step() {
32 | if (GestureDebug.isDebugFps()) {
33 | long frameTime = SystemClock.uptimeMillis() - frameStart;
34 | if (frameTime > ERROR_TIME) {
35 | Log.e(TAG, "Frame time: " + frameTime);
36 | } else if (frameTime > WARNING_TIME) {
37 | Log.w(TAG, "Frame time: " + frameTime);
38 | }
39 |
40 | framesCount++;
41 | frameStart = SystemClock.uptimeMillis();
42 | }
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/internal/GestureDebug.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.internal;
2 |
3 | public class GestureDebug {
4 |
5 | private static boolean debugFps;
6 | private static boolean debugAnimator;
7 | private static boolean drawDebugOverlay;
8 |
9 | private GestureDebug() {}
10 |
11 | @SuppressWarnings("WeakerAccess") // Public API (kinda)
12 | public static boolean isDebugFps() {
13 | return debugFps;
14 | }
15 |
16 | public static void setDebugFps(boolean debug) {
17 | debugFps = debug;
18 | }
19 |
20 | public static boolean isDebugAnimator() {
21 | return debugAnimator;
22 | }
23 |
24 | public static void setDebugAnimator(boolean debug) {
25 | debugAnimator = debug;
26 | }
27 |
28 | public static boolean isDrawDebugOverlay() {
29 | return drawDebugOverlay;
30 | }
31 |
32 | public static void setDrawDebugOverlay(boolean draw) {
33 | drawDebugOverlay = draw;
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/internal/UnitsUtils.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.internal;
2 |
3 | import android.content.Context;
4 | import android.util.TypedValue;
5 |
6 | public class UnitsUtils {
7 |
8 | private UnitsUtils() {}
9 |
10 | public static float toPixels(Context context, float value) {
11 | return toPixels(context, TypedValue.COMPLEX_UNIT_DIP, value);
12 | }
13 |
14 | public static float toPixels(Context context, int type, float value) {
15 | return TypedValue.applyDimension(type, value,
16 | context.getResources().getDisplayMetrics());
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/internal/detectors/ScaleGestureDetectorFixed.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.internal.detectors;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.Context;
5 | import android.os.Build;
6 | import android.view.MotionEvent;
7 | import android.view.ScaleGestureDetector;
8 |
9 | import androidx.annotation.NonNull;
10 |
11 | /**
12 | * 'Double tap and swipe' mode works bad for fast gestures. This class tries to fix this issue.
13 | */
14 | public class ScaleGestureDetectorFixed extends ScaleGestureDetector {
15 |
16 | private float currY;
17 | private float prevY;
18 |
19 | public ScaleGestureDetectorFixed(Context context, OnScaleGestureListener listener) {
20 | super(context, listener);
21 | warmUpScaleDetector();
22 | }
23 |
24 | /**
25 | * Scale detector is a little buggy when first time scale is occurred.
26 | * So we will feed it with fake motion event to warm it up.
27 | */
28 | private void warmUpScaleDetector() {
29 | long time = System.currentTimeMillis();
30 | MotionEvent event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0f, 0f, 0);
31 | onTouchEvent(event);
32 | event.recycle();
33 | }
34 |
35 | @Override
36 | public boolean onTouchEvent(@NonNull MotionEvent event) {
37 | final boolean result = super.onTouchEvent(event);
38 |
39 | prevY = currY;
40 | currY = event.getY();
41 |
42 | if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
43 | prevY = event.getY();
44 | }
45 |
46 | return result;
47 | }
48 |
49 | @TargetApi(Build.VERSION_CODES.KITKAT)
50 | private boolean isInDoubleTapMode() {
51 | // Indirectly determine double tap mode
52 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
53 | && isQuickScaleEnabled() && getCurrentSpan() == getCurrentSpanY();
54 | }
55 |
56 | @Override
57 | public float getScaleFactor() {
58 | float factor = super.getScaleFactor();
59 |
60 | if (isInDoubleTapMode()) {
61 | // We will filter buggy factors which may appear when crossing focus point.
62 | // We will also filter factors which are too far from 1, to make scaling smoother.
63 | return (currY > prevY && factor > 1f) || (currY < prevY && factor < 1f)
64 | ? Math.max(0.8f, Math.min(factor, 1.25f)) : 1f;
65 | } else {
66 | return factor;
67 | }
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/internal/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Internal classes.
3 | */
4 | package com.alexvasilkov.gestures.internal;
5 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/transition/internal/FromListViewListener.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.transition.internal;
2 |
3 | import android.view.View;
4 | import android.widget.AbsListView;
5 | import android.widget.ListView;
6 |
7 | import com.alexvasilkov.gestures.transition.tracker.FromTracker;
8 |
9 | public class FromListViewListener extends FromBaseListener {
10 |
11 | public FromListViewListener(ListView list, final FromTracker tracker, boolean autoScroll) {
12 | super(list, tracker, autoScroll);
13 |
14 | if (!autoScroll) {
15 | // No need to track list view scrolling if auto scroll is disabled
16 | return;
17 | }
18 |
19 | // Tracking list view scrolling to pick up newly visible views
20 | list.setOnScrollListener(new AbsListView.OnScrollListener() {
21 | @Override
22 | public void onScroll(AbsListView view, int firstVisible, int visibleCount, int total) {
23 | final ID id = getAnimator() == null ? null : getAnimator().getRequestedId();
24 |
25 | // If view was requested and list is scrolled we should try to find the view again
26 | if (id != null) {
27 | int position = tracker.getPositionById(id);
28 | if (position >= firstVisible && position < firstVisible + visibleCount) {
29 | View from = tracker.getViewById(id);
30 | if (from != null) {
31 | // View is found, we can set up 'from' view position now
32 | getAnimator().setFromView(id, from);
33 | }
34 | }
35 | }
36 | }
37 |
38 | @Override
39 | public void onScrollStateChanged(AbsListView view, int scrollState) {
40 | // No-op
41 | }
42 | });
43 | }
44 |
45 | @Override
46 | boolean isShownInList(ListView list, int pos) {
47 | return pos >= list.getFirstVisiblePosition() && pos <= list.getLastVisiblePosition();
48 | }
49 |
50 | @Override
51 | void scrollToPosition(ListView list, int pos) {
52 | list.setSelection(pos);
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/transition/internal/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Internal classes.
3 | */
4 | package com.alexvasilkov.gestures.transition.internal;
5 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/transition/tracker/AbstractTracker.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.transition.tracker;
2 |
3 | import android.view.View;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.Nullable;
7 |
8 | interface AbstractTracker {
9 |
10 | int NO_POSITION = -1;
11 |
12 | /**
13 | * @param id Item ID
14 | * @return Position of list item which contains element with given ID,
15 | * or {@link #NO_POSITION} if element with given ID is not part of the list.
16 | * Note, that there can be several elements inside single list item, but we only need to know
17 | * list item position, so we can scroll to it if required.
18 | */
19 | int getPositionById(@NonNull ID id);
20 |
21 | /**
22 | * @param id Item ID
23 | * @return View for given element ID, or {@code null} if view is not found.
24 | * Note, that it is safe to return {@code null} if view is not found on the screen, list view
25 | * will be automatically scrolled to needed position (as returned by
26 | * {@link #getPositionById(Object)}) and this method will be called again.
27 | */
28 | @Nullable
29 | View getViewById(@NonNull ID id);
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/transition/tracker/FromTracker.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.transition.tracker;
2 |
3 | public interface FromTracker extends AbstractTracker {
4 | }
5 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/transition/tracker/IntoTracker.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.transition.tracker;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | public interface IntoTracker extends AbstractTracker {
6 |
7 | /**
8 | * @param position List position
9 | * @return Item's id at given position, or {@code null} if position is invalid.
10 | * Note, that only one id per position should be possible for "To" view.
11 | */
12 | @Nullable
13 | ID getIdByPosition(int position);
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/transition/tracker/SimpleTracker.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.transition.tracker;
2 |
3 | import android.view.View;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.Nullable;
7 |
8 | /**
9 | * Class implementing both {@link FromTracker} and {@link IntoTracker} assuming that positions will
10 | * be used as items ids.
11 | *
12 | * Note, that it will only work correctly if both "from" and "to" lists are the same and there will
13 | * be no changes to them which will change existing items' positions. So you can't remove items, or
14 | * add items into the middle, but you can add new items to the end of the list, as long as both
15 | * lists are updated simultaneously.
16 | *
17 | * If you need to handle more advanced cases you should manually implement {@link FromTracker} and
18 | * {@link IntoTracker}, and use items ids instead of their positions.
19 | */
20 | public abstract class SimpleTracker implements FromTracker, IntoTracker {
21 |
22 | @Override
23 | public Integer getIdByPosition(int position) {
24 | return position;
25 | }
26 |
27 | @Override
28 | public int getPositionById(@NonNull Integer id) {
29 | return id;
30 | }
31 |
32 | @Override
33 | public View getViewById(@NonNull Integer id) {
34 | return getViewAt(id);
35 | }
36 |
37 | @Nullable
38 | protected abstract View getViewAt(int position);
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/utils/ClipHelper.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.utils;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.RectF;
5 | import android.view.View;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.annotation.Nullable;
9 |
10 | import com.alexvasilkov.gestures.State;
11 | import com.alexvasilkov.gestures.views.interfaces.ClipView;
12 |
13 | /**
14 | * Helper class to implement view clipping (with {@link ClipView} interface).
15 | *
16 | * Usage: call {@link #clipView(RectF, float)} method when needed and override
17 | * {@link View#draw(Canvas)} method:
18 | *
25 | */
26 | public class ClipHelper implements ClipView {
27 |
28 | private final View view;
29 |
30 | private boolean isClipping;
31 |
32 | private final RectF clipRect = new RectF();
33 | private float clipRotation;
34 |
35 | public ClipHelper(@NonNull View view) {
36 | this.view = view;
37 | }
38 |
39 | /**
40 | * {@inheritDoc}
41 | */
42 | @Override
43 | public void clipView(@Nullable RectF rect, float rotation) {
44 | if (rect == null) {
45 | if (isClipping) {
46 | isClipping = false;
47 | view.invalidate();
48 | }
49 | } else {
50 | isClipping = true;
51 |
52 | clipRect.set(rect);
53 | clipRotation = rotation;
54 | view.invalidate();
55 | }
56 | }
57 |
58 | public void onPreDraw(@NonNull Canvas canvas) {
59 | if (isClipping) {
60 | canvas.save();
61 |
62 | if (State.equals(clipRotation, 0f)) {
63 | canvas.clipRect(clipRect);
64 | } else {
65 | // Note, that prior Android 4.3 (18) canvas matrix is not correctly applied to
66 | // clip rect, clip rect will be set to its upper bound, which is good enough for us.
67 | canvas.rotate(clipRotation, clipRect.centerX(), clipRect.centerY());
68 | canvas.clipRect(clipRect);
69 | canvas.rotate(-clipRotation, clipRect.centerX(), clipRect.centerY());
70 | }
71 | }
72 | }
73 |
74 | public void onPostDraw(@NonNull Canvas canvas) {
75 | if (isClipping) {
76 | canvas.restore();
77 | }
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/utils/CropUtils.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.utils;
2 |
3 | import android.graphics.Bitmap;
4 | import android.graphics.Canvas;
5 | import android.graphics.Matrix;
6 | import android.graphics.Rect;
7 | import android.graphics.drawable.Drawable;
8 |
9 | import androidx.annotation.Nullable;
10 |
11 | import com.alexvasilkov.gestures.GestureController;
12 | import com.alexvasilkov.gestures.Settings;
13 | import com.alexvasilkov.gestures.State;
14 |
15 | public class CropUtils {
16 |
17 | private CropUtils() {}
18 |
19 | /**
20 | * Crops image drawable into bitmap according to current image position.
21 | *
22 | * @param drawable Image drawable
23 | * @param controller Image controller
24 | * @return Cropped image part
25 | */
26 | @Nullable
27 | public static Bitmap crop(Drawable drawable, GestureController controller) {
28 | if (drawable == null) {
29 | return null;
30 | }
31 |
32 | controller.stopAllAnimations();
33 | controller.updateState(); // Applying state restrictions
34 |
35 | final Settings settings = controller.getSettings();
36 | final State state = controller.getState();
37 | final float zoom = state.getZoom();
38 |
39 | // Computing crop size for base zoom level (zoom == 1)
40 | int width = Math.round(settings.getMovementAreaW() / zoom);
41 | int height = Math.round(settings.getMovementAreaH() / zoom);
42 |
43 | // Crop area coordinates within viewport
44 | Rect pos = new Rect();
45 | GravityUtils.getMovementAreaPosition(settings, pos);
46 |
47 | Matrix matrix = new Matrix();
48 | state.get(matrix);
49 | // Scaling to base zoom level (zoom == 1)
50 | matrix.postScale(1f / zoom, 1f / zoom, pos.left, pos.top);
51 | // Positioning crop area
52 | matrix.postTranslate(-pos.left, -pos.top);
53 |
54 | try {
55 | // Draw drawable into bitmap
56 | Bitmap dst = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
57 |
58 | Canvas canvas = new Canvas(dst);
59 | canvas.concat(matrix);
60 | drawable.draw(canvas);
61 |
62 | return dst;
63 | } catch (OutOfMemoryError e) {
64 | return null; // Not enough memory for cropped bitmap
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/utils/GravityUtils.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.utils;
2 |
3 | import android.graphics.Matrix;
4 | import android.graphics.Point;
5 | import android.graphics.Rect;
6 | import android.graphics.RectF;
7 | import android.view.Gravity;
8 |
9 | import androidx.annotation.NonNull;
10 |
11 | import com.alexvasilkov.gestures.Settings;
12 | import com.alexvasilkov.gestures.State;
13 |
14 | public class GravityUtils {
15 |
16 | private static final Matrix tmpMatrix = new Matrix();
17 | private static final RectF tmpRectF = new RectF();
18 |
19 | private static final Rect tmpRect1 = new Rect();
20 | private static final Rect tmpRect2 = new Rect();
21 |
22 |
23 | private GravityUtils() {}
24 |
25 | /**
26 | * Calculates image position (scaled and rotated) within viewport area with gravity applied.
27 | *
28 | * @param state Image state
29 | * @param settings Image settings
30 | * @param out Output rectangle
31 | */
32 | public static void getImagePosition(
33 | @NonNull State state,
34 | @NonNull Settings settings,
35 | @NonNull Rect out
36 | ) {
37 | state.get(tmpMatrix);
38 | getImagePosition(tmpMatrix, settings, out);
39 | }
40 |
41 | /**
42 | * Calculates image position (scaled and rotated) within viewport area with gravity applied.
43 | *
44 | * @param matrix Image matrix
45 | * @param settings Image settings
46 | * @param out Output rectangle
47 | */
48 | public static void getImagePosition(
49 | @NonNull Matrix matrix,
50 | @NonNull Settings settings,
51 | @NonNull Rect out
52 | ) {
53 | tmpRectF.set(0, 0, settings.getImageW(), settings.getImageH());
54 |
55 | matrix.mapRect(tmpRectF);
56 |
57 | final int w = Math.round(tmpRectF.width());
58 | final int h = Math.round(tmpRectF.height());
59 |
60 | // Calculating image position basing on gravity
61 | tmpRect1.set(0, 0, settings.getViewportW(), settings.getViewportH());
62 | Gravity.apply(settings.getGravity(), w, h, tmpRect1, out);
63 | }
64 |
65 | /**
66 | * Calculates movement area position within viewport area with gravity applied.
67 | *
68 | * @param settings Image settings
69 | * @param out Output rectangle
70 | */
71 | public static void getMovementAreaPosition(@NonNull Settings settings, @NonNull Rect out) {
72 | tmpRect1.set(0, 0, settings.getViewportW(), settings.getViewportH());
73 | Gravity.apply(settings.getGravity(),
74 | settings.getMovementAreaW(), settings.getMovementAreaH(), tmpRect1, out);
75 | }
76 |
77 | /**
78 | * Calculates default pivot point for scale and rotation.
79 | *
80 | * @param settings Image settings
81 | * @param out Output point
82 | */
83 | public static void getDefaultPivot(@NonNull Settings settings, @NonNull Point out) {
84 | getMovementAreaPosition(settings, tmpRect2);
85 | Gravity.apply(settings.getGravity(), 0, 0, tmpRect2, tmpRect1);
86 | out.set(tmpRect1.left, tmpRect1.top);
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/views/interfaces/AnimatorView.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.views.interfaces;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.alexvasilkov.gestures.animation.ViewPositionAnimator;
6 |
7 | /**
8 | * Common interface for views supporting position animation.
9 | */
10 | public interface AnimatorView {
11 |
12 | /**
13 | * @return {@link ViewPositionAnimator} instance to control animation from other view position.
14 | */
15 | @NonNull
16 | ViewPositionAnimator getPositionAnimator();
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/views/interfaces/ClipBounds.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.views.interfaces;
2 |
3 | import android.graphics.RectF;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | public interface ClipBounds {
8 |
9 | /**
10 | * Clips view so only {@code rect} part will be drawn.
11 | *
12 | * @param rect Rectangle to clip view bounds, or {@code null} to turn clipping off
13 | */
14 | void clipBounds(@Nullable RectF rect);
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/views/interfaces/ClipView.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.views.interfaces;
2 |
3 | import android.graphics.RectF;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | public interface ClipView {
8 |
9 | /**
10 | * Clips view so only {@code rect} part (modified by view's state) will be drawn.
11 | *
12 | * @param rect Clip rectangle or {@code null} to turn clipping off
13 | * @param rotation Clip rectangle rotation
14 | */
15 | void clipView(@Nullable RectF rect, float rotation);
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/library/src/main/java/com/alexvasilkov/gestures/views/interfaces/GestureView.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.views.interfaces;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.alexvasilkov.gestures.GestureController;
6 |
7 | /**
8 | * Common interface for all Gesture* views.
9 | *
10 | * All classes implementing this interface should be descendants of {@link android.view.View}.
11 | */
12 | public interface GestureView {
13 |
14 | /**
15 | * Returns {@link GestureController} which is a main engine for all gestures interactions.
16 | *
17 | * Use it to apply settings, access and modify image state and so on.
18 | *
19 | * @return {@link GestureController}.
20 | */
21 | @NonNull
22 | GestureController getController();
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | *.iml
3 |
--------------------------------------------------------------------------------
/sample/art/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/art/demo.gif
--------------------------------------------------------------------------------
/sample/art/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/art/ic_launcher.png
--------------------------------------------------------------------------------
/sample/art/ic_launcher.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/art/ic_launcher.psd
--------------------------------------------------------------------------------
/sample/art/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/art/logo.png
--------------------------------------------------------------------------------
/sample/art/logo.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/art/logo.psd
--------------------------------------------------------------------------------
/sample/art/logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/art/logo_small.png
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'com.github.triplet.play' version '3.7.0'
4 | }
5 |
6 | apply from: 'commons.gradle'
7 | apply from: 'release.gradle'
8 |
9 | android {
10 | compileSdkVersion 31
11 |
12 | defaultConfig {
13 | minSdkVersion 23
14 | targetSdkVersion 31
15 |
16 | setupVersion '2.8.3'
17 | setOutputFileName 'gesture-views'
18 |
19 | resConfigs 'en'
20 | }
21 |
22 | buildTypes {
23 | debug {
24 | applicationIdSuffix '.debug'
25 | }
26 | }
27 |
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_1_8
30 | targetCompatibility JavaVersion.VERSION_1_8
31 | }
32 | }
33 |
34 | dependencies {
35 | implementation project(':library')
36 |
37 | implementation 'androidx.appcompat:appcompat:1.4.0'
38 | implementation 'androidx.recyclerview:recyclerview:1.2.1'
39 | implementation 'androidx.viewpager2:viewpager2:1.0.0'
40 | implementation 'com.google.android.material:material:1.4.0'
41 |
42 | implementation 'com.github.bumptech.glide:glide:4.12.0'
43 |
44 | implementation 'com.alexvasilkov:android-commons:2.0.2'
45 | implementation 'com.alexvasilkov:events:1.0.0'
46 |
47 | implementation 'com.googlecode.flickrj-android:flickrj-android:2.1.0'
48 | implementation 'org.slf4j:slf4j-android:1.7.7' // Required by Flickr library
49 | }
50 |
--------------------------------------------------------------------------------
/sample/commons.gradle:
--------------------------------------------------------------------------------
1 | /**
2 | * Sets app version name and version code.
3 | *
4 | * Provided that version name is in form 'X.Y.Z', these version will be parsed and version code will
5 | * be generated from it as '(X * 10000 + Y * 100 + Z) * 10000 + build_number'.
6 | * E.g. if version name is '1.2.3' and build number is 456 then resulting code will be: '102030456'.
7 | *
8 | * Build number is a total number of commits from the branch root.
9 | *
10 | * Should be called instead of 'versionName' / 'versionCode' within defaultConfig closure:
11 | * setupVersion 'X.Y.Z'
12 | */
13 | ext.setupVersion = { version ->
14 | def final buildNumber = getBuildNumber()
15 | def final baseCode = getBaseVersionCode(version)
16 |
17 | println "VERSION: BASE NAME = ${version}\n" +
18 | "VERSION: BASE CODE = ${baseCode}\n" +
19 | "VERSION: BUILD = ${buildNumber}"
20 |
21 | android.defaultConfig.versionCode (baseCode + buildNumber)
22 | android.defaultConfig.versionName version
23 | }
24 |
25 | ext.getBaseVersionCode = { version ->
26 | // Version code has next format: XYYZZBBBB,
27 | // where X is a major version, Y is minor, Z is patch and B is build number (optional).
28 | // Since version code is an integer we are limited with 21.47.48.3647 (max int).
29 | def (major, minor, patch) = version.tokenize('.')
30 | if (major.toInteger() > 20) {
31 | throw new GradleException("Major part of version name cannot be larger than 20")
32 | } else if (minor.toInteger() > 99) {
33 | throw new GradleException("Minor part of version name cannot be larger than 99")
34 | } else if (patch.toInteger() > 99) {
35 | throw new GradleException("Patch part of version name cannot be larger than 99")
36 | }
37 | return (major.toInteger() * 10000 + minor.toInteger() * 100 + patch.toInteger()) * 10000
38 | }
39 |
40 | ext.getBuildNumber = {
41 | def build
42 |
43 | try {
44 | def final buildStr = 'git rev-list --count HEAD'.execute().text.trim()
45 | build = buildStr.toInteger()
46 | } catch (Exception ignored) {
47 | System.err.println 'Build number is not available'
48 | build = 1
49 | }
50 |
51 | if (build >= 10000) {
52 | throw new GradleException(
53 | "Build number ($build) exceeded 10000, version code should be adjusted to fit it")
54 | }
55 |
56 | return build
57 | }
58 |
59 |
60 | /**
61 | * Sets better apk file name using format: 'appName'-'buildType'-'flavor'-'versionName'.apk
62 | *
63 | * Should be called after call to `setupVersion`, e.g.:
64 | * setOutputFileName 'app-name'
65 | */
66 | ext.setOutputFileName = { appName ->
67 | android.applicationVariants.whenObjectAdded { variant ->
68 | variant.outputs.each { output ->
69 | output.outputFileName = "${appName}-${output.baseName}-${variant.versionName}.apk"
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/sample/release.gradle:
--------------------------------------------------------------------------------
1 | android {
2 | signingConfigs {
3 | debug {
4 | storeFile file('signing/debug.jks')
5 | }
6 | upload {
7 | storeFile file('signing/upload.jks')
8 | storePassword System.env.UPLOAD_KEYSTORE_PWD ?: ''
9 | keyAlias 'upload'
10 | keyPassword System.env.UPLOAD_KEYSTORE_PWD ?: ''
11 | }
12 | }
13 |
14 | buildTypes {
15 | debug {
16 | signingConfig signingConfigs.debug
17 | }
18 | release {
19 | signingConfig signingConfigs.upload
20 | }
21 | }
22 | }
23 |
24 | play {
25 | serviceAccountCredentials.set(file('signing/play_account.json'))
26 | track.set('alpha')
27 | }
28 |
29 | project.afterEvaluate {
30 | // Setting up 'publishSample' task
31 | // Note, that in order to work this task requires ENCRYPT_KEY and UPLOAD_KEYSTORE_PWD env vars
32 |
33 | final publish = 'publishSample'
34 |
35 | project.task publish
36 | project.tasks[publish].dependsOn 'decryptUploadKeystore'
37 | project.tasks[publish].dependsOn 'decryptPlayAccount'
38 | project.tasks[publish].dependsOn 'publishReleaseBundle'
39 | project.tasks[publish].finalizedBy 'cleanUploadKeystore'
40 | project.tasks[publish].finalizedBy 'cleanPlayAccount'
41 | }
42 |
43 | task decryptUploadKeystore {
44 | doFirst {
45 | println 'Decrypt upload keystore'
46 | decode('upload.jks')
47 | }
48 | }
49 |
50 | task cleanUploadKeystore {
51 | doLast {
52 | println 'Clean up upload keystore'
53 | cleanup('upload.jks')
54 | }
55 | }
56 |
57 | task decryptPlayAccount {
58 | doFirst {
59 | println 'Decrypt Play account'
60 | decode('play_account.json')
61 | }
62 | }
63 |
64 | task cleanPlayAccount {
65 | doLast {
66 | println 'Clean up Play account'
67 | cleanup('play_account.json')
68 | }
69 | }
70 |
71 | private static void decode(String file) {
72 | def command = "openssl enc -aes-256-cbc -d -md sha512 -pbkdf2 -iter 100000 -salt" +
73 | " -in sample/signing/${file}.enc -out sample/signing/$file -k $System.env.ENCRYPT_KEY"
74 | def process = command.execute()
75 | def error = process.err.text
76 | if (error != null && !error.isEmpty()) {
77 | println "Error:\n" + error
78 | throw new RuntimeException("Error deconding $file",)
79 | }
80 | }
81 |
82 | private static void cleanup(String file) {
83 | "rm sample/signing/$file".execute()
84 | }
85 |
--------------------------------------------------------------------------------
/sample/signing/.gitignore:
--------------------------------------------------------------------------------
1 | upload.jks
2 | play_account.json
--------------------------------------------------------------------------------
/sample/signing/debug.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/signing/debug.jks
--------------------------------------------------------------------------------
/sample/signing/play_account.json.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/signing/play_account.json.enc
--------------------------------------------------------------------------------
/sample/signing/upload.jks.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/signing/upload.jks.enc
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
14 |
15 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/sample/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexvasilkov/GestureViews/28d3a30f216a70881bca1136e71ccf276e24bb00/sample/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/SampleApplication.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample;
2 |
3 | import android.app.Application;
4 | import android.os.Build;
5 |
6 | import androidx.appcompat.app.AppCompatDelegate;
7 |
8 | import com.alexvasilkov.events.Events;
9 | import com.alexvasilkov.gestures.internal.GestureDebug;
10 | import com.alexvasilkov.gestures.sample.demo.utils.FlickrApi;
11 |
12 | public class SampleApplication extends Application {
13 |
14 | @Override
15 | public void onCreate() {
16 | super.onCreate();
17 |
18 | Events.register(FlickrApi.class);
19 |
20 | GestureDebug.setDebugFps(BuildConfig.DEBUG);
21 | GestureDebug.setDebugAnimator(BuildConfig.DEBUG);
22 |
23 | if (Build.VERSION.SDK_INT <= 28) {
24 | // It looks like day night theme does not work well in old Android versions
25 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/base/BaseSettingsActivity.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.base;
2 |
3 | import android.os.Bundle;
4 | import android.view.Menu;
5 | import android.view.MenuItem;
6 |
7 | import androidx.annotation.NonNull;
8 |
9 | import com.alexvasilkov.gestures.Settings;
10 | import com.alexvasilkov.gestures.sample.base.settings.SettingsController;
11 | import com.alexvasilkov.gestures.sample.base.settings.SettingsMenu;
12 |
13 | public abstract class BaseSettingsActivity extends BaseActivity {
14 |
15 | private final SettingsMenu settingsMenu = new SettingsMenu();
16 |
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 | settingsMenu.onRestoreInstanceState(savedInstanceState);
21 | }
22 |
23 | @Override
24 | protected void onPostCreate(Bundle savedInstanceState) {
25 | super.onPostCreate(savedInstanceState);
26 | getSupportActionBarNotNull().setDisplayHomeAsUpEnabled(true);
27 | }
28 |
29 | @Override
30 | protected void onSaveInstanceState(@NonNull Bundle outState) {
31 | settingsMenu.onSaveInstanceState(outState);
32 | super.onSaveInstanceState(outState);
33 | }
34 |
35 | @Override
36 | public boolean onCreateOptionsMenu(Menu menu) {
37 | super.onCreateOptionsMenu(menu);
38 | settingsMenu.onCreateOptionsMenu(menu);
39 | return true;
40 | }
41 |
42 | @Override
43 | public boolean onOptionsItemSelected(MenuItem item) {
44 | if (settingsMenu.onOptionsItemSelected(item)) {
45 | supportInvalidateOptionsMenu();
46 | onSettingsChanged();
47 | return true;
48 | } else {
49 | return super.onOptionsItemSelected(item);
50 | }
51 | }
52 |
53 | protected SettingsController getSettingsController() {
54 | return settingsMenu;
55 | }
56 |
57 | protected abstract void onSettingsChanged();
58 |
59 | protected void setDefaultSettings(Settings settings) {
60 | settingsMenu.setValuesFrom(settings);
61 | invalidateOptionsMenu();
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/base/StartActivity.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.base;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.widget.TextView;
6 |
7 | import com.alexvasilkov.gestures.sample.BuildConfig;
8 | import com.alexvasilkov.gestures.sample.R;
9 | import com.alexvasilkov.gestures.sample.demo.DemoActivity;
10 | import com.alexvasilkov.gestures.sample.ex.ExamplesActivity;
11 |
12 | public class StartActivity extends BaseActivity {
13 |
14 | @Override
15 | protected void onCreate(Bundle savedInstanceState) {
16 | super.onCreate(savedInstanceState);
17 | setContentView(R.layout.start_screen);
18 |
19 | findViewById(R.id.start_demo)
20 | .setOnClickListener(v -> startActivity(new Intent(this, DemoActivity.class)));
21 | findViewById(R.id.start_examples)
22 | .setOnClickListener(v -> startActivity(new Intent(this, ExamplesActivity.class)));
23 |
24 | final String version = "v" + BuildConfig.VERSION_NAME;
25 | final TextView versionView = findViewById(R.id.start_version);
26 | versionView.setText(version);
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/base/settings/SettingsController.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.base.settings;
2 |
3 | import com.alexvasilkov.gestures.views.interfaces.GestureView;
4 |
5 | public interface SettingsController {
6 | void apply(GestureView view);
7 | }
8 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/demo/utils/AspectImageView.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.demo.utils;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.util.AttributeSet;
6 | import android.util.Log;
7 |
8 | import androidx.appcompat.widget.AppCompatImageView;
9 |
10 | import com.alexvasilkov.gestures.sample.R;
11 |
12 | public class AspectImageView extends AppCompatImageView {
13 |
14 | public static final float DEFAULT_ASPECT = 16f / 9f;
15 |
16 | private static final int VERTICAL = 0;
17 | private static final int HORIZONTAL = 0;
18 |
19 | private float aspect = DEFAULT_ASPECT;
20 |
21 | public AspectImageView(Context context) {
22 | super(context);
23 | }
24 |
25 | public AspectImageView(Context context, AttributeSet attrs) {
26 | super(context, attrs);
27 |
28 | TypedArray arr = context.obtainStyledAttributes(attrs, new int[] { R.attr.aspect });
29 | aspect = arr.getFloat(0, aspect);
30 | arr.recycle();
31 | }
32 |
33 | @Override
34 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
35 | int width = MeasureSpec.getSize(widthMeasureSpec);
36 | int widthMode = MeasureSpec.getMode(widthMeasureSpec);
37 | int height = MeasureSpec.getSize(heightMeasureSpec);
38 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
39 |
40 | if (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST) {
41 | height = calculate(width, aspect, VERTICAL);
42 | } else if (heightMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.AT_MOST) {
43 | width = calculate(height, aspect, HORIZONTAL);
44 | } else if (width != 0) {
45 | height = calculate(width, aspect, VERTICAL);
46 | } else if (height != 0) {
47 | width = calculate(height, aspect, HORIZONTAL);
48 | } else {
49 | Log.e(AspectImageView.class.getSimpleName(),
50 | "Either width or height should have exact value");
51 | }
52 |
53 | int specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
54 | int specHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
55 |
56 | super.onMeasure(specWidth, specHeight);
57 | }
58 |
59 | private int calculate(int size, float aspect, int direction) {
60 | int wp = getPaddingLeft() + getPaddingRight();
61 | int hp = getPaddingTop() + getPaddingBottom();
62 | return direction == VERTICAL
63 | ? Math.round((size - wp) / aspect) + hp
64 | : Math.round((size - hp) * aspect) + wp;
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/demo/utils/FlickrApi.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.demo.utils;
2 |
3 | import com.alexvasilkov.events.EventResult;
4 | import com.alexvasilkov.events.Events.Background;
5 | import com.alexvasilkov.events.Events.Subscribe;
6 | import com.googlecode.flickrjandroid.Flickr;
7 | import com.googlecode.flickrjandroid.photos.Photo;
8 | import com.googlecode.flickrjandroid.photos.PhotoList;
9 | import com.googlecode.flickrjandroid.photos.PhotosInterface;
10 | import com.googlecode.flickrjandroid.photos.SearchParameters;
11 |
12 | import java.util.ArrayList;
13 | import java.util.HashSet;
14 | import java.util.List;
15 | import java.util.Set;
16 |
17 | public class FlickrApi {
18 |
19 | public static final String LOAD_IMAGES_EVENT = "LOAD_IMAGES_EVENT";
20 |
21 | private static final String API_KEY = "7f6035774a01a39f9056d6d7bde60002";
22 | private static final String SEARCH_QUERY = "landscape";
23 | private static final int PER_PAGE = 30;
24 | private static final int MAX_PAGES = 5;
25 |
26 | private static final Set photoParams = new HashSet<>();
27 |
28 | static {
29 | photoParams.add("url_t");
30 | photoParams.add("url_m");
31 | photoParams.add("url_h");
32 | photoParams.add("owner_name");
33 | }
34 |
35 | private static final List photos = new ArrayList<>();
36 | private static final List pages = new ArrayList<>();
37 |
38 | private FlickrApi() {}
39 |
40 | @Background(singleThread = true)
41 | @Subscribe(LOAD_IMAGES_EVENT)
42 | private static synchronized EventResult loadImages(int count) throws Exception {
43 | SearchParameters params = new SearchParameters();
44 | params.setText(SEARCH_QUERY);
45 | params.setSafeSearch(Flickr.SAFETYLEVEL_SAFE);
46 | params.setSort(SearchParameters.INTERESTINGNESS_DESC);
47 | params.setExtras(photoParams);
48 |
49 | boolean hasNext = hasNext();
50 |
51 | final PhotosInterface flickrPhotos = new Flickr(API_KEY).getPhotosInterface();
52 | while (photos.size() < count && hasNext) {
53 | final PhotoList loaded = flickrPhotos.search(params, PER_PAGE, pages.size() + 1);
54 | pages.add(loaded);
55 | photos.addAll(loaded);
56 |
57 | hasNext = hasNext();
58 | }
59 |
60 | int resultSize = Math.min(photos.size(), count);
61 |
62 | List result = new ArrayList<>(photos.subList(0, resultSize));
63 | if (!hasNext) {
64 | hasNext = photos.size() > count;
65 | }
66 |
67 | return EventResult.create().result(result, hasNext).build();
68 | }
69 |
70 | private static boolean hasNext() {
71 | if (pages.isEmpty()) {
72 | return true;
73 | } else if (pages.size() >= MAX_PAGES) {
74 | return false;
75 | } else {
76 | PhotoList page = pages.get(pages.size() - 1);
77 | return page.getPage() * page.getPerPage() < page.getTotal();
78 | }
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/demo/utils/RecyclerAdapterHelper.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.demo.utils;
2 |
3 | import androidx.recyclerview.widget.DiffUtil;
4 | import androidx.recyclerview.widget.RecyclerView;
5 |
6 | import java.util.List;
7 |
8 | public class RecyclerAdapterHelper {
9 |
10 | private RecyclerAdapterHelper() {}
11 |
12 | /**
13 | * Calls adapter's notify* methods when items are added / removed / moved / updated.
14 | */
15 | public static void notifyChanges(RecyclerView.Adapter> adapter,
16 | final List oldList, final List newList) {
17 |
18 | DiffUtil.calculateDiff(new DiffUtil.Callback() {
19 | @Override
20 | public int getOldListSize() {
21 | return oldList == null ? 0 : oldList.size();
22 | }
23 |
24 | @Override
25 | public int getNewListSize() {
26 | return newList == null ? 0 : newList.size();
27 | }
28 |
29 | @Override
30 | public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
31 | return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
32 | }
33 |
34 | @Override
35 | public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
36 | return areItemsTheSame(oldItemPosition, newItemPosition);
37 | }
38 | }).dispatchUpdatesTo(adapter);
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/ex/animations/ImageAnimationActivity.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.ex.animations;
2 |
3 | import android.os.Bundle;
4 | import android.view.View;
5 | import android.widget.ImageView;
6 |
7 | import com.alexvasilkov.gestures.sample.R;
8 | import com.alexvasilkov.gestures.sample.base.BaseSettingsActivity;
9 | import com.alexvasilkov.gestures.sample.ex.utils.GlideHelper;
10 | import com.alexvasilkov.gestures.sample.ex.utils.Painting;
11 | import com.alexvasilkov.gestures.transition.GestureTransitions;
12 | import com.alexvasilkov.gestures.transition.ViewsTransitionAnimator;
13 | import com.alexvasilkov.gestures.views.GestureImageView;
14 |
15 | /**
16 | * This example demonstrates image animation from small mode into a full one.
17 | */
18 | public class ImageAnimationActivity extends BaseSettingsActivity {
19 |
20 | private static final int PAINTING_ID = 2;
21 |
22 | private ImageView image;
23 | private GestureImageView fullImage;
24 | private View fullBackground;
25 | private ViewsTransitionAnimator> animator;
26 |
27 | private Painting painting;
28 |
29 | @Override
30 | protected void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 |
33 | initContentView();
34 |
35 | image = findViewById(R.id.single_image);
36 | fullImage = findViewById(R.id.single_image_full);
37 | fullBackground = findViewById(R.id.single_image_back);
38 |
39 | // Loading image
40 | painting = Painting.list(getResources())[PAINTING_ID];
41 | GlideHelper.loadThumb(image, painting.thumbId);
42 |
43 | // We will expand image on click
44 | image.setOnClickListener(view -> openFullImage());
45 |
46 | // Initializing image animator
47 | animator = GestureTransitions.from(image).into(fullImage);
48 | animator.addPositionUpdateListener(this::applyImageAnimationState);
49 | }
50 |
51 | /**
52 | * Override this method if you want to provide slightly different layout.
53 | */
54 | protected void initContentView() {
55 | setContentView(R.layout.image_animation_screen);
56 | setTitle(R.string.example_image_animation);
57 | }
58 |
59 | @Override
60 | public void onBackPressed() {
61 | // We should leave full image mode instead of closing the screen
62 | if (!animator.isLeaving()) {
63 | animator.exit(true);
64 | } else {
65 | super.onBackPressed();
66 | }
67 | }
68 |
69 | @Override
70 | protected void onSettingsChanged() {
71 | // Applying settings from toolbar menu, see BaseExampleActivity
72 | getSettingsController().apply(fullImage);
73 | // Resetting to initial image state
74 | fullImage.getController().resetState();
75 | }
76 |
77 | private void openFullImage() {
78 | // Setting image drawable from 'from' view to 'to' to prevent flickering
79 | if (fullImage.getDrawable() == null) {
80 | fullImage.setImageDrawable(image.getDrawable());
81 | }
82 |
83 | // Updating gesture image settings
84 | getSettingsController().apply(fullImage);
85 | // Resetting to initial image state
86 | fullImage.getController().resetState();
87 |
88 | animator.enterSingle(true);
89 | GlideHelper.loadFull(fullImage, painting.imageId, painting.thumbId);
90 | }
91 |
92 | private void applyImageAnimationState(float position, boolean isLeaving) {
93 | fullBackground.setAlpha(position);
94 | fullBackground.setVisibility(position == 0f && isLeaving ? View.INVISIBLE : View.VISIBLE);
95 | fullImage.setVisibility(position == 0f && isLeaving ? View.INVISIBLE : View.VISIBLE);
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/ex/animations/RoundImageAnimationActivity.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.ex.animations;
2 |
3 | import com.alexvasilkov.gestures.commons.circle.CircleGestureImageView;
4 | import com.alexvasilkov.gestures.commons.circle.CircleImageView;
5 | import com.alexvasilkov.gestures.sample.R;
6 |
7 | /**
8 | * Same as {@link ImageAnimationActivity} example but shows how to animate rounded image
9 | * using {@link CircleImageView} and {@link CircleGestureImageView}.
10 | */
11 | public class RoundImageAnimationActivity extends ImageAnimationActivity {
12 |
13 | @Override
14 | protected void initContentView() {
15 | setContentView(R.layout.image_animation_round_screen);
16 | setTitle(R.string.example_image_animation_circular);
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/ex/animations/cross/CrossEvents.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.ex.animations.cross;
2 |
3 | class CrossEvents {
4 |
5 | static final String SHOW_IMAGE = "show_image";
6 | static final String POSITION_CHANGED = "position_changed";
7 |
8 | private CrossEvents() {}
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/ex/animations/cross/ImageCrossAnimationActivity.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.ex.animations.cross;
2 |
3 | import android.os.Bundle;
4 | import android.view.View;
5 | import android.widget.ImageView;
6 |
7 | import com.alexvasilkov.events.Events;
8 | import com.alexvasilkov.gestures.animation.ViewPosition;
9 | import com.alexvasilkov.gestures.sample.R;
10 | import com.alexvasilkov.gestures.sample.base.BaseActivity;
11 | import com.alexvasilkov.gestures.sample.ex.utils.GlideHelper;
12 | import com.alexvasilkov.gestures.sample.ex.utils.Painting;
13 |
14 | /**
15 | * This example demonstrates image animation that crosses activities bounds.
16 | * Cross-activities animation is pretty complicated, since we'll need to have a connection between
17 | * activities in order to properly coordinate image position changes and animation state.
18 | * In this example we will use {@link Events} library to set up such connection, but you can also
19 | * do it using e.g. LocalBroadcastManager or manually by setting and removing listeners.
20 | */
21 | public class ImageCrossAnimationActivity extends BaseActivity {
22 |
23 | private static final int PAINTING_ID = 2;
24 |
25 | private ImageView image;
26 |
27 | @Override
28 | protected void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 |
31 | setContentView(R.layout.image_cross_animation_from_screen);
32 | setTitle(R.string.example_image_animation_cross);
33 | getSupportActionBarNotNull().setDisplayHomeAsUpEnabled(true);
34 |
35 | image = findViewById(R.id.single_image_from);
36 |
37 | // Loading image
38 | Painting painting = Painting.list(getResources())[PAINTING_ID];
39 | GlideHelper.loadThumb(image, painting.thumbId);
40 |
41 | // Setting image click listener
42 | image.setOnClickListener(this::showFullImage);
43 |
44 | // Image position may change (e.g. when screen orientation is changed), so we should update
45 | // fullscreen image to ensure exit animation will return image into correct position.
46 | image.getViewTreeObserver().addOnGlobalLayoutListener(this::onLayoutChanges);
47 | }
48 |
49 | private void showFullImage(View image) {
50 | // Requesting opening image in a new activity with animation.
51 | // First of all we need to get current image position:
52 | ViewPosition position = ViewPosition.from(image);
53 | // Now pass this position to a new activity. New activity should start without any
54 | // animations and should have transparent window (set through activity theme).
55 | FullImageActivity.open(this, position, PAINTING_ID);
56 | }
57 |
58 | private void onLayoutChanges() {
59 | // Notifying fullscreen image activity about image position changes.
60 | ViewPosition position = ViewPosition.from(image);
61 | Events.create(CrossEvents.POSITION_CHANGED).param(position).post();
62 | }
63 |
64 | @Events.Subscribe(CrossEvents.SHOW_IMAGE)
65 | private void showImage(boolean show) {
66 | // Fullscreen activity requested to show or hide original image
67 | image.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/alexvasilkov/gestures/sample/ex/image/pager/ViewPagerActivity.java:
--------------------------------------------------------------------------------
1 | package com.alexvasilkov.gestures.sample.ex.image.pager;
2 |
3 | import android.os.Bundle;
4 |
5 | import androidx.viewpager.widget.ViewPager;
6 |
7 | import com.alexvasilkov.gestures.commons.RecyclePagerAdapter;
8 | import com.alexvasilkov.gestures.sample.R;
9 | import com.alexvasilkov.gestures.sample.base.BaseSettingsActivity;
10 | import com.alexvasilkov.gestures.views.GestureImageView;
11 |
12 | import java.util.Objects;
13 |
14 | /**
15 | * This example demonstrates usage of {@link GestureImageView} within ViewPager.
16 | * Two things worth noting here:
17 | *
18 | * For each GestureImageView inside ViewPager we should enable smooth scrolling by calling
19 | * {@code gestureImage.getController().enableScrollInViewPager(viewPager)}
20 | * It is advised to use {@link RecyclePagerAdapter} as ViewPager adapter for better
21 | * performance when dealing with heavy data like images.
22 | *
18 | \n\nSettings
19 | \nAll features above are configurable, plus:
20 | \n
Max zoom level
21 | \n
Double tap zoom level
22 | \n
Image gravity (CENTER by default)
23 | \n
Fit method (INSIDE by default)
24 | \n
Animations duration
25 | \n
Disable all gestures
26 | \n
Disable bounds restrictions
27 | \n
… and more
28 | \n\nHint: you can use toolbar menu to change some of the setting on the fly in this and
29 | other examples in the app.
30 | \n\nListeners
31 | \nBoth OnClickListener and OnLongClickListener are supported,
32 | you can also set OnGestureListener to listen for additional
33 | events and their locations.
34 |
35 |
36 |
37 | By default GestureImageView will only allow ViewPager scrolling if it is zoomed out to a
38 | min zoom.
39 | \n\nBut it is also possible to enable seamless ViewPager integration with next features:
40 | \n\n
Image panning smoothly turns into ViewPager flipping and vise versa
41 | \n\n
ViewPager flipping is made harder when image is zoomed in, to prevent accidental
42 | page flips when panning zoomed image