/*
* Copyright (C) 2016 stfalcon.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.stfalcon.frescoimageviewer;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.net.Uri;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DimenRes;
import android.support.annotation.StyleRes;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/*
* Created by Alexander Krol (troy379) on 29.08.16.
*/
public class ImageViewer implements OnDismissListener, DialogInterface.OnKeyListener {
private static final String TAG = ImageViewer.class.getSimpleName();
private Builder builder;
private AlertDialog dialog;
private ImageViewerView viewer;
protected ImageViewer(Builder builder) {
this.builder = builder;
createDialog();
}
/**
* Displays the built viewer if passed images list isn't empty
*/
public void show() {
if (!builder.dataSet.data.isEmpty()) {
dialog.show();
} else {
Log.w(TAG, "Images list cannot be empty! Viewer ignored.");
}
}
public String getUrl() {
return viewer.getUrl();
}
private void createDialog() {
viewer = new ImageViewerView(builder.context);
viewer.setCustomImageRequestBuilder(builder.customImageRequestBuilder);
viewer.setCustomDraweeHierarchyBuilder(builder.customHierarchyBuilder);
viewer.allowZooming(builder.isZoomingAllowed);
viewer.allowSwipeToDismiss(builder.isSwipeToDismissAllowed);
viewer.setOnDismissListener(this);
viewer.setBackgroundColor(builder.backgroundColor);
viewer.setOverlayView(builder.overlayView);
viewer.setImageMargin(builder.imageMarginPixels);
viewer.setContainerPadding(builder.containerPaddingPixels);
viewer.setUrls(builder.dataSet, builder.startPosition);
viewer.setPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
if (builder.imageChangeListener != null) {
builder.imageChangeListener.onImageChange(position);
}
}
});
dialog = new AlertDialog.Builder(builder.context, getDialogStyle())
.setView(viewer)
.setOnKeyListener(this)
.create();
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialogInterface) {
if (builder.onDismissListener != null) {
builder.onDismissListener.onDismiss();
}
}
});
}
/**
* Fires when swipe to dismiss was initiated
*/
@Override
public void onDismiss() {
dialog.dismiss();
}
/**
* Resets image on {@literal KeyEvent.KEYCODE_BACK} to normal scale if needed, otherwise - hide the viewer.
*/
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK &&
event.getAction() == KeyEvent.ACTION_UP &&
!event.isCanceled()) {
if (viewer.isScaled()) {
viewer.resetScale();
} else {
dialog.cancel();
}
}
return true;
}
/**
* Creates new {@code ImageRequestBuilder}.
*/
public static ImageRequestBuilder createImageRequestBuilder() {
return ImageRequestBuilder.newBuilderWithSource(Uri.parse(""));
}
/**
* Interface definition for a callback to be invoked when image was changed
*/
public interface OnImageChangeListener {
void onImageChange(int position);
}
/**
* Interface definition for a callback to be invoked when viewer was dismissed
*/
public interface OnDismissListener {
void onDismiss();
}
private @StyleRes int getDialogStyle() {
return builder.shouldStatusBarHide
? android.R.style.Theme_Translucent_NoTitleBar_Fullscreen
: android.R.style.Theme_Translucent_NoTitleBar;
}
/**
* Interface used to format custom objects into an image url.
*/
public interface Formatter<T> {
/**
* Formats an image url representation of the object.
*
* @param t The object that needs to be formatted into url.
* @return An url of image.
*/
String format(T t);
}
static class DataSet<T> {
private List<T> data;
private Formatter<T> formatter;
DataSet(List<T> data) {
this.data = data;
}
String format(int position) {
return format(data.get(position));
}
String format(T t) {
if (formatter == null) return t.toString();
else return formatter.format(t);
}
public List<T> getData() {
return data;
}
}
/**
* Builder class for {@link ImageViewer}
*/
public static class Builder<T> {
private Context context;
private DataSet<T> dataSet;
private @ColorInt int backgroundColor = Color.BLACK;
private int startPosition;
private OnImageChangeListener imageChangeListener;
private OnDismissListener onDismissListener;
private View overlayView;
private int imageMarginPixels;
private int[] containerPaddingPixels = new int[4];
private ImageRequestBuilder customImageRequestBuilder;
private GenericDraweeHierarchyBuilder customHierarchyBuilder;
private boolean shouldStatusBarHide = true;
private boolean isZoomingAllowed = true;
private boolean isSwipeToDismissAllowed = true;
/**
* Constructor using a context and images urls array for this builder and the {@link ImageViewer} it creates.
*/
public Builder(Context context, T[] images) {
this(context, new ArrayList<>(Arrays.asList(images)));
}
/**
* Constructor using a context and images urls list for this builder and the {@link ImageViewer} it creates.
*/
public Builder(Context context, List<T> images) {
this.context = context;
this.dataSet = new DataSet<>(images);
}
/**
* If you use an non-string collection, you can use custom {@link Formatter} to represent it as url.
*/
public Builder setFormatter(Formatter<T> formatter) {
this.dataSet.formatter = formatter;
return this;
}
/**
* Set background color resource for viewer
*
* @return This Builder object to allow for chaining of calls to set methods
*/
@SuppressWarnings("deprecation")
public Builder setBackgroundColorRes(@ColorRes int color) {
return this.setBackgroundColor(context.getResources().getColor(color));
}
/**
* Set background color int for viewer
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setBackgroundColor(@ColorInt int color) {
this.backgroundColor = color;
return this;
}
/**
* Set background color int for viewer
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setStartPosition(int position) {
this.startPosition = position;
return this;
}
/**
* Set {@link ImageViewer.OnImageChangeListener} for viewer.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setImageChangeListener(OnImageChangeListener imageChangeListener) {
this.imageChangeListener = imageChangeListener;
return this;
}
/**
* Set overlay view
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setOverlayView(View view) {
this.overlayView = view;
return this;
}
/**
* Set space between the images in px.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setImageMarginPx(int marginPixels) {
this.imageMarginPixels = marginPixels;
return this;
}
/**
* Set space between the images using dimension.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setImageMargin(Context context, @DimenRes int dimen) {
this.imageMarginPixels = Math.round(context.getResources().getDimension(dimen));
return this;
}
/**
* Set {@code start}, {@code top}, {@code end} and {@code bottom} padding for zooming and scrolling area in px.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setContainerPaddingPx(int start, int top, int end, int bottom) {
this.containerPaddingPixels = new int[]{start, top, end, bottom};
return this;
}
/**
* Set {@code start}, {@code top}, {@code end} and {@code bottom} padding for zooming and scrolling area using dimension.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setContainerPadding(Context context,
@DimenRes int start, @DimenRes int top,
@DimenRes int end, @DimenRes int bottom) {
setContainerPaddingPx(
Math.round(context.getResources().getDimension(start)),
Math.round(context.getResources().getDimension(top)),
Math.round(context.getResources().getDimension(end)),
Math.round(context.getResources().getDimension(bottom))
);
return this;
}
/**
* Set common padding for zooming and scrolling area in px.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setContainerPaddingPx(int padding) {
this.containerPaddingPixels = new int[]{padding, padding, padding, padding};
return this;
}
/**
* Set common padding for zooming and scrolling area using dimension.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setContainerPadding(Context context, @DimenRes int padding) {
int paddingPx = Math.round(context.getResources().getDimension(padding));
setContainerPaddingPx(paddingPx, paddingPx, paddingPx, paddingPx);
return this;
}
/**
* Set status bar visibility. By default is true.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder hideStatusBar(boolean shouldHide) {
this.shouldStatusBarHide = shouldHide;
return this;
}
/**
* Allow or disallow zooming. By default is true.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder allowZooming(boolean value) {
this.isZoomingAllowed = value;
return this;
}
/**
* Allow or disallow swipe to dismiss gesture. By default is true.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder allowSwipeToDismiss(boolean value) {
this.isSwipeToDismissAllowed = value;
return this;
}
/**
* Set {@link ImageViewer.OnDismissListener} for viewer.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
return this;
}
/**
* Set @{@code ImageRequestBuilder} for drawees. Use it for post-processing, custom resize options etc.
* Use {@link ImageViewer#createImageRequestBuilder()} to create its new instance.
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setCustomImageRequestBuilder(ImageRequestBuilder customImageRequestBuilder) {
this.customImageRequestBuilder = customImageRequestBuilder;
return this;
}
/**
* Set {@link GenericDraweeHierarchyBuilder} for drawees inside viewer.
* Use it for drawee customizing (e.g. failure image, placeholder, progressbar etc.)
* N.B.! Due to zoom logic there is limitation of scale type which always equals FIT_CENTER. Other values will be ignored
*
* @return This Builder object to allow for chaining of calls to set methods
*/
public Builder setCustomDraweeHierarchyBuilder(GenericDraweeHierarchyBuilder customHierarchyBuilder) {
this.customHierarchyBuilder = customHierarchyBuilder;
return this;
}
/**
* Creates a {@link ImageViewer} with the arguments supplied to this builder. It does not
* {@link ImageViewer#show()} the dialog. This allows the user to do any extra processing
* before displaying the dialog. Use {@link #show()} if you don't have any other processing
* to do and want this to be created and displayed.
*/
public ImageViewer build() {
return new ImageViewer(this);
}
/**
* Creates a {@link ImageViewer} with the arguments supplied to this builder and
* {@link ImageViewer#show()}'s the dialog.
*/
public ImageViewer show() {
ImageViewer dialog = build();
dialog.show();
return dialog;
}
}
}