From 08f928a2b2d596c3a98b1d6d5f3fe708c5c472cf Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Fri, 14 Jul 2017 08:26:58 +0300 Subject: [PATCH] Improve media browser and video viewer * show/hide status bar by tapping a photo * dim and color status bar in video/media viewers * show/hide status bar in video viewer * use shared element transition when opening a photo is possible * center video in VideoView --- .../tusky/ViewMediaActivity.java | 55 ++++++-- .../tusky/ViewVideoActivity.java | 50 +++++++- .../tusky/adapter/StatusViewHolder.java | 4 +- .../tusky/fragment/NotificationsFragment.java | 5 +- .../tusky/fragment/SFragment.java | 16 ++- .../tusky/fragment/TimelineFragment.java | 5 +- .../tusky/fragment/ViewMediaFragment.java | 119 ++++++++++++++---- .../tusky/fragment/ViewThreadFragment.java | 5 +- .../interfaces/StatusActionListener.java | 2 +- .../tusky/pager/ImagePagerAdapter.java | 8 +- .../main/res/layout/activity_view_video.xml | 3 +- 11 files changed, 222 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.java index b22d452f..b6e453a7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.java @@ -16,10 +16,13 @@ package com.keylesspalace.tusky; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.app.DownloadManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -45,20 +48,25 @@ import com.keylesspalace.tusky.view.ImageViewPager; import java.io.File; -public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment.OnDismissListener { +public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment.PhotoActionsListener { private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; private ImageViewPager viewPager; private View anyView; private String[] imageUrls; + private Toolbar toolbar; + + private boolean isToolbarVisible = true; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_view_media); + supportPostponeEnterTransition(); + // Obtain the views. - final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar = (Toolbar) findViewById(R.id.toolbar); viewPager = (ImageViewPager) findViewById(R.id.view_pager); anyView = toolbar; @@ -69,13 +77,14 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment // Setup the view pager. final ImagePagerAdapter adapter = new ImagePagerAdapter(getSupportFragmentManager(), - imageUrls); + imageUrls, initialPosition); viewPager.setAdapter(adapter); viewPager.setCurrentItem(initialPosition); viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) {} + int positionOffsetPixels) { + } @Override public void onPageSelected(int position) { @@ -84,7 +93,8 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment } @Override - public void onPageScrollStateChanged(int state) {} + public void onPageScrollStateChanged(int state) { + } }); // Setup the toolbar. @@ -98,7 +108,7 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment toolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - finish(); + supportFinishAfterTransition(); } }); toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { @@ -113,6 +123,13 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment return true; } }); + + View decorView = getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE; + decorView.setSystemUiVisibility(uiOptions); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(Color.BLACK); + } } @Override @@ -132,12 +149,28 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment @Override public void onDismiss() { - finish(); + supportFinishAfterTransition(); + } + + @Override + public void onPhotoTap() { + isToolbarVisible = !isToolbarVisible; + final int visibility = isToolbarVisible ? View.VISIBLE : View.INVISIBLE; + int alpha = isToolbarVisible ? 1 : 0; + toolbar.animate().alpha(alpha) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + toolbar.setVisibility(visibility); + animation.removeListener(this); + } + }) + .start(); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], - @NonNull int[] grantResults) { + @NonNull int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: { if (grantResults.length > 0 @@ -158,7 +191,7 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment } private void doErrorDialog(@StringRes int descriptionId, @StringRes int actionId, - View.OnClickListener listener) { + View.OnClickListener listener) { if (anyView != null) { Snackbar bar = Snackbar.make(anyView, getString(descriptionId), Snackbar.LENGTH_SHORT); @@ -170,9 +203,9 @@ public class ViewMediaActivity extends BaseActivity implements ViewMediaFragment private void downloadImage() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { + != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, - new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); } else { String url = imageUrls[viewPager.getCurrentItem()]; diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java index a6e06a52..7a7b33ed 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.java @@ -15,17 +15,28 @@ package com.keylesspalace.tusky; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.graphics.Color; import android.media.MediaPlayer; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.widget.MediaController; import android.widget.ProgressBar; import android.widget.VideoView; public class ViewVideoActivity extends BaseActivity { + + Handler handler = new Handler(Looper.getMainLooper()); + Toolbar toolbar; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -34,7 +45,7 @@ public class ViewVideoActivity extends BaseActivity { final ProgressBar progressBar = (ProgressBar) findViewById(R.id.video_progress); VideoView videoView = (VideoView) findViewById(R.id.video_player); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar bar = getSupportActionBar(); if (bar != null) { @@ -55,9 +66,28 @@ public class ViewVideoActivity extends BaseActivity { public void onPrepared(MediaPlayer mp) { progressBar.setVisibility(View.GONE); mp.setLooping(true); + hideToolbarAfterDelay(); } }); videoView.start(); + + videoView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + handler.removeCallbacksAndMessages(null); + toolbar.animate().cancel(); + toolbar.setAlpha(1); + toolbar.setVisibility(View.VISIBLE); + hideToolbarAfterDelay(); + } + return false; + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(Color.BLACK); + } } @Override @@ -70,4 +100,22 @@ public class ViewVideoActivity extends BaseActivity { } return super.onOptionsItemSelected(item); } + + void hideToolbarAfterDelay() { + handler.postDelayed(new Runnable() { + @Override + public void run() { + toolbar.animate().alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + View decorView = getWindow().getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE; + decorView.setSystemUiVisibility(uiOptions); + toolbar.setVisibility(View.INVISIBLE); + animation.removeListener(this); + } + }); + } + }, 3000); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 30dbd3cf..f1de206e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -275,7 +275,7 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { previews[i].setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - listener.onViewMedia(urls, urlIndex, type); + listener.onViewMedia(urls, urlIndex, type, v); } }); } @@ -359,7 +359,7 @@ public class StatusViewHolder extends RecyclerView.ViewHolder { mediaLabel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - listener.onViewMedia(urls, 0, type); + listener.onViewMedia(urls, 0, type, null); } }); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 3df0db63..b5fa5401 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -243,8 +243,9 @@ public class NotificationsFragment extends SFragment implements } @Override - public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type) { - super.viewMedia(urls, urlIndex, type); + public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + View view) { + super.viewMedia(urls, urlIndex, type, view); } @Override diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index c76b4447..1a5a6612 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -19,7 +19,9 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.content.LocalBroadcastManager; +import android.support.v4.view.ViewCompat; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.text.Spanned; @@ -293,13 +295,23 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov popup.show(); } - protected void viewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type) { + protected void viewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + @Nullable View view) { switch (type) { case IMAGE: { Intent intent = new Intent(getContext(), ViewMediaActivity.class); intent.putExtra("urls", urls); intent.putExtra("urlIndex", urlIndex); - startActivity(intent); + if (view != null) { + String url = urls[urlIndex]; + ViewCompat.setTransitionName(view, url); + ActivityOptionsCompat options = + ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), + view, url); + startActivity(intent, options.toBundle()); + } else { + startActivity(intent); + } break; } case GIFV: diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 0a6b7d2c..548851bd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -342,8 +342,9 @@ public class TimelineFragment extends SFragment implements } @Override - public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type) { - super.viewMedia(urls, urlIndex, type); + public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + View view) { + super.viewMedia(urls, urlIndex, type, view); } @Override diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java index b0102df7..fa409757 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.java @@ -17,6 +17,7 @@ package com.keylesspalace.tusky.fragment; import android.content.Context; import android.os.Bundle; +import android.support.v4.view.ViewCompat; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -29,20 +30,30 @@ import com.github.chrisbanes.photoview.PhotoView; import com.github.chrisbanes.photoview.PhotoViewAttacher; import com.keylesspalace.tusky.R; import com.squareup.picasso.Callback; +import com.squareup.picasso.NetworkPolicy; import com.squareup.picasso.Picasso; public class ViewMediaFragment extends BaseFragment { - public interface OnDismissListener { + public interface PhotoActionsListener { void onDismiss(); + + void onPhotoTap(); } private PhotoViewAttacher attacher; - private OnDismissListener onDismissListener; + private PhotoActionsListener photoActionsListener; + View rootView; + PhotoView photoView; - public static ViewMediaFragment newInstance(String url) { + private static final String ARG_URL = "url"; + private static final String ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"; + + public static ViewMediaFragment newInstance(String url, boolean shouldStartPostponedTransition) { Bundle arguments = new Bundle(); ViewMediaFragment fragment = new ViewMediaFragment(); arguments.putString("url", url); + arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition); + fragment.setArguments(arguments); return fragment; } @@ -50,18 +61,17 @@ public class ViewMediaFragment extends BaseFragment { @Override public void onAttach(Context context) { super.onAttach(context); - onDismissListener = (OnDismissListener) context; + photoActionsListener = (PhotoActionsListener) context; } @Override public View onCreateView(LayoutInflater inflater, final ViewGroup container, - Bundle savedInstanceState) { - final View rootView = inflater.inflate(R.layout.fragment_view_media, container, false); + Bundle savedInstanceState) { + rootView = inflater.inflate(R.layout.fragment_view_media, container, false); + photoView = (PhotoView) rootView.findViewById(R.id.view_media_image); - PhotoView photoView = (PhotoView) rootView.findViewById(R.id.view_media_image); - - Bundle arguments = getArguments(); - String url = arguments.getString("url"); + final Bundle arguments = getArguments(); + final String url = arguments.getString("url"); attacher = new PhotoViewAttacher(photoView); @@ -69,7 +79,14 @@ public class ViewMediaFragment extends BaseFragment { attacher.setOnOutsidePhotoTapListener(new OnOutsidePhotoTapListener() { @Override public void onOutsidePhotoTap(ImageView imageView) { - onDismissListener.onDismiss(); + photoActionsListener.onDismiss(); + } + }); + + attacher.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + photoActionsListener.onPhotoTap(); } }); @@ -80,26 +97,82 @@ public class ViewMediaFragment extends BaseFragment { public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (Math.abs(velocityY) > Math.abs(velocityX)) { - onDismissListener.onDismiss(); + photoActionsListener.onDismiss(); return true; } return false; } }); - Picasso.with(getContext()) - .load(url) - .into(photoView, new Callback() { - @Override - public void onSuccess() { - rootView.findViewById(R.id.view_media_progress).setVisibility(View.GONE); - attacher.update(); - } + ViewCompat.setTransitionName(photoView, url); - @Override - public void onError() {} - }); + // If we are the view to be shown initially... + if (arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)) { + // Try to load image from disk. + Picasso.with(getContext()) + .load(url) + .noFade() + .networkPolicy(NetworkPolicy.OFFLINE) + .into(photoView, new Callback() { + @Override + public void onSuccess() { + // if we loaded image from disk, we should check that view is attached. + if (ViewCompat.isAttachedToWindow(photoView)) { + finishLoadingSuccessfully(); + } else { + // if view is not attached yet, wait for an attachment and + // start transition when it's finally ready. + photoView.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + finishLoadingSuccessfully(); + photoView.removeOnAttachStateChangeListener(this); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + } + } + + @Override + public void onError() { + // if there's no image in cache, load from network and start trnasition + // immediately. + getActivity().supportStartPostponedEnterTransition(); + loadImageFromNetwork(url, photoView); + } + }); + } else { + // if we're not initial page, don't bother. + loadImageFromNetwork(url, photoView); + } return rootView; } + + private void loadImageFromNetwork(String url, ImageView photoView) { + Picasso.with(getContext()) + .load(url) + .noPlaceholder() + .into(photoView, new Callback() { + @Override + public void onSuccess() { + finishLoadingSuccessfully(); + } + + @Override + public void onError() { + rootView.findViewById(R.id.view_media_progress).setVisibility(View.GONE); + } + }); + } + + private void finishLoadingSuccessfully() { + rootView.findViewById(R.id.view_media_progress).setVisibility(View.GONE); + attacher.update(); + getActivity().supportStartPostponedEnterTransition(); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 50069fae..8bc482ef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -199,8 +199,9 @@ public class ViewThreadFragment extends SFragment implements } @Override - public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type) { - super.viewMedia(urls, urlIndex, type); + public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + View view) { + super.viewMedia(urls, urlIndex, type, view); } @Override diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index 6bf13f15..fc6a7dcb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -25,7 +25,7 @@ public interface StatusActionListener extends LinkListener { void onReblog(final boolean reblog, final int position); void onFavourite(final boolean favourite, final int position); void onMore(View view, final int position); - void onViewMedia(String[] urls, int index, Status.MediaAttachment.Type type); + void onViewMedia(String[] urls, int index, Status.MediaAttachment.Type type, View view); void onViewThread(int position); void onOpenReblog(int position); void onExpandedChange(boolean expanded, int position); diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.java index 561dbee4..9f9511cb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.java @@ -10,16 +10,20 @@ import java.util.Locale; public class ImagePagerAdapter extends FragmentPagerAdapter { private String[] urls; + private FragmentManager fragmentManager; + private int initialPosition; - public ImagePagerAdapter(FragmentManager fragmentManager, String[] urls) { + public ImagePagerAdapter(FragmentManager fragmentManager, String[] urls, int initialPosition) { super(fragmentManager); this.urls = urls; + this.fragmentManager = fragmentManager; + this.initialPosition = initialPosition; } @Override public Fragment getItem(int position) { if (position >= 0 && position < urls.length) { - return ViewMediaFragment.newInstance(urls[position]); + return ViewMediaFragment.newInstance(urls[position], position == initialPosition); } else { return null; } diff --git a/app/src/main/res/layout/activity_view_video.xml b/app/src/main/res/layout/activity_view_video.xml index 0575d902..c20a559a 100644 --- a/app/src/main/res/layout/activity_view_video.xml +++ b/app/src/main/res/layout/activity_view_video.xml @@ -10,10 +10,9 @@ tools:context=".ViewVideoActivity"> + android:layout_gravity="center" />