Allow opening avatars and cover images in photo viewer

closes #24
This commit is contained in:
Grishka
2022-05-03 22:14:56 +03:00
parent bbedf46b21
commit 9823537474
4 changed files with 174 additions and 25 deletions

View File

@@ -9,6 +9,7 @@ import android.app.Fragment;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@@ -47,9 +48,12 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment; import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable; import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan; import org.joinmastodon.android.ui.text.CustomEmojiSpan;
@@ -121,6 +125,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private boolean refreshing; private boolean refreshing;
private View fab; private View fab;
private WindowInsets childInsets; private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer;
public ProfileFragment(){ public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar); super(R.layout.loader_fragment_overlay_toolbar);
@@ -595,12 +600,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){ private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
int topBarsH=getToolbar().getHeight()+statusBarHeight; int topBarsH=getToolbar().getHeight()+statusBarHeight;
if(scrollY>avatar.getTop()-topBarsH){ if(scrollY>avatarBorder.getTop()-topBarsH){
float avaAlpha=Math.max(1f-((scrollY-(avatar.getTop()-topBarsH))/(float)V.dp(38)), 0f); float avaAlpha=Math.max(1f-((scrollY-(avatarBorder.getTop()-topBarsH))/(float)V.dp(38)), 0f);
avatar.setAlpha(avaAlpha);
avatarBorder.setAlpha(avaAlpha); avatarBorder.setAlpha(avaAlpha);
}else{ }else{
avatar.setAlpha(1f);
avatarBorder.setAlpha(1f); avatarBorder.setAlpha(1f);
} }
if(scrollY>cover.getHeight()-topBarsH){ if(scrollY>cover.getHeight()-topBarsH){
@@ -622,6 +625,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
toolbarTitleView.setTranslationY(titleTransY); toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY); toolbarSubtitleView.setTranslationY(titleTransY);
} }
if(currentPhotoViewer!=null){
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
}
} }
private Fragment getFragmentForPage(int page){ private Fragment getFragmentForPage(int page){
@@ -804,15 +810,38 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return false; return false;
} }
private List<Attachment> createFakeAttachments(String url, Drawable drawable){
Attachment att=new Attachment();
att.type=Attachment.Type.IMAGE;
att.url=url;
att.meta=new Attachment.Metadata();
att.meta.width=drawable.getIntrinsicWidth();
att.meta.height=drawable.getIntrinsicHeight();
return Collections.singletonList(att);
}
private void onAvatarClick(View v){ private void onAvatarClick(View v){
if(isInEditMode){ if(isInEditMode){
startImagePicker(AVATAR_RESULT); startImagePicker(AVATAR_RESULT);
}else{
Drawable ava=avatar.getDrawable();
if(ava==null)
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
} }
} }
private void onCoverClick(View v){ private void onCoverClick(View v){
if(isInEditMode){ if(isInEditMode){
startImagePicker(COVER_RESULT); startImagePicker(COVER_RESULT);
}else{
Drawable drawable=cover.getDrawable();
if(drawable==null || drawable instanceof ColorDrawable)
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
} }
} }

View File

