Refactor media views (#866)
* Migrate ImagePagerAdapter to kotlin * Migrate ViewMediaFragment to kotlin * Make images and videos share the same activity/pager * Show descriptions above videos * Cleanup * Address code review feedback * Migrate media fragments to constraint layout
This commit is contained in:
parent
1556a88d05
commit
952d2a6512
16 changed files with 632 additions and 594 deletions
|
|
@ -30,7 +30,6 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewVideoActivity
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
|
@ -207,7 +206,9 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
val type = items[currentIndex].attachment.type
|
||||
|
||||
when (type) {
|
||||
Attachment.Type.IMAGE -> {
|
||||
Attachment.Type.IMAGE,
|
||||
Attachment.Type.GIFV,
|
||||
Attachment.Type.VIDEO -> {
|
||||
val intent = ViewMediaActivity.newIntent(context, items, currentIndex)
|
||||
if (view != null && activity != null) {
|
||||
val url = items[currentIndex].attachment.url
|
||||
|
|
@ -218,13 +219,6 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
Attachment.Type.GIFV, Attachment.Type.VIDEO -> {
|
||||
val intent = Intent(context, ViewVideoActivity::class.java)
|
||||
intent.putExtra(ViewVideoActivity.URL_EXTRA, items[currentIndex].attachment.url)
|
||||
intent.putExtra(ViewVideoActivity.STATUS_ID_EXTRA, items[currentIndex].statusId)
|
||||
intent.putExtra(ViewVideoActivity.STATUS_URL_EXTRA, items[currentIndex].statusUrl)
|
||||
startActivity(intent)
|
||||
}
|
||||
Attachment.Type.UNKNOWN -> {
|
||||
}/* Intentionally do nothing. This case is here is to handle when new attachment
|
||||
* types are added to the API before code is added here to handle them. So, the
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import com.keylesspalace.tusky.R;
|
|||
import com.keylesspalace.tusky.ReportActivity;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.ViewTagActivity;
|
||||
import com.keylesspalace.tusky.ViewVideoActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
|
|
@ -234,6 +233,8 @@ public abstract class SFragment extends BaseFragment {
|
|||
final Attachment active = actionable.getAttachments().get(urlIndex);
|
||||
Attachment.Type type = active.getType();
|
||||
switch (type) {
|
||||
case GIFV:
|
||||
case VIDEO:
|
||||
case IMAGE: {
|
||||
final List<AttachmentViewData> attachments = AttachmentViewData.list(actionable);
|
||||
final Intent intent = ViewMediaActivity.newIntent(getContext(), attachments,
|
||||
|
|
@ -250,15 +251,6 @@ public abstract class SFragment extends BaseFragment {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case GIFV:
|
||||
case VIDEO: {
|
||||
Intent intent = new Intent(getContext(), ViewVideoActivity.class);
|
||||
intent.putExtra(ViewVideoActivity.URL_EXTRA, active.getUrl());
|
||||
intent.putExtra(ViewVideoActivity.STATUS_ID_EXTRA, actionable.getId());
|
||||
intent.putExtra(ViewVideoActivity.STATUS_URL_EXTRA, actionable.getUrl());
|
||||
startActivity(intent);
|
||||
break;
|
||||
}
|
||||
case UNKNOWN: {
|
||||
/* Intentionally do nothing. This case is here is to handle when new attachment
|
||||
* types are added to the API before code is added here to handle them. So, the
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
|
||||
import com.github.chrisbanes.photoview.PhotoViewAttacher
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.squareup.picasso.Callback
|
||||
import com.squareup.picasso.NetworkPolicy
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.android.synthetic.main.activity_view_media.*
|
||||
import kotlinx.android.synthetic.main.fragment_view_image.*
|
||||
|
||||
class ViewImageFragment : ViewMediaFragment() {
|
||||
interface PhotoActionsListener {
|
||||
fun onBringUp()
|
||||
fun onDismiss()
|
||||
fun onPhotoTap()
|
||||
}
|
||||
|
||||
private lateinit var attacher: PhotoViewAttacher
|
||||
private lateinit var photoActionsListener: PhotoActionsListener
|
||||
private lateinit var toolbar: View
|
||||
|
||||
private var showingDescription = false
|
||||
private var isDescriptionVisible = false
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ViewImageFragment"
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
photoActionsListener = context as PhotoActionsListener
|
||||
}
|
||||
|
||||
override fun setupMediaView(url: String) {
|
||||
attacher = PhotoViewAttacher(photoView)
|
||||
|
||||
// Clicking outside the photo closes the viewer.
|
||||
attacher.setOnOutsidePhotoTapListener { _ -> photoActionsListener.onDismiss() }
|
||||
|
||||
attacher.setOnClickListener { _ -> onMediaTap() }
|
||||
|
||||
/* A vertical swipe motion also closes the viewer. This is especially useful when the photo
|
||||
* mostly fills the screen so clicking outside is difficult. */
|
||||
attacher.setOnSingleFlingListener { _, _, velocityX, velocityY ->
|
||||
var result = false
|
||||
if (Math.abs(velocityY) > Math.abs(velocityX)) {
|
||||
photoActionsListener.onDismiss()
|
||||
result = true
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// If we are the view to be shown initially...
|
||||
if (arguments!!.getBoolean(ViewMediaFragment.ARG_START_POSTPONED_TRANSITION)) {
|
||||
// Try to load image from disk.
|
||||
Picasso.with(context)
|
||||
.load(url)
|
||||
.noFade()
|
||||
.networkPolicy(NetworkPolicy.OFFLINE)
|
||||
.into(photoView, object : Callback {
|
||||
override fun 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(
|
||||
object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View?) {
|
||||
finishLoadingSuccessfully()
|
||||
photoView.removeOnAttachStateChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View?) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError() {
|
||||
// if there's no image in cache, load from network and start transition
|
||||
// immediately.
|
||||
photoActionsListener.onBringUp()
|
||||
loadImageFromNetwork(url, photoView)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// if we're not initial page, don't bother.
|
||||
loadImageFromNetwork(url, photoView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
toolbar = activity!!.toolbar
|
||||
return inflater.inflate(R.layout.fragment_view_image, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val arguments = this.arguments!!
|
||||
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
|
||||
val url: String?
|
||||
|
||||
if (attachment != null) {
|
||||
url = attachment.url
|
||||
|
||||
val description = attachment.description
|
||||
|
||||
descriptionView.text = description
|
||||
showingDescription = !TextUtils.isEmpty(description)
|
||||
isDescriptionVisible = showingDescription
|
||||
} else {
|
||||
url = arguments.getString(ARG_AVATAR_URL)
|
||||
if (url == null) {
|
||||
throw IllegalArgumentException("attachment or avatar url has to be set")
|
||||
}
|
||||
|
||||
showingDescription = false
|
||||
isDescriptionVisible = false
|
||||
}
|
||||
|
||||
// Setting visibility without animations so it looks nice when you scroll images
|
||||
if (showingDescription && (activity as ViewMediaActivity).isToolbarVisible()) {
|
||||
descriptionView.show()
|
||||
} else {
|
||||
descriptionView.hide()
|
||||
}
|
||||
|
||||
setupMediaView(url)
|
||||
|
||||
setupToolbarVisibilityListener()
|
||||
}
|
||||
|
||||
|
||||
private fun onMediaTap() {
|
||||
photoActionsListener.onPhotoTap()
|
||||
}
|
||||
|
||||
override fun onToolbarVisibilityChange(visible: Boolean) {
|
||||
if (photoView == null || !userVisibleHint) {
|
||||
return
|
||||
}
|
||||
isDescriptionVisible = showingDescription && visible
|
||||
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
|
||||
descriptionView.animate().alpha(alpha)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (isDescriptionVisible) {
|
||||
descriptionView.show()
|
||||
} else {
|
||||
descriptionView.hide()
|
||||
}
|
||||
animation.removeListener(this)
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
Picasso.with(context).cancelRequest(photoView)
|
||||
}
|
||||
|
||||
private fun loadImageFromNetwork(url: String, photoView: ImageView) {
|
||||
Picasso.with(context)
|
||||
.load(url)
|
||||
.noPlaceholder()
|
||||
.networkPolicy(NetworkPolicy.NO_STORE)
|
||||
.into(photoView, object : Callback {
|
||||
override fun onSuccess() {
|
||||
finishLoadingSuccessfully()
|
||||
}
|
||||
|
||||
override fun onError() {
|
||||
progressBar.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun finishLoadingSuccessfully() {
|
||||
progressBar.hide()
|
||||
attacher.update()
|
||||
photoActionsListener.onBringUp()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.github.chrisbanes.photoview.PhotoView;
|
||||
import com.github.chrisbanes.photoview.PhotoViewAttacher;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.squareup.picasso.Callback;
|
||||
import com.squareup.picasso.NetworkPolicy;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import kotlin.jvm.functions.Function0;
|
||||
|
||||
public final class ViewMediaFragment extends BaseFragment {
|
||||
public interface PhotoActionsListener {
|
||||
void onBringUp();
|
||||
|
||||
void onDismiss();
|
||||
|
||||
void onPhotoTap();
|
||||
}
|
||||
|
||||
private PhotoViewAttacher attacher;
|
||||
private PhotoActionsListener photoActionsListener;
|
||||
private View rootView;
|
||||
private PhotoView photoView;
|
||||
private TextView descriptionView;
|
||||
|
||||
private boolean showingDescription;
|
||||
private boolean isDescriptionVisible;
|
||||
private Function0 toolbarVisibiltyDisposable;
|
||||
|
||||
private static final String ARG_START_POSTPONED_TRANSITION = "startPostponedTransition";
|
||||
private static final String ARG_ATTACHMENT = "attach";
|
||||
private static final String ARG_AVATAR_URL = "avatarUrl";
|
||||
|
||||
public static ViewMediaFragment newInstance(@NonNull Attachment attachment,
|
||||
boolean shouldStartPostponedTransition) {
|
||||
Bundle arguments = new Bundle(2);
|
||||
ViewMediaFragment fragment = new ViewMediaFragment();
|
||||
arguments.putParcelable(ARG_ATTACHMENT, attachment);
|
||||
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition);
|
||||
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static ViewMediaFragment newAvatarInstance(@NonNull String avatarUrl) {
|
||||
Bundle arguments = new Bundle(2);
|
||||
ViewMediaFragment fragment = new ViewMediaFragment();
|
||||
arguments.putString(ARG_AVATAR_URL, avatarUrl);
|
||||
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true);
|
||||
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
photoActionsListener = (PhotoActionsListener) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
rootView = inflater.inflate(R.layout.fragment_view_media, container, false);
|
||||
photoView = rootView.findViewById(R.id.view_media_image);
|
||||
descriptionView = rootView.findViewById(R.id.tv_media_description);
|
||||
|
||||
final Bundle arguments = Objects.requireNonNull(getArguments(), "Empty arguments");
|
||||
final Attachment attachment = arguments.getParcelable(ARG_ATTACHMENT);
|
||||
final String url;
|
||||
|
||||
if(attachment != null) {
|
||||
url = attachment.getUrl();
|
||||
|
||||
@Nullable final String description = attachment.getDescription();
|
||||
|
||||
descriptionView.setText(description);
|
||||
showingDescription = !TextUtils.isEmpty(description);
|
||||
isDescriptionVisible = showingDescription;
|
||||
} else {
|
||||
url = arguments.getString(ARG_AVATAR_URL);
|
||||
if(url == null) {
|
||||
throw new IllegalArgumentException("attachment or avatar url has to be set");
|
||||
}
|
||||
|
||||
showingDescription = false;
|
||||
isDescriptionVisible = false;
|
||||
}
|
||||
|
||||
// Setting visibility without animations so it looks nice when you scroll images
|
||||
//noinspection ConstantConditions
|
||||
descriptionView.setVisibility(showingDescription
|
||||
&& (((ViewMediaActivity) getActivity())).isToolbarVisible()
|
||||
? View.VISIBLE : View.GONE);
|
||||
|
||||
attacher = new PhotoViewAttacher(photoView);
|
||||
|
||||
// Clicking outside the photo closes the viewer.
|
||||
attacher.setOnOutsidePhotoTapListener(imageView -> photoActionsListener.onDismiss());
|
||||
|
||||
attacher.setOnClickListener(v -> onMediaTap());
|
||||
|
||||
/* A vertical swipe motion also closes the viewer. This is especially useful when the photo
|
||||
* mostly fills the screen so clicking outside is difficult. */
|
||||
attacher.setOnSingleFlingListener((e1, e2, velocityX, velocityY) -> {
|
||||
if (Math.abs(velocityY) > Math.abs(velocityX)) {
|
||||
photoActionsListener.onDismiss();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
ViewCompat.setTransitionName(photoView, url);
|
||||
|
||||
// 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 transition
|
||||
// immediately.
|
||||
photoActionsListener.onBringUp();
|
||||
|
||||
loadImageFromNetwork(url, photoView);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// if we're not initial page, don't bother.
|
||||
loadImageFromNetwork(url, photoView);
|
||||
}
|
||||
|
||||
toolbarVisibiltyDisposable = ((ViewMediaActivity) getActivity())
|
||||
.addToolbarVisibilityListener(this::onToolbarVisibilityChange);
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (toolbarVisibiltyDisposable != null) toolbarVisibiltyDisposable.invoke();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void onMediaTap() {
|
||||
photoActionsListener.onPhotoTap();
|
||||
}
|
||||
|
||||
private void onToolbarVisibilityChange(boolean visible) {
|
||||
isDescriptionVisible = showingDescription && visible;
|
||||
final int visibility = isDescriptionVisible ? View.VISIBLE : View.INVISIBLE;
|
||||
int alpha = isDescriptionVisible ? 1 : 0;
|
||||
descriptionView.animate().alpha(alpha)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
descriptionView.setVisibility(visibility);
|
||||
animation.removeListener(this);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
Picasso.with(getContext())
|
||||
.cancelRequest(photoView);
|
||||
}
|
||||
|
||||
private void loadImageFromNetwork(String url, ImageView photoView) {
|
||||
Picasso.with(getContext())
|
||||
.load(url)
|
||||
.noPlaceholder()
|
||||
.networkPolicy(NetworkPolicy.NO_STORE)
|
||||
.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();
|
||||
photoActionsListener.onBringUp();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
|
||||
abstract class ViewMediaFragment : BaseFragment() {
|
||||
private var toolbarVisibiltyDisposable: Function0<Boolean>? = null
|
||||
|
||||
abstract fun setupMediaView(url: String)
|
||||
abstract fun onToolbarVisibilityChange(visible: Boolean)
|
||||
|
||||
companion object {
|
||||
@JvmStatic protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"
|
||||
@JvmStatic protected val ARG_ATTACHMENT = "attach"
|
||||
@JvmStatic protected val ARG_AVATAR_URL = "avatarUrl"
|
||||
private const val TAG = "ViewMediaFragment"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment {
|
||||
val arguments = Bundle(2)
|
||||
arguments.putParcelable(ARG_ATTACHMENT, attachment)
|
||||
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition)
|
||||
|
||||
val fragment = when (attachment.type) {
|
||||
Attachment.Type.IMAGE -> ViewImageFragment()
|
||||
Attachment.Type.VIDEO,
|
||||
Attachment.Type.GIFV -> ViewVideoFragment()
|
||||
else -> throw Exception("Unknown media type: $attachment")
|
||||
}
|
||||
fragment.arguments = arguments
|
||||
return fragment
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newAvatarInstance(avatarUrl: String): ViewMediaFragment {
|
||||
val arguments = Bundle(2)
|
||||
val fragment = ViewImageFragment()
|
||||
arguments.putString(ARG_AVATAR_URL, avatarUrl)
|
||||
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true)
|
||||
|
||||
fragment.arguments = arguments
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setupToolbarVisibilityListener() {
|
||||
toolbarVisibiltyDisposable = (activity as ViewMediaActivity).addToolbarVisibilityListener(object: ViewMediaActivity.ToolbarVisibilityListener {
|
||||
override fun onToolbarVisiblityChanged(isVisible: Boolean) {
|
||||
onToolbarVisibilityChange(isVisible)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
toolbarVisibiltyDisposable?.invoke()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.MediaController
|
||||
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import kotlinx.android.synthetic.main.activity_view_media.*
|
||||
import kotlinx.android.synthetic.main.fragment_view_video.*
|
||||
|
||||
class ViewVideoFragment : ViewMediaFragment() {
|
||||
private lateinit var toolbar: View
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val hideToolbar = Runnable {
|
||||
// Hoist toolbar hiding to activity so it can track state across different fragments
|
||||
// This is explicitly stored as runnable so that we pass it to the handler later for cancellation
|
||||
mediaActivity.onPhotoTap()
|
||||
}
|
||||
private lateinit var mediaActivity: ViewMediaActivity
|
||||
private val TOOLBAR_HIDE_DELAY_MS = 3000L
|
||||
|
||||
private var showingDescription = false
|
||||
private var isDescriptionVisible = false
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ViewVideoFragment"
|
||||
}
|
||||
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
// Start/pause/resume video playback as fragment is shown/hidden
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
if (videoPlayer == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isVisibleToUser) {
|
||||
if (mediaActivity.isToolbarVisible()) {
|
||||
handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS)
|
||||
}
|
||||
videoPlayer?.start()
|
||||
} else {
|
||||
handler.removeCallbacks(hideToolbar)
|
||||
videoPlayer?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun setupMediaView(url: String) {
|
||||
val videoView = videoPlayer
|
||||
videoView.setVideoPath(url)
|
||||
val controller = MediaController(mediaActivity)
|
||||
controller.setMediaPlayer(videoView)
|
||||
videoView.setMediaController(controller)
|
||||
videoView.requestFocus()
|
||||
videoView.setOnTouchListener { _, _ ->
|
||||
mediaActivity.onPhotoTap()
|
||||
false
|
||||
}
|
||||
videoView.setOnPreparedListener { mp ->
|
||||
progressBar.hide()
|
||||
mp.isLooping = true
|
||||
if (arguments!!.getBoolean(ViewMediaFragment.ARG_START_POSTPONED_TRANSITION)) {
|
||||
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
|
||||
videoView.start()
|
||||
}
|
||||
}
|
||||
|
||||
if (arguments!!.getBoolean(ViewMediaFragment.ARG_START_POSTPONED_TRANSITION)) {
|
||||
mediaActivity.onBringUp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideToolbarAfterDelay(delayMilliseconds: Long) {
|
||||
handler.postDelayed(hideToolbar, delayMilliseconds)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
toolbar = activity!!.toolbar
|
||||
mediaActivity = activity as ViewMediaActivity
|
||||
return inflater.inflate(R.layout.fragment_view_video, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val arguments = this.arguments!!
|
||||
val attachment = arguments.getParcelable<Attachment>(ViewMediaFragment.ARG_ATTACHMENT)
|
||||
val url: String
|
||||
|
||||
if (attachment == null) {
|
||||
throw IllegalArgumentException("attachment has to be set")
|
||||
}
|
||||
url = attachment.url
|
||||
val description = attachment.description
|
||||
mediaDescription.text = description
|
||||
showingDescription = !TextUtils.isEmpty(description)
|
||||
isDescriptionVisible = showingDescription
|
||||
|
||||
// Setting visibility without animations so it looks nice when you scroll media
|
||||
//noinspection ConstantConditions
|
||||
if (showingDescription && mediaActivity.isToolbarVisible()) {
|
||||
mediaDescription.show()
|
||||
} else {
|
||||
mediaDescription.hide()
|
||||
|
||||
}
|
||||
|
||||
ViewCompat.setTransitionName(videoPlayer!!, url)
|
||||
|
||||
setupMediaView(url)
|
||||
|
||||
setupToolbarVisibilityListener()
|
||||
}
|
||||
|
||||
override fun onToolbarVisibilityChange(visible: Boolean) {
|
||||
if (videoPlayer == null || !userVisibleHint) {
|
||||
return
|
||||
}
|
||||
|
||||
isDescriptionVisible = showingDescription && visible
|
||||
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
|
||||
mediaDescription.animate().alpha(alpha)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (isDescriptionVisible) {
|
||||
mediaDescription.show()
|
||||
} else {
|
||||
mediaDescription.hide()
|
||||
}
|
||||
animation.removeListener(this)
|
||||
}
|
||||
})
|
||||
.start()
|
||||
|
||||
if (visible) {
|
||||
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
|
||||
} else {
|
||||
handler.removeCallbacks(hideToolbar)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue