diff --git a/src/main/java/org/amahi/anywhere/activity/ServerFileImageActivity.java b/src/main/java/org/amahi/anywhere/activity/ServerFileImageActivity.java index 126ca2d6e..b66740596 100644 --- a/src/main/java/org/amahi/anywhere/activity/ServerFileImageActivity.java +++ b/src/main/java/org/amahi/anywhere/activity/ServerFileImageActivity.java @@ -39,7 +39,9 @@ import org.amahi.anywhere.server.client.ServerClient; import org.amahi.anywhere.server.model.ServerFile; import org.amahi.anywhere.server.model.ServerShare; +import org.amahi.anywhere.util.FullScreenHelper; import org.amahi.anywhere.util.Intents; +import org.amahi.anywhere.view.ClickableViewPager; import java.util.ArrayList; import java.util.Arrays; @@ -51,14 +53,14 @@ /** * Image activity. Shows images as a slide show. - * Backed up by {@link android.widget.ImageView}. + * Backed up by {@link org.amahi.anywhere.view.TouchImageView}. */ public class ServerFileImageActivity extends Activity implements ViewPager.OnPageChangeListener { private static final Set SUPPORTED_FORMATS; static { - SUPPORTED_FORMATS = new HashSet(Arrays.asList( + SUPPORTED_FORMATS = new HashSet<>(Arrays.asList( "image/bmp", "image/jpeg", "image/gif", @@ -80,12 +82,26 @@ protected void onCreate(Bundle savedInstanceState) { setUpHomeNavigation(); setUpImage(); + + setUpFullScreen(); } private void setUpInjections() { AmahiApplication.from(this).inject(this); } + private void setUpFullScreen() { + final FullScreenHelper fullScreen = new FullScreenHelper(getActionBar(), getImagePager(), null); + fullScreen.enableOnClickToggle(false); + getImagePager().setOnViewPagerClickListener(new ClickableViewPager.OnClickListener() { + @Override + public void onViewPagerClick(ViewPager viewPager) { + fullScreen.toggle(); + } + }); + fullScreen.init(); + } + private void setUpHomeNavigation() { getActionBar().setHomeButtonEnabled(true); } @@ -113,8 +129,8 @@ private void setUpImageAdapter() { getImagePager().setAdapter(new ServerFilesImagePagerAdapter(getFragmentManager(), getShare(), getImageFiles())); } - private ViewPager getImagePager() { - return (ViewPager) findViewById(R.id.pager_images); + private ClickableViewPager getImagePager() { + return (ClickableViewPager) findViewById(R.id.pager_images); } private ServerShare getShare() { @@ -142,7 +158,7 @@ private void setUpImagePosition() { } private void setUpImageListener() { - getImagePager().setOnPageChangeListener(this); + getImagePager().addOnPageChangeListener(this); } @Override diff --git a/src/main/java/org/amahi/anywhere/fragment/ServerFileImageFragment.java b/src/main/java/org/amahi/anywhere/fragment/ServerFileImageFragment.java index 900c63343..bea5cef50 100644 --- a/src/main/java/org/amahi/anywhere/fragment/ServerFileImageFragment.java +++ b/src/main/java/org/amahi/anywhere/fragment/ServerFileImageFragment.java @@ -25,7 +25,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; @@ -36,6 +35,7 @@ import org.amahi.anywhere.server.model.ServerFile; import org.amahi.anywhere.server.model.ServerShare; import org.amahi.anywhere.util.Fragments; +import org.amahi.anywhere.view.TouchImageView; import javax.inject.Inject; @@ -90,8 +90,8 @@ private ServerFile getFile() { return getArguments().getParcelable(Fragments.Arguments.SERVER_FILE); } - private ImageView getImageView() { - return (ImageView) getView().findViewById(R.id.image); + private TouchImageView getImageView() { + return (TouchImageView) getView().findViewById(R.id.image); } @Override diff --git a/src/main/java/org/amahi/anywhere/util/FullScreenHelper.java b/src/main/java/org/amahi/anywhere/util/FullScreenHelper.java new file mode 100644 index 000000000..12bed9e9d --- /dev/null +++ b/src/main/java/org/amahi/anywhere/util/FullScreenHelper.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.util; + +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.view.MotionEvent; +import android.view.View; + +/** + * FullScreenHelper Helper class. + * Shows content as full screen and implements touch listener + * to control the show/hide of controls and ActionBar. + */ + +public class FullScreenHelper { + + private boolean autoHide = true; + private int autoHideDelayMillis = 3000; + private boolean onClickToggleEnabled = true; + private boolean mVisible; + private static final int UI_ANIMATION_DELAY = 300; + + private View mControlsView; + private View mContentView; + + private ActionBar actionBar; + + public FullScreenHelper(ActionBar actionBar, @NonNull View contentView) { + setActionBar(actionBar); + setmContentView(contentView); + } + + public FullScreenHelper(ActionBar actionBar, @NonNull View contentView, View controlsView) { + setActionBar(actionBar); + setmContentView(contentView); + setmControlsView(controlsView); + } + + public void setmControlsView(View mControlsView) { + this.mControlsView = mControlsView; + } + + public void setmContentView(View mContentView) { + this.mContentView = mContentView; + } + + public void setActionBar(ActionBar actionBar) { + this.actionBar = actionBar; + } + + public void setAutoHide(Boolean autoHide) { + this.autoHide = autoHide; + } + + public void setAutoHideDelayMillis(int millis) { + this.autoHideDelayMillis = millis; + } + + public void enableOnClickToggle(Boolean enable) { + this.onClickToggleEnabled = enable; + } + + public void init() { + mVisible = true; + + // Trigger the initial hide() shortly after the activity has been + // created, to briefly hint to the user that UI controls + // are available. + delayedHide(autoHideDelayMillis); + + if (onClickToggleEnabled) { + // Toggle hide/show on click + mContentView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + toggle(); + } + }); + } + + if (mControlsView != null) { + // Upon interacting with UI controls, delay any scheduled hide() + // operations to prevent the jarring behavior of controls going away + // while interacting with the UI. + mControlsView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (autoHide) { + delayedHide(autoHideDelayMillis); + } + return false; + } + }); + } + } + + public void toggle() { + if (mVisible) { + hide(); + } else { + show(); + } + } + + private final Handler mHideHandler = new Handler(); + + private final Runnable mHidePart2Runnable = new Runnable() { + @SuppressLint("InlinedApi") + @Override + public void run() { + mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + } + }; + + private final Runnable mShowPart2Runnable = new Runnable() { + @Override + public void run() { + if (actionBar != null) + actionBar.show(); + if (mControlsView != null) + mControlsView.setVisibility(View.VISIBLE); + } + }; + + private final Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + hide(); + } + }; + + private void hide() { + if (actionBar != null) + actionBar.hide(); + if (mControlsView != null) + mControlsView.setVisibility(View.GONE); + mVisible = false; + + // Schedule a runnable to remove the status and navigation bar after a delay + mHideHandler.removeCallbacks(mShowPart2Runnable); + mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY); + } + + @SuppressLint("InlinedApi") + private void show() { + // Show the system bar + mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + mVisible = true; + + // Schedule a runnable to display UI elements after a delay + mHideHandler.removeCallbacks(mHidePart2Runnable); + mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY); + } + + /** + * Schedules a call to hide() in [delay] milliseconds, canceling any + * previously scheduled calls. + */ + private void delayedHide(int delayMillis) { + mHideHandler.removeCallbacks(mHideRunnable); + mHideHandler.postDelayed(mHideRunnable, delayMillis); + } +} diff --git a/src/main/java/org/amahi/anywhere/view/ClickableViewPager.java b/src/main/java/org/amahi/anywhere/view/ClickableViewPager.java new file mode 100644 index 000000000..4b89b4675 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/view/ClickableViewPager.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + + +package org.amahi.anywhere.view; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; + +/** + * Custom ViewPager for implementing click events. + */ +public class ClickableViewPager extends ViewPager { + + private OnClickListener mOnClickListener; + private GestureDetector tapGestureDetector; + + public ClickableViewPager(Context context) { + super(context); + + setup(); + } + + public ClickableViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + + setup(); + } + + private void setup() { + tapGestureDetector = new GestureDetector(getContext(), new TapGestureListener()); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + tapGestureDetector.onTouchEvent(ev); + return super.onInterceptTouchEvent(ev); + } + + public void setOnViewPagerClickListener(OnClickListener onClickListener) { + mOnClickListener = onClickListener; + } + + public interface OnClickListener { + void onViewPagerClick(ViewPager viewPager); + } + + private class TapGestureListener extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (mOnClickListener != null) { + mOnClickListener.onViewPagerClick(ClickableViewPager.this); + } + return true; + } + } +} diff --git a/src/main/java/org/amahi/anywhere/view/TouchImageView.java b/src/main/java/org/amahi/anywhere/view/TouchImageView.java new file mode 100644 index 000000000..704682961 --- /dev/null +++ b/src/main/java/org/amahi/anywhere/view/TouchImageView.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2014 Amahi + * + * This file is part of Amahi. + * + * Amahi is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Amahi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Amahi. If not, see . + */ + +package org.amahi.anywhere.view; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Custom ImageView supporting touch gestures. Extends default ImageView class. + * Enables double click to zoom in / zoom out image. + * Enables dragging a zoomed in image to view it completely. + */ +public class TouchImageView extends AppCompatImageView { + + Matrix matrix; + + private enum State { + NONE, + DRAG, + ZOOM + } + State mode = State.NONE; + + PointF last = new PointF(); + PointF start = new PointF(); + float minScale = 1f; + float doubleClickScale = 2f; + float maxScale = 3f; + float[] m; + + int viewWidth, viewHeight; + static final int CLICK = 3; + private long DOUBLE_CLICK_INTERVAL = ViewConfiguration.getDoubleTapTimeout(); + float saveScale = 1f; + protected float origWidth, origHeight; + int oldMeasuredWidth, oldMeasuredHeight; + private long thisTouchTime; + private long previousTouchTime = 0; + + ScaleGestureDetector mScaleDetector; + + Context context; + + public TouchImageView(Context context) { + super(context); + sharedConstructing(context); + } + + public TouchImageView(Context context, AttributeSet attrs) { + super(context, attrs); + sharedConstructing(context); + } + + private void sharedConstructing(Context context) { + super.setClickable(true); + this.context = context; + mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); + matrix = new Matrix(); + m = new float[9]; + setImageMatrix(matrix); + setScaleType(AppCompatImageView.ScaleType.MATRIX); + + setOnTouchListener(new View.OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent event) { + mScaleDetector.onTouchEvent(event); + PointF curr = new PointF(event.getX(), event.getY()); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onSingleClick(curr); + thisTouchTime = System.currentTimeMillis(); + if (thisTouchTime - previousTouchTime <= DOUBLE_CLICK_INTERVAL) { + onDoubleClick(curr); + } + previousTouchTime = thisTouchTime; + break; + + case MotionEvent.ACTION_MOVE: + if (mode == State.DRAG) { + float deltaX = curr.x - last.x; + float deltaY = curr.y - last.y; + if(deltaX != 0f || deltaY != 0f) { + float fixTransX = getFixDragTrans(deltaX, viewWidth, + origWidth * saveScale); + float fixTransY = getFixDragTrans(deltaY, viewHeight, + origHeight * saveScale); + if (saveScale > 1f) { + matrix.getValues(m); + float absTransX = Math.abs(m[Matrix.MTRANS_X]); + float transXMax = (origWidth*(saveScale-1f)); + if ((transXMax - absTransX < 0.5f && fixTransX < 0f) + || (absTransX < 0.5f && fixTransX > 0f)) + getParent().requestDisallowInterceptTouchEvent(false); + else + getParent().requestDisallowInterceptTouchEvent(true); + } + matrix.postTranslate(fixTransX, fixTransY); + fixTrans(); + last.set(curr.x, curr.y); + } + } + break; + + case MotionEvent.ACTION_UP: + mode = State.NONE; + int xDiff = (int) Math.abs(curr.x - start.x); + int yDiff = (int) Math.abs(curr.y - start.y); + if (xDiff < CLICK && yDiff < CLICK) + performClick(); + break; + + case MotionEvent.ACTION_POINTER_UP: + mode = State.NONE; + break; + } + + setImageMatrix(matrix); + invalidate(); + return true; // indicate event was handled + } + + }); + + } + + private void onSingleClick(PointF curr) { + last.set(curr); + start.set(last); + mode = State.DRAG; + } + + private void onDoubleClick(PointF curr) { + float mScaleFactor; + if (saveScale < doubleClickScale) { + mScaleFactor = doubleClickScale / saveScale; + saveScale = doubleClickScale; + matrix.postScale(mScaleFactor, mScaleFactor, curr.x, viewHeight/2); + } else { + mScaleFactor = minScale / saveScale; + saveScale = minScale; + matrix.postScale(mScaleFactor, mScaleFactor, viewWidth/2, viewHeight/2); + } + fixTrans(); + } + + public void setMaxZoom(float x) { + maxScale = x; + } + + public void setDoubleClickZoom(float x) { + doubleClickScale = x; + } + + private class ScaleListener extends + ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + mode = State.ZOOM; + return true; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float mScaleFactor = detector.getScaleFactor(); + float origScale = saveScale; + saveScale *= mScaleFactor; + if (saveScale > maxScale) { + saveScale = maxScale; + mScaleFactor = maxScale / origScale; + } else if (saveScale < minScale) { + saveScale = minScale; + mScaleFactor = minScale / origScale; + } + + if (origWidth * saveScale <= viewWidth + || origHeight * saveScale <= viewHeight) + matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2, + viewHeight / 2); + else + matrix.postScale(mScaleFactor, mScaleFactor, + detector.getFocusX(), detector.getFocusY()); + + fixTrans(); + return true; + } + } + + void fixTrans() { + matrix.getValues(m); + float transX = m[Matrix.MTRANS_X]; + float transY = m[Matrix.MTRANS_Y]; + + float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale); + float fixTransY = getFixTrans(transY, viewHeight, origHeight * saveScale); + + if (fixTransX != 0 || fixTransY != 0) + matrix.postTranslate(fixTransX, fixTransY); + } + + float getFixTrans(float trans, float viewSize, float contentSize) { + float minTrans, maxTrans; + + if (contentSize <= viewSize) { + minTrans = 0; + maxTrans = viewSize - contentSize; + } else { + minTrans = viewSize - contentSize; + maxTrans = 0; + } + + if (trans < minTrans) + return -trans + minTrans; + if (trans > maxTrans) + return -trans + maxTrans; + return 0; + } + + float getFixDragTrans(float delta, float viewSize, float contentSize) { + if (contentSize <= viewSize) { + return 0f; + } + return delta; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + viewWidth = View.MeasureSpec.getSize(widthMeasureSpec); + viewHeight = View.MeasureSpec.getSize(heightMeasureSpec); + + // + // Rescales image on rotation + // + if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight + || viewWidth == 0 || viewHeight == 0) + return; + oldMeasuredHeight = viewHeight; + oldMeasuredWidth = viewWidth; + + if (saveScale == 1) { + // Fit to screen. + float scale; + + Drawable drawable = getDrawable(); + if (drawable == null || drawable.getIntrinsicWidth() == 0 + || drawable.getIntrinsicHeight() == 0) + return; + int bmWidth = drawable.getIntrinsicWidth(); + int bmHeight = drawable.getIntrinsicHeight(); + + Log.d("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight); + + float scaleX = (float) viewWidth / (float) bmWidth; + float scaleY = (float) viewHeight / (float) bmHeight; + scale = Math.min(scaleX, scaleY); + matrix.setScale(scale, scale); + + // Center the image + float redundantYSpace = (float) viewHeight + - (scale * (float) bmHeight); + float redundantXSpace = (float) viewWidth + - (scale * (float) bmWidth); + redundantYSpace /= (float) 2; + redundantXSpace /= (float) 2; + + matrix.postTranslate(redundantXSpace, redundantYSpace); + + origWidth = viewWidth - 2 * redundantXSpace; + origHeight = viewHeight - 2 * redundantYSpace; + setImageMatrix(matrix); + } + fixTrans(); + } +} \ No newline at end of file diff --git a/src/main/res/layout/activity_server_file_image.xml b/src/main/res/layout/activity_server_file_image.xml index 2c73fbc81..7296e55b3 100644 --- a/src/main/res/layout/activity_server_file_image.xml +++ b/src/main/res/layout/activity_server_file_image.xml @@ -18,7 +18,8 @@ ~ along with Amahi. If not, see . --> - \ No newline at end of file diff --git a/src/main/res/layout/fragment_server_file_image.xml b/src/main/res/layout/fragment_server_file_image.xml index 888dee752..bac6b8a11 100644 --- a/src/main/res/layout/fragment_server_file_image.xml +++ b/src/main/res/layout/fragment_server_file_image.xml @@ -28,7 +28,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"/> -