@@ -0,0 +1,87 @@
package org.joinmastodon.android.ui;
import android.app.Fragment;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.View;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import java.util.function.Supplier;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SingleImagePhotoViewerListener implements PhotoViewer.Listener{
private final View sourceView, transformView;
private final int[] cornerRadius;
private final Runnable onDismissed;
private final Fragment parentFragment;
private final Supplier<Drawable> currentDrawableSupplier;
private final Runnable onStart, onEnd;
private float origAlpha;
public SingleImagePhotoViewerListener(View sourceView, View transformView, int[] cornerRadius, Fragment parentFragment, Runnable onDismissed, Supplier<Drawable> currentDrawableSupplier, Runnable onStart, Runnable onEnd){
this.sourceView=sourceView;
this.transformView=transformView;
this.cornerRadius=cornerRadius;
this.onDismissed=onDismissed;
this.parentFragment=parentFragment;
this.currentDrawableSupplier=currentDrawableSupplier;
this.onStart=onStart;
this.onEnd=onEnd;
if(cornerRadius!=null && cornerRadius.length!=4)
throw new IllegalArgumentException("Corner radius must be null or have length of 4");
}
@Override
public void setPhotoViewVisibility(int index, boolean visible){
transformView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
int[] loc={0, 0};
sourceView.getLocationOnScreen(loc);
outRect.set(loc[0], loc[1], loc[0]+sourceView.getWidth(), loc[1]+sourceView.getHeight());
if(cornerRadius!=null)
System.arraycopy(cornerRadius, 0, outCornerRadius, 0, 4);
transformView.setTranslationZ(1);
if(onStart!=null)
onStart.run();
return true;
}
@Override
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
transformView.setTranslationX(translateX);
transformView.setTranslationY(translateY);
transformView.setScaleX(scale);
transformView.setScaleY(scale);
}
@Override
public void endPhotoViewTransition(){
setTransitioningViewTransform(0f, 0f, 1f);
transformView.setTranslationZ(0);
if(onEnd!=null)
onEnd.run();
}
@Nullable
@Override
public Drawable getPhotoViewCurrentDrawable(int index){
return currentDrawableSupplier.get();
}
@Override
public void photoViewerDismissed(){
onDismissed.run();
}
@Override
public void onRequestPermissions(String[] permissions){
parentFragment.requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
}
}

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -54,6 +55,9 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
private float lastFlingVelocityY; private float lastFlingVelocityY;
private float backgroundAlphaForTransition=1f; private float backgroundAlphaForTransition=1f;
private boolean forceUpdateLayout; private boolean forceUpdateLayout;
private int[] transitionCornerRadius;
private Path transitionClipPath=new Path();
private float[] tmpFloatArray=new float[8];
private static final String TAG="ZoomPanView"; private static final String TAG="ZoomPanView";
@@ -148,10 +152,25 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
child.getMatrix().mapRect(tmpRect2); child.getMatrix().mapRect(tmpRect2);
tmpRect2.offset(child.getLeft(), child.getTop()); tmpRect2.offset(child.getLeft(), child.getTop());
canvas.save(); canvas.save();
canvas.clipRect(interpolate(tmpRect2.left, tmpRect.left, cropAnimationValue), if(transitionCornerRadius!=null){
interpolate(tmpRect2.top, tmpRect.top, cropAnimationValue), float radiusScale=child.getScaleX();
interpolate(tmpRect2.right, tmpRect.right, cropAnimationValue), tmpFloatArray[0]=tmpFloatArray[1]=(float)transitionCornerRadius[0]*radiusScale*(1f-cropAnimationValue);
interpolate(tmpRect2.bottom, tmpRect.bottom, cropAnimationValue)); tmpFloatArray[2]=tmpFloatArray[3]=(float)transitionCornerRadius[1]*radiusScale*(1f-cropAnimationValue);
tmpFloatArray[4]=tmpFloatArray[5]=(float)transitionCornerRadius[2]*radiusScale*(1f-cropAnimationValue);
tmpFloatArray[6]=tmpFloatArray[7]=(float)transitionCornerRadius[3]*radiusScale*(1f-cropAnimationValue);
transitionClipPath.rewind();
transitionClipPath.addRoundRect(interpolate(tmpRect2.left, tmpRect.left, cropAnimationValue),
interpolate(tmpRect2.top, tmpRect.top, cropAnimationValue),
interpolate(tmpRect2.right, tmpRect.right, cropAnimationValue),
interpolate(tmpRect2.bottom, tmpRect.bottom, cropAnimationValue),
tmpFloatArray, Path.Direction.CW);
canvas.clipPath(transitionClipPath);
}else{
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); boolean res=super.drawChild(canvas, child, drawingTime);
canvas.restore(); canvas.restore();
return res; return res;
@@ -189,6 +208,18 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
return initialScale; return initialScale;
} }
private void validateAndSetCornerRadius(int[] cornerRadius){
transitionCornerRadius=null;
if(cornerRadius!=null && cornerRadius.length==4){
for(int corner:cornerRadius){
if(corner>0){
transitionCornerRadius=cornerRadius;
break;
}
}
}
}
public void animateIn(Rect rect, int[] cornerRadius){ public void animateIn(Rect rect, int[] cornerRadius){
int[] loc={0, 0}; int[] loc={0, 0};
getLocationOnScreen(loc); getLocationOnScreen(loc);
@@ -204,6 +235,7 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
animatingTransition=true; animatingTransition=true;
matrix.getValues(matrixValues); matrix.getValues(matrixValues);
validateAndSetCornerRadius(cornerRadius);
child.setAlpha(0f); child.setAlpha(0f);
setupAndStartTransitionAnim(new SpringAnimation(this, CROP_AND_FADE, 1f).setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE)); setupAndStartTransitionAnim(new SpringAnimation(this, CROP_AND_FADE, 1f).setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE));
@@ -233,6 +265,7 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
animatingTransition=true; animatingTransition=true;
dismissAfterTransition=true; dismissAfterTransition=true;
rawCropAndFadeValue=1f; rawCropAndFadeValue=1f;
validateAndSetCornerRadius(cornerRadius);
setupAndStartTransitionAnim(new SpringAnimation(this, CROP_AND_FADE, 0f).setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE)); 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_X, initialScale));

