Photo viewer!
This commit is contained in:
parent
dc836b58f8
commit
c3b7fb7002
|
@ -32,6 +32,8 @@ dependencies {
|
|||
implementation 'me.grishka.litex:recyclerview:1.2.1'
|
||||
implementation 'me.grishka.litex:swiperefreshlayout:1.1.0'
|
||||
implementation 'me.grishka.litex:browser:1.4.0'
|
||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.PhotoStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -18,10 +27,11 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
|||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class StatusListFragment extends BaseRecyclerFragment<Status>{
|
||||
public abstract class StatusListFragment extends BaseRecyclerFragment<Status> implements PhotoViewerHost{
|
||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||
protected DisplayItemsAdapter adapter;
|
||||
protected String accountID;
|
||||
protected PhotoViewer currentPhotoViewer;
|
||||
|
||||
public StatusListFragment(){
|
||||
super(20);
|
||||
|
@ -84,6 +94,104 @@ public abstract class StatusListFragment extends BaseRecyclerFragment<Status>{
|
|||
imgLoader.activate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openPhotoViewer(Status _status, int attachmentIndex){
|
||||
final Status status=_status.reblog!=null ? _status.reblog : _status;
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
||||
private PhotoStatusDisplayItem.Holder transitioningHolder;
|
||||
|
||||
@Override
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
PhotoStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
|
||||
if(holder!=null)
|
||||
holder.photo.setAlpha(visible ? 1f : 0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||
PhotoStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
|
||||
if(holder!=null){
|
||||
transitioningHolder=holder;
|
||||
View view=transitioningHolder.photo;
|
||||
int[] pos={0, 0};
|
||||
view.getLocationOnScreen(pos);
|
||||
outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight());
|
||||
list.setClipChildren(false);
|
||||
transitioningHolder.itemView.setElevation(1f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
|
||||
View view=transitioningHolder.photo;
|
||||
view.setTranslationX(translateX);
|
||||
view.setTranslationY(translateY);
|
||||
view.setScaleX(scale);
|
||||
view.setScaleY(scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endPhotoViewTransition(){
|
||||
View view=transitioningHolder.photo;
|
||||
view.setTranslationX(0f);
|
||||
view.setTranslationY(0f);
|
||||
view.setScaleX(1f);
|
||||
view.setScaleY(1f);
|
||||
transitioningHolder.itemView.setElevation(0f);
|
||||
list.setClipChildren(true);
|
||||
transitioningHolder=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable getPhotoViewCurrentDrawable(int index){
|
||||
PhotoStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
|
||||
if(holder!=null)
|
||||
return holder.photo.getDrawable();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
currentPhotoViewer=null;
|
||||
}
|
||||
|
||||
private PhotoStatusDisplayItem.Holder findPhotoViewHolder(int index){
|
||||
int offset=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(item.status==_status){
|
||||
if(item instanceof PhotoStatusDisplayItem){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(getMainAdapterOffset()+offset+index);
|
||||
if(holder instanceof PhotoStatusDisplayItem.Holder){
|
||||
return (PhotoStatusDisplayItem.Holder) holder;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
if(currentPhotoViewer!=null)
|
||||
currentPhotoViewer.offsetView(-dx, -dy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected int getMainAdapterOffset(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.app.Fragment;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
@ -18,10 +20,12 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||
public class PhotoStatusDisplayItem extends StatusDisplayItem{
|
||||
private Attachment attachment;
|
||||
private ImageLoaderRequest request;
|
||||
public PhotoStatusDisplayItem(Status status, Attachment photo){
|
||||
private Fragment parentFragment;
|
||||
public PhotoStatusDisplayItem(Status status, Attachment photo, Fragment parentFragment){
|
||||
super(status);
|
||||
this.attachment=photo;
|
||||
request=new UrlImageLoaderRequest(photo.url, 1000, 1000);
|
||||
this.parentFragment=parentFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -40,10 +44,11 @@ public class PhotoStatusDisplayItem extends StatusDisplayItem{
|
|||
}
|
||||
|
||||
public static class Holder extends BindableViewHolder<PhotoStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final ImageView photo;
|
||||
public final ImageView photo;
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_photo, parent);
|
||||
photo=findViewById(R.id.photo);
|
||||
photo.setOnClickListener(this::onViewClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,5 +65,12 @@ public class PhotoStatusDisplayItem extends StatusDisplayItem{
|
|||
public void clearImage(int index){
|
||||
photo.setImageDrawable(item.attachment.blurhashPlaceholder);
|
||||
}
|
||||
|
||||
private void onViewClick(View v){
|
||||
if(item.parentFragment instanceof PhotoViewerHost){
|
||||
Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status;
|
||||
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.status, contentStatus.mediaAttachments.indexOf(item.attachment));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ public abstract class StatusDisplayItem{
|
|||
items.add(new TextStatusDisplayItem(status, HtmlParser.parse(statusForContent.content, statusForContent.emojis), fragment));
|
||||
for(Attachment attachment:statusForContent.mediaAttachments){
|
||||
if(attachment.type==Attachment.Type.IMAGE){
|
||||
items.add(new PhotoStatusDisplayItem(status, attachment));
|
||||
items.add(new PhotoStatusDisplayItem(status, attachment, fragment));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
package org.joinmastodon.android.ui.photoviewer;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
|
||||
public class PhotoViewer implements ZoomPanView.Listener{
|
||||
private Activity activity;
|
||||
private List<Attachment> attachments;
|
||||
private int currentIndex;
|
||||
private WindowManager wm;
|
||||
private Listener listener;
|
||||
|
||||
private FrameLayout windowView;
|
||||
private ViewPager2 pager;
|
||||
private ColorDrawable background=new ColorDrawable(0xff000000);
|
||||
|
||||
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Listener listener){
|
||||
this.activity=activity;
|
||||
this.attachments=attachments;
|
||||
currentIndex=index;
|
||||
this.listener=listener;
|
||||
|
||||
wm=activity.getWindowManager();
|
||||
|
||||
windowView=new FrameLayout(activity){
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event){
|
||||
if(event.getAction()==KeyEvent.ACTION_DOWN && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
onStartSwipeToDismissTransition(0f);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
windowView.setBackground(background);
|
||||
background.setAlpha(0);
|
||||
pager=new ViewPager2(activity);
|
||||
pager.setAdapter(new PhotoViewAdapter());
|
||||
pager.setCurrentItem(index, false);
|
||||
windowView.addView(pager);
|
||||
pager.setMotionEventSplittingEnabled(false);
|
||||
|
||||
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
|
||||
wlp.type=WindowManager.LayoutParams.TYPE_APPLICATION;
|
||||
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
|
||||
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||
wlp.format=PixelFormat.RGBA_8888;
|
||||
wm.addView(windowView, wlp);
|
||||
|
||||
windowView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
windowView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
Rect rect=new Rect();
|
||||
int[] radius=new int[4];
|
||||
if(listener.startPhotoViewTransition(index, rect, radius)){
|
||||
RecyclerView rv=(RecyclerView) pager.getChildAt(0);
|
||||
PhotoViewHolder holder=(PhotoViewHolder) rv.findViewHolderForAdapterPosition(index);
|
||||
holder.zoomPanView.animateIn(rect, radius);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionAnimationUpdate(float translateX, float translateY, float scale){
|
||||
listener.setTransitioningViewTransform(translateX, translateY, scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionAnimationFinished(){
|
||||
listener.endPhotoViewTransition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetBackgroundAlpha(float alpha){
|
||||
background.setAlpha(Math.round(alpha*255f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSwipeToDismiss(){
|
||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSwipeToDismissTransition(float velocityY){
|
||||
// stop receiving input events to allow the user to interact with the underlying UI while the animation is still running
|
||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||
wlp.flags|=WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||
wm.updateViewLayout(windowView, wlp);
|
||||
|
||||
int index=pager.getCurrentItem();
|
||||
listener.setPhotoViewVisibility(index, true);
|
||||
Rect rect=new Rect();
|
||||
int[] radius=new int[4];
|
||||
if(listener.startPhotoViewTransition(index, rect, radius)){
|
||||
RecyclerView rv=(RecyclerView) pager.getChildAt(0);
|
||||
PhotoViewHolder holder=(PhotoViewHolder) rv.findViewHolderForAdapterPosition(index);
|
||||
holder.zoomPanView.animateOut(rect, radius, velocityY);
|
||||
}else{
|
||||
windowView.animate()
|
||||
.alpha(0)
|
||||
.setDuration(300)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.withEndAction(()->wm.removeView(windowView))
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwipeToDismissCanceled(){
|
||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(){
|
||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
||||
wm.removeView(windowView);
|
||||
listener.photoViewerDismissed();
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called when the list containing photo views is scrolled
|
||||
* @param x
|
||||
* @param y
|
||||
*/
|
||||
public void offsetView(float x, float y){
|
||||
pager.setTranslationX(pager.getTranslationX()+x);
|
||||
pager.setTranslationY(pager.getTranslationY()+y);
|
||||
}
|
||||
|
||||
public interface Listener{
|
||||
void setPhotoViewVisibility(int index, boolean visible);
|
||||
|
||||
/**
|
||||
* Find a view for transition, save a reference to it until <code>{@link #endPhotoViewTransition()}</code> is called,
|
||||
* and set up the view hierarchy for transition (the photo view may need to be drawn outside of the bounds of its parent).
|
||||
* @param index the index of the photo/page
|
||||
* @param outRect output: the rect of the photo view <b>in screen coordinates</b>
|
||||
* @param outCornerRadius output: corner radiuses of the view [top-left, top-right, bottom-right, bottom-left]
|
||||
* @return true if the view was found and outRect and outCornerRadius are valid
|
||||
*/
|
||||
boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius);
|
||||
|
||||
/**
|
||||
* Update the transformation parameters of the transitioning photo view.
|
||||
* Only called if a previous call to {@link #startPhotoViewTransition(int, Rect, int[])} returned true.
|
||||
* @param translateX X translation
|
||||
* @param translateY Y translation
|
||||
* @param scale X and Y scale
|
||||
*/
|
||||
void setTransitioningViewTransform(float translateX, float translateY, float scale);
|
||||
|
||||
/**
|
||||
* End the transition, returning all transformations to their initial state.
|
||||
*/
|
||||
void endPhotoViewTransition();
|
||||
|
||||
/**
|
||||
* Get the current drawable that a photo view displays.
|
||||
* @param index the index of the photo
|
||||
* @return the drawable, or null if the view doesn't exist
|
||||
*/
|
||||
@Nullable
|
||||
Drawable getPhotoViewCurrentDrawable(int index);
|
||||
|
||||
void photoViewerDismissed();
|
||||
}
|
||||
|
||||
private class PhotoViewAdapter extends RecyclerView.Adapter<PhotoViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new PhotoViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PhotoViewHolder holder, int position){
|
||||
holder.bind(attachments.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return attachments.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class PhotoViewHolder extends BindableViewHolder<Attachment> implements ViewImageLoader.Target{
|
||||
public ImageView imageView;
|
||||
public ZoomPanView zoomPanView;
|
||||
|
||||
public PhotoViewHolder(){
|
||||
super(new ZoomPanView(activity));
|
||||
zoomPanView=(ZoomPanView) itemView;
|
||||
zoomPanView.setListener(PhotoViewer.this);
|
||||
itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
imageView=new ImageView(activity);
|
||||
((FrameLayout)itemView).addView(imageView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Attachment item){
|
||||
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) imageView.getLayoutParams();
|
||||
params.width=item.getWidth();
|
||||
params.height=item.getHeight();
|
||||
zoomPanView.setScrollDirections(getAbsoluteAdapterPosition()>0, getAbsoluteAdapterPosition()<attachments.size()-1);
|
||||
ViewImageLoader.load(this, listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()), new UrlImageLoaderRequest(item.url), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageDrawable(Drawable d){
|
||||
imageView.setImageDrawable(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(){
|
||||
return imageView;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.joinmastodon.android.ui.photoviewer;
|
||||
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public interface PhotoViewerHost{
|
||||
void openPhotoViewer(Status status, int attachmentIndex);
|
||||
}
|
|
@ -0,0 +1,591 @@
|
|||
package org.joinmastodon.android.ui.photoviewer;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.OverScroller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.FloatPropertyCompat;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnScaleGestureListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener{
|
||||
private View child;
|
||||
private Matrix matrix=new Matrix();
|
||||
private float[] matrixValues=new float[9];
|
||||
private ScaleGestureDetector scaleDetector;
|
||||
private GestureDetector gestureDetector;
|
||||
private OverScroller scroller;
|
||||
private boolean scaling, scrolling, swipingToDismiss, wasScaling, animatingTransform, animatingTransition, dismissAfterTransition, animatingCanceledDismiss;
|
||||
private boolean wasAnimatingTransition; // to drop any sequences of touch events that start during animation but continue after it
|
||||
|
||||
// these keep track of view translation/scrolling
|
||||
private float transX, transY;
|
||||
// translation/scrolling limits, updated whenever scale changes
|
||||
private float minTransX, minTransY, maxTransX, maxTransY;
|
||||
// total scroll offsets since the last ACTION_DOWN event, to detect scrolling axis
|
||||
private float totalScrollX, totalScrollY;
|
||||
// scale factor limits
|
||||
private float minScale, maxScale;
|
||||
// coordinates of the last scale gesture, to undo extra if it goes above maxScale
|
||||
private float lastScaleCenterX, lastScaleCenterY;
|
||||
private boolean canScrollLeft, canScrollRight;
|
||||
private ArrayList<SpringAnimation> runningTransformAnimations=new ArrayList<>(), runningTransitionAnimations=new ArrayList<>();
|
||||
|
||||
private RectF tmpRect=new RectF(), tmpRect2=new RectF();
|
||||
// the initial/final crop rect for open/close transitions, in child coordinates
|
||||
private RectF transitionCropRect=new RectF();
|
||||
private float cropAnimationValue, rawCropAndFadeValue;
|
||||
private float lastFlingVelocityY;
|
||||
private float backgroundAlphaForTransition=1f;
|
||||
|
||||
private static final String TAG="ZoomPanView";
|
||||
|
||||
private Runnable scrollerUpdater=this::doScrollerAnimation;
|
||||
private Listener listener;
|
||||
private static final FloatPropertyCompat<ZoomPanView> CROP_AND_FADE=new FloatPropertyCompat<>("cropAndFade"){
|
||||
@Override
|
||||
public float getValue(ZoomPanView object){
|
||||
return object.rawCropAndFadeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(ZoomPanView object, float value){
|
||||
object.rawCropAndFadeValue=value;
|
||||
if(value>0.1f)
|
||||
object.child.setAlpha(Math.min((value-0.1f)/0.4f, 1f));
|
||||
else
|
||||
object.child.setAlpha(0f);
|
||||
|
||||
if(value>0.3f)
|
||||
object.setCropAnimationValue(Math.min(1f, (value-0.3f)/0.7f));
|
||||
else
|
||||
object.setCropAnimationValue(0f);
|
||||
|
||||
if(value>0.5f)
|
||||
object.listener.onSetBackgroundAlpha(Math.min(1f, (value-0.5f)/0.5f*object.backgroundAlphaForTransition));
|
||||
else
|
||||
object.listener.onSetBackgroundAlpha(0f);
|
||||
|
||||
object.invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
public ZoomPanView(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ZoomPanView(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ZoomPanView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
gestureDetector=new GestureDetector(context, this);
|
||||
gestureDetector.setIsLongpressEnabled(false);
|
||||
gestureDetector.setOnDoubleTapListener(this);
|
||||
scaleDetector=new ScaleGestureDetector(context, this);
|
||||
scroller=new OverScroller(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
child=getChildAt(0);
|
||||
if(child==null)
|
||||
return;
|
||||
|
||||
int width=right-left;
|
||||
int height=bottom-top;
|
||||
float scale=Math.min(width/(float)child.getWidth(), height/(float)child.getHeight());
|
||||
minScale=scale;
|
||||
maxScale=Math.max(3f, height/(float)child.getHeight());
|
||||
matrix.setScale(scale, scale);
|
||||
if(!animatingTransition)
|
||||
updateViewTransform(false);
|
||||
updateLimits(scale);
|
||||
transX=transY=0;
|
||||
}
|
||||
|
||||
private float interpolate(float a, float b, float k){
|
||||
return a+(b-a)*k;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean drawChild(Canvas canvas, View child, long drawingTime){
|
||||
if(!canvas.isHardwareAccelerated())
|
||||
return false;
|
||||
if(child==this.child && animatingTransition){
|
||||
tmpRect.set(0, 0, child.getWidth(), child.getHeight());
|
||||
child.getMatrix().mapRect(tmpRect);
|
||||
tmpRect.offset(child.getLeft(), child.getTop());
|
||||
tmpRect2.set(transitionCropRect);
|
||||
child.getMatrix().mapRect(tmpRect2);
|
||||
tmpRect2.offset(child.getLeft(), child.getTop());
|
||||
canvas.save();
|
||||
canvas.clipRect(interpolate(tmpRect2.left, tmpRect.left, cropAnimationValue),
|
||||
interpolate(tmpRect2.top, tmpRect.top, cropAnimationValue),
|
||||
interpolate(tmpRect2.right, tmpRect.right, cropAnimationValue),
|
||||
interpolate(tmpRect2.bottom, tmpRect.bottom, cropAnimationValue));
|
||||
boolean res=super.drawChild(canvas, child, drawingTime);
|
||||
canvas.restore();
|
||||
return res;
|
||||
}
|
||||
return super.drawChild(canvas, child, drawingTime);
|
||||
}
|
||||
|
||||
public void setListener(Listener listener){
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
private void setCropAnimationValue(float val){
|
||||
cropAnimationValue=val;
|
||||
}
|
||||
|
||||
private float prepareTransitionCropRect(Rect rect){
|
||||
float initialScale;
|
||||
float scaleW=rect.width()/(float)child.getWidth();
|
||||
float scaleH=rect.height()/(float)child.getHeight();
|
||||
if(scaleW>scaleH){
|
||||
initialScale=scaleW;
|
||||
float scaledHeight=rect.height()/scaleW;
|
||||
transitionCropRect.left=0;
|
||||
transitionCropRect.right=child.getWidth();
|
||||
transitionCropRect.top=child.getHeight()/2f-scaledHeight/2f;
|
||||
transitionCropRect.bottom=transitionCropRect.top+scaledHeight;
|
||||
}else{
|
||||
initialScale=scaleH;
|
||||
float scaledWidth=rect.width()/scaleH;
|
||||
transitionCropRect.top=0;
|
||||
transitionCropRect.bottom=child.getHeight();
|
||||
transitionCropRect.left=child.getWidth()/2f-scaledWidth/2f;
|
||||
transitionCropRect.right=transitionCropRect.left+scaledWidth;
|
||||
}
|
||||
return initialScale;
|
||||
}
|
||||
|
||||
public void animateIn(Rect rect, int[] cornerRadius){
|
||||
int[] loc={0, 0};
|
||||
getLocationOnScreen(loc);
|
||||
int centerX=loc[0]+getWidth()/2;
|
||||
int centerY=loc[1]+getHeight()/2;
|
||||
float initialTransX=rect.centerX()-centerX;
|
||||
float initialTransY=rect.centerY()-centerY;
|
||||
child.setTranslationX(initialTransX);
|
||||
child.setTranslationY(initialTransY);
|
||||
float initialScale=prepareTransitionCropRect(rect);
|
||||
child.setScaleX(initialScale);
|
||||
child.setScaleY(initialScale);
|
||||
animatingTransition=true;
|
||||
|
||||
matrix.getValues(matrixValues);
|
||||
|
||||
child.setAlpha(0f);
|
||||
setupAndStartTransitionAnim(new SpringAnimation(this, CROP_AND_FADE, 1f).setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.SCALE_X, matrixValues[Matrix.MSCALE_X]));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.SCALE_Y, matrixValues[Matrix.MSCALE_Y]));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.TRANSLATION_X, matrixValues[Matrix.MTRANS_X]));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.TRANSLATION_Y, matrixValues[Matrix.MTRANS_Y]));
|
||||
postOnAnimation(new Runnable(){
|
||||
@Override
|
||||
public void run(){
|
||||
if(animatingTransition){
|
||||
listener.onTransitionAnimationUpdate(child.getTranslationX()-initialTransX, child.getTranslationY()-initialTransY, child.getScaleX()/initialScale);
|
||||
postOnAnimation(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void animateOut(Rect rect, int[] cornerRadius, float velocityY){
|
||||
int[] loc={0, 0};
|
||||
getLocationOnScreen(loc);
|
||||
int centerX=loc[0]+getWidth()/2;
|
||||
int centerY=loc[1]+getHeight()/2;
|
||||
float initialTransX=rect.centerX()-centerX;
|
||||
float initialTransY=rect.centerY()-centerY;
|
||||
float initialScale=prepareTransitionCropRect(rect);
|
||||
animatingTransition=true;
|
||||
dismissAfterTransition=true;
|
||||
rawCropAndFadeValue=1f;
|
||||
|
||||
setupAndStartTransitionAnim(new SpringAnimation(this, CROP_AND_FADE, 0f).setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.SCALE_X, initialScale));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.SCALE_Y, initialScale));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.TRANSLATION_X, initialTransX));
|
||||
setupAndStartTransitionAnim(new SpringAnimation(child, DynamicAnimation.TRANSLATION_Y, initialTransY).setStartVelocity(velocityY));
|
||||
postOnAnimation(new Runnable(){
|
||||
@Override
|
||||
public void run(){
|
||||
if(animatingTransition){
|
||||
listener.onTransitionAnimationUpdate(child.getTranslationX()-initialTransX, child.getTranslationY()-initialTransY, child.getScaleX()/initialScale);
|
||||
postOnAnimation(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateViewTransform(boolean animated){
|
||||
matrix.getValues(matrixValues);
|
||||
if(animated){
|
||||
animatingTransform=true;
|
||||
setupAndStartTransformAnim(new SpringAnimation(child, DynamicAnimation.SCALE_X, matrixValues[Matrix.MSCALE_X]));
|
||||
setupAndStartTransformAnim(new SpringAnimation(child, DynamicAnimation.SCALE_Y, matrixValues[Matrix.MSCALE_Y]));
|
||||
setupAndStartTransformAnim(new SpringAnimation(child, DynamicAnimation.TRANSLATION_X, matrixValues[Matrix.MTRANS_X]));
|
||||
setupAndStartTransformAnim(new SpringAnimation(child, DynamicAnimation.TRANSLATION_Y, matrixValues[Matrix.MTRANS_Y]));
|
||||
if(backgroundAlphaForTransition<1f){
|
||||
setupAndStartTransformAnim(new SpringAnimation(this, new FloatPropertyCompat<>("backgroundAlpha"){
|
||||
@Override
|
||||
public float getValue(ZoomPanView object){
|
||||
return backgroundAlphaForTransition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(ZoomPanView object, float value){
|
||||
backgroundAlphaForTransition=value;
|
||||
listener.onSetBackgroundAlpha(value);
|
||||
}
|
||||
}, 1f).setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_ALPHA));
|
||||
}
|
||||
}else{
|
||||
if(animatingTransition)
|
||||
Log.w(TAG, "updateViewTransform: ", new Throwable().fillInStackTrace());
|
||||
child.setScaleX(matrixValues[Matrix.MSCALE_X]);
|
||||
child.setScaleY(matrixValues[Matrix.MSCALE_Y]);
|
||||
child.setTranslationX(matrixValues[Matrix.MTRANS_X]);
|
||||
child.setTranslationY(matrixValues[Matrix.MTRANS_Y]);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLimits(float targetScale){
|
||||
float scaledWidth=child.getWidth()*targetScale;
|
||||
float scaledHeight=child.getHeight()*targetScale;
|
||||
if(scaledWidth>getWidth()){
|
||||
minTransX=(getWidth()-scaledWidth)/2f;
|
||||
maxTransX=-minTransX;
|
||||
}else{
|
||||
minTransX=maxTransX=0f;
|
||||
}
|
||||
if(scaledHeight>getHeight()){
|
||||
minTransY=(getHeight()-scaledHeight)/2f;
|
||||
maxTransY=-minTransY;
|
||||
}else{
|
||||
minTransY=maxTransY=0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void springBack(){
|
||||
if(child.getScaleX()<minScale){
|
||||
matrix.setScale(minScale, minScale);
|
||||
updateViewTransform(true);
|
||||
updateLimits(minScale);
|
||||
transX=transY=0;
|
||||
return;
|
||||
}
|
||||
boolean needAnimate=false;
|
||||
if(child.getScaleX()>maxScale){
|
||||
float scaleCorrection=maxScale/child.getScaleX();
|
||||
matrix.postScale(scaleCorrection, scaleCorrection, lastScaleCenterX, lastScaleCenterY);
|
||||
matrix.getValues(matrixValues);
|
||||
transX=matrixValues[Matrix.MTRANS_X];
|
||||
transY=matrixValues[Matrix.MTRANS_Y];
|
||||
updateLimits(maxScale);
|
||||
needAnimate=true;
|
||||
}
|
||||
needAnimate|=clampMatrixTranslationToLimits();
|
||||
if(needAnimate){
|
||||
updateViewTransform(true);
|
||||
}else if(animatingCanceledDismiss){
|
||||
animatingCanceledDismiss=false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean clampMatrixTranslationToLimits(){
|
||||
boolean needAnimate=false;
|
||||
float dtx=0f, dty=0f;
|
||||
if(transX>maxTransX){
|
||||
dtx=maxTransX-transX;
|
||||
transX=maxTransX;
|
||||
needAnimate=true;
|
||||
}else if(transX<minTransX){
|
||||
dtx=minTransX-transX;
|
||||
transX=minTransX;
|
||||
needAnimate=true;
|
||||
}
|
||||
|
||||
if(transY>maxTransY){
|
||||
dty=maxTransY-transY;
|
||||
transY=maxTransY;
|
||||
needAnimate=true;
|
||||
}else if(transY<minTransY){
|
||||
dty=minTransY-transY;
|
||||
transY=minTransY;
|
||||
needAnimate=true;
|
||||
}
|
||||
if(needAnimate)
|
||||
matrix.postTranslate(dtx, dty);
|
||||
return needAnimate;
|
||||
}
|
||||
|
||||
public void setScrollDirections(boolean left, boolean right){
|
||||
canScrollLeft=left;
|
||||
canScrollRight=right;
|
||||
}
|
||||
|
||||
private void onTransformAnimationEnd(DynamicAnimation<?> animation, boolean canceled, float value, float velocity){
|
||||
runningTransformAnimations.remove(animation);
|
||||
if(runningTransformAnimations.isEmpty()){
|
||||
animatingTransform=false;
|
||||
if(animatingCanceledDismiss){
|
||||
animatingCanceledDismiss=false;
|
||||
listener.onSwipeToDismissCanceled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onTransitionAnimationEnd(DynamicAnimation<?> animation, boolean canceled, float value, float velocity){
|
||||
runningTransitionAnimations.remove(animation);
|
||||
if(runningTransitionAnimations.isEmpty()){
|
||||
animatingTransition=false;
|
||||
wasAnimatingTransition=true;
|
||||
listener.onTransitionAnimationFinished();
|
||||
if(dismissAfterTransition)
|
||||
listener.onDismissed();
|
||||
else
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupAndStartTransformAnim(SpringAnimation anim){
|
||||
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW).setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
|
||||
anim.addEndListener(this::onTransformAnimationEnd).start();
|
||||
runningTransformAnimations.add(anim);
|
||||
}
|
||||
|
||||
private void setupAndStartTransitionAnim(SpringAnimation anim){
|
||||
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW).setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
|
||||
anim.addEndListener(this::onTransitionAnimationEnd).start();
|
||||
runningTransitionAnimations.add(anim);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev){
|
||||
boolean isUp=ev.getAction()==MotionEvent.ACTION_UP || ev.getAction()==MotionEvent.ACTION_CANCEL;
|
||||
if(animatingTransition || (wasAnimatingTransition && ev.getAction()!=MotionEvent.ACTION_DOWN))
|
||||
return true;
|
||||
scaleDetector.onTouchEvent(ev);
|
||||
if(!swipingToDismiss && isUp){
|
||||
if(scrolling || wasScaling){
|
||||
scrolling=false;
|
||||
wasScaling=false;
|
||||
springBack();
|
||||
}
|
||||
}
|
||||
if(scaling)
|
||||
return true;
|
||||
gestureDetector.onTouchEvent(ev);
|
||||
if(swipingToDismiss && isUp){
|
||||
swipingToDismiss=false;
|
||||
scrolling=false;
|
||||
if(Math.abs(child.getTranslationY())>getHeight()/4f){
|
||||
listener.onStartSwipeToDismissTransition(lastFlingVelocityY);
|
||||
}else{
|
||||
animatingCanceledDismiss=true;
|
||||
springBack();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector){
|
||||
float factor=detector.getScaleFactor();
|
||||
matrix.postScale(factor, factor, detector.getFocusX()-getWidth()/2f, detector.getFocusY()-getHeight()/2f);
|
||||
updateViewTransform(false);
|
||||
lastScaleCenterX=detector.getFocusX()-getWidth()/2f;
|
||||
lastScaleCenterY=detector.getFocusY()-getHeight()/2f;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector){
|
||||
requestDisallowInterceptTouchEvent(true);
|
||||
scaling=true;
|
||||
wasScaling=true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(ScaleGestureDetector detector){
|
||||
scaling=false;
|
||||
updateLimits(child.getScaleX());
|
||||
transX=child.getTranslationX();
|
||||
transY=child.getTranslationY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e){
|
||||
if(e.getAction()==MotionEvent.ACTION_UP){
|
||||
if(e.getEventTime()-e.getDownTime()<ViewConfiguration.getTapTimeout()){
|
||||
if(animatingTransform)
|
||||
return false;
|
||||
if(child.getScaleX()<maxScale){
|
||||
float scale=maxScale/child.getScaleX();
|
||||
matrix.postScale(scale, scale, e.getX()-getWidth()/2f, e.getY()-getHeight()/2f);
|
||||
matrix.getValues(matrixValues);
|
||||
transX=matrixValues[Matrix.MTRANS_X];
|
||||
transY=matrixValues[Matrix.MTRANS_Y];
|
||||
updateLimits(maxScale);
|
||||
clampMatrixTranslationToLimits();
|
||||
updateViewTransform(true);
|
||||
}else{
|
||||
matrix.setScale(minScale, minScale);
|
||||
updateLimits(minScale);
|
||||
transX=transY=0;
|
||||
updateViewTransform(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e){
|
||||
totalScrollX=totalScrollY=0;
|
||||
lastFlingVelocityY=0;
|
||||
wasAnimatingTransition=false;
|
||||
if(!scroller.isFinished()){
|
||||
scroller.forceFinished(true);
|
||||
removeCallbacks(scrollerUpdater);
|
||||
}
|
||||
requestDisallowInterceptTouchEvent(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e){}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
|
||||
if(minTransY==maxTransY && minTransY==0f){
|
||||
if(minTransX==maxTransX && minTransX==0f){
|
||||
if(Math.abs(totalScrollY)>Math.abs(totalScrollX)){
|
||||
if(!swipingToDismiss){
|
||||
swipingToDismiss=true;
|
||||
matrix.postTranslate(-totalScrollX, 0);
|
||||
transX-=totalScrollX;
|
||||
listener.onStartSwipeToDismiss();
|
||||
}
|
||||
matrix.postTranslate(0, -distanceY);
|
||||
transY-=distanceY;
|
||||
updateViewTransform(false);
|
||||
float alpha=1f-Math.abs(transY)/getHeight();
|
||||
backgroundAlphaForTransition=alpha;
|
||||
listener.onSetBackgroundAlpha(alpha);
|
||||
return true;
|
||||
}
|
||||
}else{
|
||||
distanceY=0;
|
||||
}
|
||||
}
|
||||
totalScrollX-=distanceX;
|
||||
totalScrollY-=distanceY;
|
||||
matrix.postTranslate(-distanceX, -distanceY);
|
||||
transX-=distanceX;
|
||||
transY-=distanceY;
|
||||
boolean atEdge=false;
|
||||
if(transX<minTransX && canScrollRight){
|
||||
matrix.postTranslate(minTransX-transX, 0f);
|
||||
transX=minTransX;
|
||||
atEdge=true;
|
||||
}else if(transX>maxTransX && canScrollLeft){
|
||||
matrix.postTranslate(maxTransX-transX, 0f);
|
||||
transX=maxTransX;
|
||||
atEdge=true;
|
||||
}
|
||||
updateViewTransform(false);
|
||||
if(!scrolling){
|
||||
scrolling=true;
|
||||
// if the image is at the edge horizontally, or the user is dragging more vertically, intercept;
|
||||
// otherwise, give these touch events to the view pager to scroll pages
|
||||
requestDisallowInterceptTouchEvent(!atEdge || Math.abs(totalScrollX)<Math.abs(totalScrollY));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e){}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY){
|
||||
if(swipingToDismiss){
|
||||
lastFlingVelocityY=velocityY;
|
||||
if(Math.abs(velocityY)>=V.dp(1000)){
|
||||
swipingToDismiss=false;
|
||||
scrolling=false;
|
||||
listener.onStartSwipeToDismissTransition(velocityY);
|
||||
}
|
||||
}else if(!animatingTransform){
|
||||
scroller.fling(Math.round(transX), Math.round(transY), Math.round(velocityX), Math.round(velocityY), Math.round(minTransX), Math.round(maxTransX), Math.round(minTransY), Math.round(maxTransY), 0, 0);
|
||||
postOnAnimation(scrollerUpdater);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void doScrollerAnimation(){
|
||||
if(scroller.computeScrollOffset()){
|
||||
float dx=transX-scroller.getCurrX();
|
||||
float dy=transY-scroller.getCurrY();
|
||||
transX-=dx;
|
||||
transY-=dy;
|
||||
matrix.postTranslate(-dx, -dy);
|
||||
updateViewTransform(false);
|
||||
postOnAnimation(scrollerUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener{
|
||||
void onTransitionAnimationUpdate(float translateX, float translateY, float scale);
|
||||
void onTransitionAnimationFinished();
|
||||
void onSetBackgroundAlpha(float alpha);
|
||||
void onStartSwipeToDismiss();
|
||||
void onStartSwipeToDismissTransition(float velocityY);
|
||||
void onSwipeToDismissCanceled();
|
||||
void onDismissed();
|
||||
}
|
||||
}
|
|
@ -5,8 +5,9 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/photo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="250dp"
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
</FrameLayout>
|
Loading…
Reference in New Issue