mirror of
https://github.com/mastodon/mastodon-android.git
synced 2025-01-31 02:44:48 +01:00
parent
1b6c299251
commit
3de494f9e9
@ -31,7 +31,7 @@ import okio.Source;
|
||||
public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
private File tempFile;
|
||||
private Uri uri;
|
||||
private String contentType;
|
||||
private MediaType contentType;
|
||||
private int maxSize;
|
||||
|
||||
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
|
||||
@ -42,15 +42,16 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
opts.inJustDecodeBounds=true;
|
||||
if("file".equals(uri.getScheme())){
|
||||
BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
|
||||
contentType=UiUtils.getFileMediaType(new File(uri.getPath()));
|
||||
}else{
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
BitmapFactory.decodeStream(in, null, opts);
|
||||
}
|
||||
contentType=MastodonApp.context.getContentResolver().getType(uri);
|
||||
String mime=MastodonApp.context.getContentResolver().getType(uri);
|
||||
contentType=TextUtils.isEmpty(mime) ? null : MediaType.get(mime);
|
||||
}
|
||||
if(TextUtils.isEmpty(contentType))
|
||||
contentType="image/jpeg";
|
||||
if(contentType==null)
|
||||
contentType=MediaType.get("image/jpeg");
|
||||
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
|
||||
Bitmap bitmap;
|
||||
if(Build.VERSION.SDK_INT>=28){
|
||||
@ -136,7 +137,7 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||
}else{
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
|
||||
contentType="image/jpeg";
|
||||
contentType=MediaType.get("image/jpeg");
|
||||
}
|
||||
}
|
||||
length=tempFile.length();
|
||||
@ -163,7 +164,7 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
|
||||
@Override
|
||||
public MediaType contentType(){
|
||||
return MediaType.get(contentType);
|
||||
return contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -9,6 +9,7 @@ import android.app.Fragment;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Outline;
|
||||
@ -66,6 +67,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.photoviewer.AvatarCropper;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
@ -554,8 +556,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
}
|
||||
setTitle(account.displayName);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
ViewImageLoader.loadWithoutAnimation(avatar, avatar.getDrawable(), new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.loadWithoutAnimation(cover, cover.getDrawable(), new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
@ -1174,9 +1176,16 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(resultCode==Activity.RESULT_OK){
|
||||
if(requestCode==AVATAR_RESULT){
|
||||
editNewAvatar=data.getData();
|
||||
ViewImageLoader.loadWithoutAnimation(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
|
||||
if(!isTablet){
|
||||
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
int radius=V.dp(25);
|
||||
new AvatarCropper(getActivity(), data.getData(), new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->{}, null, null, null), (thumbnail, uri)->{
|
||||
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
avatar.setImageDrawable(thumbnail);
|
||||
editNewAvatar=uri;
|
||||
editDirty=true;
|
||||
}, ()->getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)).show();
|
||||
}else if(requestCode==COVER_RESULT){
|
||||
editNewCover=data.getData();
|
||||
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
|
||||
|
@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@ -24,7 +25,9 @@ import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.photoviewer.AvatarCropper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
@ -53,6 +56,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
|
||||
private Uri avatarUri, coverUri;
|
||||
private LinearLayout scrollContent;
|
||||
private CheckableListItem<Void> discoverableItem;
|
||||
private View avaBorder;
|
||||
|
||||
private static final int AVATAR_RESULT=348;
|
||||
private static final int COVER_RESULT=183;
|
||||
@ -80,6 +84,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
|
||||
bioEdit=view.findViewById(R.id.bio);
|
||||
avaImage=view.findViewById(R.id.avatar);
|
||||
coverImage=view.findViewById(R.id.header);
|
||||
avaBorder=view.findViewById(R.id.avatar_border);
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
@ -152,20 +157,25 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(resultCode!=Activity.RESULT_OK)
|
||||
return;
|
||||
ImageView img;
|
||||
Uri uri=data.getData();
|
||||
int size;
|
||||
if(requestCode==AVATAR_RESULT){
|
||||
img=avaImage;
|
||||
avatarUri=uri;
|
||||
size=V.dp(100);
|
||||
if(!isTablet){
|
||||
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
int radius=V.dp(25);
|
||||
new AvatarCropper(getActivity(), data.getData(), new SingleImagePhotoViewerListener(avaImage, avaBorder, new int[]{radius, radius, radius, radius}, this, ()->{}, null, null, null), (thumbnail, newUri)->{
|
||||
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
avaImage.setImageDrawable(thumbnail);
|
||||
avaImage.setForeground(null);
|
||||
avatarUri=newUri;
|
||||
}, ()->getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)).show();
|
||||
}else{
|
||||
img=coverImage;
|
||||
coverUri=uri;
|
||||
size=V.dp(1000);
|
||||
ViewImageLoader.load(coverImage, null, new UrlImageLoaderRequest(uri, size, size));
|
||||
coverImage.setForeground(null);
|
||||
}
|
||||
img.setForeground(null);
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
|
||||
}
|
||||
|
||||
private void showDiscoverabilityAlert(){
|
||||
|
@ -0,0 +1,360 @@
|
||||
package org.joinmastodon.android.ui.photoviewer;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
import android.window.OnBackInvokedDispatcher;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.WindowRootFrameLayout;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class AvatarCropper implements ZoomPanView.Listener{
|
||||
private Activity activity;
|
||||
private Context context;
|
||||
private WindowManager wm;
|
||||
private WindowRootFrameLayout windowView;
|
||||
private FragmentRootLinearLayout overlay;
|
||||
private ZoomPanView zoomPanView;
|
||||
private ImageButton closeButton;
|
||||
private ImageView image;
|
||||
private View confirmButton;
|
||||
private Runnable onCancel;
|
||||
private OnCropChosenListener cropChosenListener;
|
||||
private Uri originalUri;
|
||||
private PhotoViewer.Listener listener;
|
||||
private Drawable background=new ColorDrawable(0xff000000);
|
||||
|
||||
public AvatarCropper(Activity activity, Uri imageUri, PhotoViewer.Listener photoViewerListener, OnCropChosenListener cropChosenListener, Runnable onCancel){
|
||||
this.activity=activity;
|
||||
this.context=new ContextThemeWrapper(activity, UiUtils.getThemeForUserPreference(context, GlobalUserPreferences.ThemePreference.DARK));
|
||||
originalUri=imageUri;
|
||||
wm=context.getSystemService(WindowManager.class);
|
||||
this.cropChosenListener=cropChosenListener;
|
||||
this.onCancel=onCancel;
|
||||
this.listener=photoViewerListener;
|
||||
|
||||
windowView=(WindowRootFrameLayout) LayoutInflater.from(this.context).inflate(R.layout.avatar_cropper, null);
|
||||
overlay=windowView.findViewById(R.id.overlay);
|
||||
closeButton=windowView.findViewById(R.id.btn_back);
|
||||
zoomPanView=windowView.findViewById(R.id.zoom_pan_view);
|
||||
image=windowView.findViewById(R.id.image);
|
||||
confirmButton=windowView.findViewById(R.id.btn_confirm);
|
||||
|
||||
windowView.setBackground(background);
|
||||
windowView.setDispatchApplyWindowInsetsListener((v, insets)->{
|
||||
int bottomInset=0;
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
bottomInset=inset>0 ? Math.max(inset, V.dp(24)) : 0;
|
||||
}
|
||||
((FrameLayout.LayoutParams)confirmButton.getLayoutParams()).bottomMargin=bottomInset+V.dp(16+80);
|
||||
return overlay.dispatchApplyWindowInsets(insets);
|
||||
});
|
||||
windowView.setDispatchKeyEventListener((v, keyCode, event)->{
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
if(event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
dismiss(true, onCancel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
closeButton.setOnClickListener(v->dismiss(true, onCancel));
|
||||
overlay.setStatusBarColor(0);
|
||||
overlay.setNavigationBarColor(0);
|
||||
overlay.setBackground(new OverlayDrawable());
|
||||
zoomPanView.setListener(this);
|
||||
zoomPanView.setFill(true);
|
||||
zoomPanView.setSwipeToDismissEnabled(false);
|
||||
ViewImageLoader.load(new ViewImageLoader.Target(){
|
||||
@Override
|
||||
public void setImageDrawable(Drawable d){
|
||||
if(d!=null){
|
||||
image.setImageDrawable(d);
|
||||
image.setLayoutParams(new FrameLayout.LayoutParams(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Gravity.CENTER));
|
||||
zoomPanView.updateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(){
|
||||
return image;
|
||||
}
|
||||
}, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, 0, 0, List.of(), imageUri), false);
|
||||
windowView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)->{
|
||||
if(left==oldLeft && top==oldTop && right==oldRight && bottom==oldBottom)
|
||||
return;
|
||||
int width=right-left;
|
||||
int height=bottom-top;
|
||||
int size=V.dp(192);
|
||||
int hpad=(width-size)/2;
|
||||
int vpad=(height-size)/2;
|
||||
zoomPanView.setPadding(hpad, vpad, hpad, vpad);
|
||||
zoomPanView.updateLayout();
|
||||
});
|
||||
confirmButton.setOnClickListener(v->confirm());
|
||||
}
|
||||
|
||||
public void show(){
|
||||
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.TRANSLUCENT;
|
||||
wlp.setTitle(context.getString(R.string.avatar_move_and_scale));
|
||||
if(Build.VERSION.SDK_INT>=28)
|
||||
wlp.layoutInDisplayCutoutMode=Build.VERSION.SDK_INT>=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
wm.addView(windowView, wlp);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->dismiss(true, onCancel));
|
||||
}
|
||||
}
|
||||
|
||||
public void dismiss(boolean animated, Runnable onDone){
|
||||
if(animated){
|
||||
windowView.animate()
|
||||
.alpha(0)
|
||||
.setDuration(250)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.withEndAction(()->{
|
||||
wm.removeView(windowView);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
})
|
||||
.start();
|
||||
}else{
|
||||
wm.removeView(windowView);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
}
|
||||
}
|
||||
|
||||
@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(255*alpha));
|
||||
overlay.setAlpha(alpha);
|
||||
confirmButton.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSwipeToDismiss(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSwipeToDismissTransition(float velocityY){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwipeToDismissCanceled(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(){
|
||||
listener.setPhotoViewVisibility(0, true);
|
||||
wm.removeView(windowView);
|
||||
listener.photoViewerDismissed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleTap(){
|
||||
|
||||
}
|
||||
|
||||
private void confirm(){
|
||||
// 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;
|
||||
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | (activity.getWindow().getDecorView().getSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)));
|
||||
wm.updateViewLayout(windowView, wlp);
|
||||
|
||||
Drawable drawable=image.getDrawable();
|
||||
zoomPanView.endAllAnimations();
|
||||
Rect rect=new Rect();
|
||||
image.getHitRect(rect);
|
||||
float scale=image.getScaleX();
|
||||
int x=Math.round((zoomPanView.getPaddingLeft()-rect.left)/scale);
|
||||
int y=Math.round((zoomPanView.getPaddingTop()-rect.top)/scale);
|
||||
int size=Math.round(V.dp(192)/scale);
|
||||
if(x==0 && y==0 && drawable.getIntrinsicWidth()==drawable.getIntrinsicHeight() && size==drawable.getIntrinsicWidth()){
|
||||
dismissWithTransition();
|
||||
cropChosenListener.onCropChosen(drawable, originalUri);
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap croppedBitmap=Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
Canvas c=new Canvas(croppedBitmap);
|
||||
c.translate(-x, -y);
|
||||
drawable.draw(c);
|
||||
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
String mimetype;
|
||||
if("file".equals(originalUri.getScheme())){
|
||||
mimetype=UiUtils.getFileMediaType(new File(originalUri.getPath())).type();
|
||||
}else{
|
||||
mimetype=activity.getContentResolver().getType(originalUri);
|
||||
}
|
||||
if(mimetype==null)
|
||||
mimetype="image/jpeg";
|
||||
Bitmap.CompressFormat format=switch(mimetype){
|
||||
case "image/png", "image/gif" -> Bitmap.CompressFormat.PNG;
|
||||
default -> Bitmap.CompressFormat.JPEG;
|
||||
};
|
||||
File outputFile=new File(activity.getCacheDir(), "avatar_upload."+(format==Bitmap.CompressFormat.PNG ? "png" : "jpg"));
|
||||
try(FileOutputStream out=new FileOutputStream(outputFile)){
|
||||
croppedBitmap.compress(format, 97, out);
|
||||
}catch(IOException e){
|
||||
activity.runOnUiThread(()->{
|
||||
Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show();
|
||||
dismiss(true, onCancel);
|
||||
});
|
||||
return;
|
||||
}
|
||||
outputFile.deleteOnExit();
|
||||
activity.runOnUiThread(()->{
|
||||
image.setImageBitmap(croppedBitmap);
|
||||
image.getLayoutParams().width=image.getLayoutParams().height=size;
|
||||
zoomPanView.updateLayout();
|
||||
cropChosenListener.onCropChosen(new BitmapDrawable(croppedBitmap), Uri.fromFile(outputFile));
|
||||
dismissWithTransition();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void dismissWithTransition(){
|
||||
zoomPanView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
zoomPanView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
listener.setPhotoViewVisibility(0, true);
|
||||
int[] radius=new int[4];
|
||||
Rect rect=new Rect();
|
||||
if(listener.startPhotoViewTransition(0, rect, radius)){
|
||||
zoomPanView.animateOut(rect, radius, 0);
|
||||
}else{
|
||||
windowView.animate()
|
||||
.alpha(0)
|
||||
.setDuration(300)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.withEndAction(AvatarCropper.this::onDismissed)
|
||||
.start();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class OverlayDrawable extends Drawable{
|
||||
private Path path=new Path(), tmpPath=new Path();
|
||||
private Paint overlayPaint=new Paint(Paint.ANTI_ALIAS_FLAG), strokePaint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
public OverlayDrawable(){
|
||||
overlayPaint.setColor(0xb3000000);
|
||||
strokePaint.setColor(0x4dffffff);
|
||||
strokePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
|
||||
strokePaint.setStyle(Paint.Style.STROKE);
|
||||
strokePaint.setStrokeWidth(V.dp(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
canvas.drawPath(path, overlayPaint);
|
||||
|
||||
Rect bounds=getBounds();
|
||||
float size=V.dp(192)-strokePaint.getStrokeWidth();
|
||||
float x=bounds.centerX()-size/2;
|
||||
float y=bounds.centerY()-size/2;
|
||||
float radius=V.dp(40)-strokePaint.getStrokeWidth()/2f;
|
||||
canvas.drawRoundRect(x, y, x+size, y+size, radius, radius, strokePaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(@NonNull Rect bounds){
|
||||
path.rewind();
|
||||
path.addRect(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
|
||||
tmpPath.rewind();
|
||||
int size=V.dp(192);
|
||||
int x=bounds.centerX()-size/2;
|
||||
int y=bounds.centerY()-size/2;
|
||||
tmpPath.addRoundRect(x, y, x+size, y+size, V.dp(40), V.dp(40), Path.Direction.CW);
|
||||
path.op(tmpPath, Path.Op.DIFFERENCE);
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnCropChosenListener{
|
||||
void onCropChosen(Drawable thumbnail, Uri uri);
|
||||
}
|
||||
}
|
@ -77,6 +77,7 @@ import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.drawables.VideoPlayerSeekBarThumbDrawable;
|
||||
import org.joinmastodon.android.ui.utils.BlurHashDecoder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.WindowRootFrameLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.File;
|
||||
@ -121,7 +122,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
private String accountID;
|
||||
private BaseStatusListFragment<?> parentFragment;
|
||||
|
||||
private FrameLayout windowView;
|
||||
private WindowRootFrameLayout windowView;
|
||||
private FragmentRootLinearLayout uiOverlay;
|
||||
private ViewPager2 pager;
|
||||
private ColorDrawable background=new ColorDrawable(0xff000000);
|
||||
@ -205,9 +206,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
|
||||
wm=activity.getWindowManager();
|
||||
|
||||
windowView=new FrameLayout(activity){
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event){
|
||||
windowView=new WindowRootFrameLayout(activity);
|
||||
windowView.setDispatchKeyEventListener((v, keyCode, event)->{
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
if(event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
onStartSwipeToDismissTransition(0f);
|
||||
@ -215,10 +215,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
|
||||
});
|
||||
windowView.setDispatchApplyWindowInsetsListener((v, insets)->{
|
||||
int bottomInset=insets.getSystemWindowInsetBottom();
|
||||
bottomBar.setPadding(bottomBar.getPaddingLeft(), bottomBar.getPaddingTop(), bottomBar.getPaddingRight(), bottomInset>0 ? Math.max(bottomInset+V.dp(8), V.dp(40)) : V.dp(12));
|
||||
insets=insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
|
||||
@ -239,8 +237,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
}
|
||||
uiOverlay.dispatchApplyWindowInsets(insets);
|
||||
return insets.consumeSystemWindowInsets();
|
||||
}
|
||||
};
|
||||
});
|
||||
windowView.setBackground(background);
|
||||
background.setAlpha(0);
|
||||
pager=new ViewPager2(activity);
|
||||
|
@ -46,6 +46,8 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
private float lastScaleCenterX, lastScaleCenterY;
|
||||
private boolean canScrollLeft, canScrollRight;
|
||||
private ArrayList<SpringAnimation> runningTransformAnimations=new ArrayList<>(), runningTransitionAnimations=new ArrayList<>();
|
||||
private boolean fill; // whether the image should fill the viewport at min scale
|
||||
private boolean swipeToDismissEnabled=true;
|
||||
|
||||
private RectF tmpRect=new RectF(), tmpRect2=new RectF();
|
||||
// the initial/final crop rect for open/close transitions, in child coordinates
|
||||
@ -116,14 +118,19 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
if(child==null)
|
||||
return;
|
||||
|
||||
int width=right-left;
|
||||
int height=bottom-top;
|
||||
int width=right-left-getPaddingLeft()-getPaddingRight();
|
||||
int height=bottom-top-getPaddingTop()-getPaddingBottom();
|
||||
if(width==0 || height==0 || child.getWidth()==0 || child.getWidth()==0){
|
||||
matrix.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
float scale=Math.min(width/(float)child.getWidth(), height/(float)child.getHeight());
|
||||
float scale;
|
||||
if(fill){
|
||||
scale=Math.max(width/(float)child.getWidth(), height/(float)child.getHeight());
|
||||
}else{
|
||||
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);
|
||||
@ -323,14 +330,14 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
private void updateLimits(float targetScale){
|
||||
float scaledWidth=child.getWidth()*targetScale;
|
||||
float scaledHeight=child.getHeight()*targetScale;
|
||||
if(scaledWidth>getWidth()){
|
||||
minTransX=(getWidth()-Math.round(scaledWidth))/2f;
|
||||
if(scaledWidth>getInsetWidth()){
|
||||
minTransX=(getInsetWidth()-Math.round(scaledWidth))/2f;
|
||||
maxTransX=-minTransX;
|
||||
}else{
|
||||
minTransX=maxTransX=0f;
|
||||
}
|
||||
if(scaledHeight>getHeight()){
|
||||
minTransY=(getHeight()-Math.round(scaledHeight))/2f;
|
||||
if(scaledHeight>getInsetHeight()){
|
||||
minTransY=(getInsetHeight()-Math.round(scaledHeight))/2f;
|
||||
maxTransY=-minTransY;
|
||||
}else{
|
||||
minTransY=maxTransY=0f;
|
||||
@ -468,10 +475,10 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector){
|
||||
float factor=detector.getScaleFactor();
|
||||
matrix.postScale(factor, factor, detector.getFocusX()-getWidth()/2f, detector.getFocusY()-getHeight()/2f);
|
||||
matrix.postScale(factor, factor, detector.getFocusX()-getInsetWidth()/2f-getPaddingLeft(), detector.getFocusY()-getInsetHeight()/2f-getPaddingTop());
|
||||
updateViewTransform(false);
|
||||
lastScaleCenterX=detector.getFocusX()-getWidth()/2f;
|
||||
lastScaleCenterY=detector.getFocusY()-getHeight()/2f;
|
||||
lastScaleCenterX=detector.getFocusX()-getInsetWidth()/2f-getPaddingLeft();
|
||||
lastScaleCenterY=detector.getFocusY()-getInsetHeight()/2f-getPaddingTop();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -510,7 +517,7 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
return false;
|
||||
if(child.getScaleX()<maxScale){
|
||||
float scale=maxScale/child.getScaleX();
|
||||
matrix.postScale(scale, scale, e.getX()-getWidth()/2f, e.getY()-getHeight()/2f);
|
||||
matrix.postScale(scale, scale, e.getX()-getInsetWidth()/2f-getPaddingLeft(), e.getY()-getInsetHeight()/2f-getPaddingTop());
|
||||
matrix.getValues(matrixValues);
|
||||
transX=matrixValues[Matrix.MTRANS_X];
|
||||
transY=matrixValues[Matrix.MTRANS_Y];
|
||||
@ -554,7 +561,7 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
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(Math.abs(totalScrollY)>Math.abs(totalScrollX) && swipeToDismissEnabled){
|
||||
if(!swipingToDismiss){
|
||||
swipingToDismiss=true;
|
||||
matrix.postTranslate(-totalScrollX, 0);
|
||||
@ -630,6 +637,38 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
}
|
||||
}
|
||||
|
||||
public int getInsetWidth(){
|
||||
return getWidth()-getPaddingLeft()-getPaddingRight();
|
||||
}
|
||||
|
||||
public int getInsetHeight(){
|
||||
return getHeight()-getPaddingTop()-getPaddingBottom();
|
||||
}
|
||||
|
||||
public void setFill(boolean fill){
|
||||
this.fill=fill;
|
||||
}
|
||||
|
||||
public void endAllAnimations(){
|
||||
if(!runningTransformAnimations.isEmpty()){
|
||||
endTransformAnimations();
|
||||
}else{
|
||||
springBack();
|
||||
endTransformAnimations();
|
||||
}
|
||||
updateViewTransform(false);
|
||||
}
|
||||
|
||||
public void setSwipeToDismissEnabled(boolean swipeToDismissEnabled){
|
||||
this.swipeToDismissEnabled=swipeToDismissEnabled;
|
||||
}
|
||||
|
||||
private void endTransformAnimations(){
|
||||
for(SpringAnimation anim:new ArrayList<>(runningTransformAnimations)){
|
||||
anim.skipToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener{
|
||||
void onTransitionAnimationUpdate(float translateX, float translateY, float scale);
|
||||
void onTransitionAnimationFinished();
|
||||
|
@ -0,0 +1,44 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
public class WindowRootFrameLayout extends FrameLayout{
|
||||
private OnKeyListener dispatchKeyEventListener;
|
||||
private OnApplyWindowInsetsListener dispatchApplyWindowInsetsListener;
|
||||
|
||||
public WindowRootFrameLayout(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public WindowRootFrameLayout(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public WindowRootFrameLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event){
|
||||
return (dispatchKeyEventListener!=null && dispatchKeyEventListener.onKey(this, event.getKeyCode(), event)) || super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
public void setDispatchKeyEventListener(OnKeyListener dispatchKeyEventListener){
|
||||
this.dispatchKeyEventListener=dispatchKeyEventListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
|
||||
if(dispatchApplyWindowInsetsListener!=null)
|
||||
return dispatchApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
|
||||
return super.dispatchApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
public void setDispatchApplyWindowInsetsListener(OnApplyWindowInsetsListener dispatchApplyWindowInsetsListener){
|
||||
this.dispatchApplyWindowInsetsListener=dispatchApplyWindowInsetsListener;
|
||||
}
|
||||
}
|
79
mastodon/src/main/res/layout/avatar_cropper.xml
Normal file
79
mastodon/src/main/res/layout/avatar_cropper.xml
Normal file
@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.joinmastodon.android.ui.views.WindowRootFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.joinmastodon.android.ui.photoviewer.ZoomPanView
|
||||
android:id="@+id/zoom_pan_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false">
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"/>
|
||||
</org.joinmastodon.android.ui.photoviewer.ZoomPanView>
|
||||
|
||||
<me.grishka.appkit.views.FragmentRootLinearLayout
|
||||
android:id="@+id/overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:actionBarSize"
|
||||
android:layout_gravity="top"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="56dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?android:actionBarItemBackground"
|
||||
android:tint="?colorM3OnSurfaceVariant"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
android:contentDescription="@string/back"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textSize="22dp"
|
||||
android:textColor="?colorM3OnSurface"
|
||||
android:text="@string/avatar_move_and_scale"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</me.grishka.appkit.views.FragmentRootLinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_confirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:minWidth="152dp"
|
||||
android:paddingStart="16dp"
|
||||
style="@style/Widget.Mastodon.M3.Button.Filled">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:drawableStart="@drawable/ic_check_24px"
|
||||
style="@style/Widget.Mastodon.M3.Button.Filled"
|
||||
android:background="@null"
|
||||
android:padding="0dp"
|
||||
android:drawablePadding="7dp"
|
||||
android:drawableTint="@color/button_text_m3_filled"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:text="@string/confirm_avatar_crop"/>
|
||||
</FrameLayout>
|
||||
|
||||
</org.joinmastodon.android.ui.views.WindowRootFrameLayout>
|
@ -39,6 +39,7 @@
|
||||
android:background="?colorM3SecondaryContainer"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/avatar_border"
|
||||
android:layout_width="104dp"
|
||||
android:layout_height="104dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
|
@ -811,4 +811,6 @@
|
||||
<string name="moderation_warning_action_suspend">Your account has been suspended.</string>
|
||||
<string name="moderation_warning_learn_more">Learn more</string>
|
||||
<string name="text_show_more">More</string>
|
||||
<string name="avatar_move_and_scale">Move and scale</string>
|
||||
<string name="confirm_avatar_crop">Choose</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user