View File

@@ -52,7 +52,7 @@
tools:visibility="visible" tools:visibility="visible"
android:text="@string/follows_you"/> android:text="@string/follows_you"/>
<View <FrameLayout
android:id="@+id/avatar_border" android:id="@+id/avatar_border"
android:layout_width="102dp" android:layout_width="102dp"
android:layout_height="102dp" android:layout_height="102dp"
@@ -60,19 +60,19 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_marginTop="-40dp" android:layout_marginTop="-40dp"
android:layout_marginStart="14dp" android:layout_marginStart="14dp"
android:background="@drawable/profile_ava_bg"/> android:outlineProvider="@null"
android:background="@drawable/profile_ava_bg">
<ImageView <ImageView
android:id="@+id/avatar" android:id="@+id/avatar"
android:layout_width="98dp" android:layout_width="98dp"
android:layout_height="98dp" android:layout_height="98dp"
android:layout_below="@id/cover" android:layout_gravity="center"
android:layout_alignParentStart="true" android:scaleType="centerCrop"
android:layout_marginStart="16dp" android:contentDescription="@string/profile_picture"
android:layout_marginTop="-38dp" tools:src="#0f0" />
android:scaleType="centerCrop"
android:contentDescription="@string/profile_picture" </FrameLayout>
tools:src="#0f0" />
<LinearLayout <LinearLayout
android:id="@+id/profile_counters" android:id="@+id/profile_counters"
@@ -196,10 +196,10 @@
android:id="@+id/name" android:id="@+id/name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/avatar" android:layout_below="@id/avatar_border"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="12dp"
android:layout_toStartOf="@id/profile_action_btn_wrap" android:layout_toStartOf="@id/profile_action_btn_wrap"
android:textAppearance="@style/m3_headline_small" android:textAppearance="@style/m3_headline_small"
android:textAlignment="viewStart" android:textAlignment="viewStart"
@@ -232,10 +232,10 @@
android:id="@+id/name_edit" android:id="@+id/name_edit"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/avatar" android:layout_below="@id/avatar_border"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="12dp"
android:layout_toStartOf="@id/profile_action_btn_wrap" android:layout_toStartOf="@id/profile_action_btn_wrap"
android:textAppearance="@style/m3_body_large" android:textAppearance="@style/m3_body_large"
android:textSize="16sp" android:textSize="16sp"