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 layoutmain
parent
fd0f5edeef
commit
1deaaa1144
@ -1,185 +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 |
|
||||||
|
|
||||||
import android.animation.Animator |
|
||||||
import android.animation.AnimatorListenerAdapter |
|
||||||
import android.annotation.SuppressLint |
|
||||||
import android.app.DownloadManager |
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import android.content.pm.PackageManager |
|
||||||
import android.graphics.Color |
|
||||||
import android.net.Uri |
|
||||||
import android.os.Bundle |
|
||||||
import android.os.Handler |
|
||||||
import android.os.Looper |
|
||||||
import android.support.v4.content.FileProvider |
|
||||||
import android.util.Log |
|
||||||
import android.view.Menu |
|
||||||
import android.view.MenuItem |
|
||||||
import android.view.MotionEvent |
|
||||||
import android.view.View |
|
||||||
import android.webkit.MimeTypeMap |
|
||||||
import android.widget.MediaController |
|
||||||
|
|
||||||
import kotlinx.android.synthetic.main.activity_view_video.* |
|
||||||
|
|
||||||
import java.io.File |
|
||||||
|
|
||||||
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID |
|
||||||
import com.keylesspalace.tusky.util.getTemporaryMediaFilename |
|
||||||
import com.keylesspalace.tusky.util.hide |
|
||||||
import com.keylesspalace.tusky.util.show |
|
||||||
|
|
||||||
class ViewVideoActivity: BaseActivity() { |
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper()) |
|
||||||
private lateinit var url: String |
|
||||||
private lateinit var statusID: String |
|
||||||
private lateinit var statusURL: String |
|
||||||
|
|
||||||
companion object { |
|
||||||
private const val TAG = "ViewVideoActivity" |
|
||||||
const val URL_EXTRA = "url" |
|
||||||
const val STATUS_ID_EXTRA = "statusID" |
|
||||||
const val STATUS_URL_EXTRA = "statusURL" |
|
||||||
} |
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility") |
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
setContentView(R.layout.activity_view_video) |
|
||||||
|
|
||||||
setSupportActionBar(toolbar) |
|
||||||
val bar = supportActionBar |
|
||||||
if (bar != null) { |
|
||||||
bar.title = null |
|
||||||
bar.setDisplayHomeAsUpEnabled(true) |
|
||||||
bar.setDisplayShowHomeEnabled(true) |
|
||||||
} |
|
||||||
toolbar.setOnMenuItemClickListener {item -> |
|
||||||
val id = item.itemId |
|
||||||
when (id) { |
|
||||||
R.id.action_download -> downloadFile(url) |
|
||||||
R.id.action_open_status -> onOpenStatus() |
|
||||||
R.id.action_share_media -> shareVideo() |
|
||||||
} |
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
url = intent.getStringExtra(URL_EXTRA) |
|
||||||
statusID = intent.getStringExtra(STATUS_ID_EXTRA) |
|
||||||
statusURL = intent.getStringExtra(STATUS_URL_EXTRA) |
|
||||||
|
|
||||||
videoPlayer.setVideoPath(url) |
|
||||||
val controller = MediaController(this) |
|
||||||
controller.setMediaPlayer(videoPlayer) |
|
||||||
videoPlayer.setMediaController(controller) |
|
||||||
videoPlayer.requestFocus() |
|
||||||
videoPlayer.setOnPreparedListener { mp -> |
|
||||||
videoProgressBar.hide() |
|
||||||
mp.isLooping = true |
|
||||||
hideToolbarAfterDelay() |
|
||||||
} |
|
||||||
videoPlayer.start() |
|
||||||
|
|
||||||
videoPlayer.setOnTouchListener { _, event -> |
|
||||||
if (event.action == MotionEvent.ACTION_DOWN) { |
|
||||||
handler.removeCallbacksAndMessages(null) |
|
||||||
toolbar.animate().cancel() |
|
||||||
toolbar.alpha = 1.0f |
|
||||||
toolbar.show() |
|
||||||
hideToolbarAfterDelay() |
|
||||||
} |
|
||||||
false |
|
||||||
} |
|
||||||
|
|
||||||
window.statusBarColor = Color.BLACK |
|
||||||
} |
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean { |
|
||||||
when (item.itemId) { |
|
||||||
android.R.id.home -> { |
|
||||||
onBackPressed() |
|
||||||
return true |
|
||||||
} |
|
||||||
} |
|
||||||
return super.onOptionsItemSelected(item) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean { |
|
||||||
menuInflater.inflate(R.menu.view_media_toolbar, menu) |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { |
|
||||||
when (requestCode) { |
|
||||||
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE -> { |
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
|
||||||
downloadFile(url) |
|
||||||
} else { |
|
||||||
showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { _ -> downloadFile(url) } |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun hideToolbarAfterDelay() { |
|
||||||
handler.postDelayed({ |
|
||||||
toolbar.animate().alpha(0.0f).setListener(object: AnimatorListenerAdapter() { |
|
||||||
override fun onAnimationEnd(animation: Animator) { |
|
||||||
val decorView = window.decorView |
|
||||||
val uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE |
|
||||||
decorView.systemUiVisibility = uiOptions |
|
||||||
toolbar.hide() |
|
||||||
animation.removeListener(this) |
|
||||||
} |
|
||||||
}) |
|
||||||
}, 3000) |
|
||||||
} |
|
||||||
|
|
||||||
private fun onOpenStatus() { |
|
||||||
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, statusID, statusURL)) |
|
||||||
} |
|
||||||
|
|
||||||
private fun shareVideo() { |
|
||||||
val directory = applicationContext.getExternalFilesDir("Tusky") |
|
||||||
if (directory == null || !(directory.exists())) { |
|
||||||
Log.e(TAG, "Error obtaining directory to save temporary media.") |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
val uri = Uri.parse(url) |
|
||||||
val mimeTypeMap = MimeTypeMap.getSingleton() |
|
||||||
val extension = MimeTypeMap.getFileExtensionFromUrl(url) |
|
||||||
val mimeType = mimeTypeMap.getMimeTypeFromExtension(extension) |
|
||||||
val filename = getTemporaryMediaFilename(extension) |
|
||||||
val file = File(directory, filename) |
|
||||||
|
|
||||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager |
|
||||||
val request = DownloadManager.Request(uri) |
|
||||||
request.setDestinationUri(Uri.fromFile(file)) |
|
||||||
request.setVisibleInDownloadsUi(false) |
|
||||||
downloadManager.enqueue(request) |
|
||||||
|
|
||||||
val sendIntent = Intent() |
|
||||||
sendIntent.action = Intent.ACTION_SEND |
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)) |
|
||||||
sendIntent.type = mimeType |
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to))) |
|
||||||
} |
|
||||||
} |
|
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,42 +0,0 @@ |
|||||||
package com.keylesspalace.tusky.pager; |
|
||||||
|
|
||||||
import android.support.v4.app.Fragment; |
|
||||||
import android.support.v4.app.FragmentManager; |
|
||||||
import android.support.v4.app.FragmentPagerAdapter; |
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment; |
|
||||||
import com.keylesspalace.tusky.fragment.ViewMediaFragment; |
|
||||||
|
|
||||||
import java.util.List; |
|
||||||
import java.util.Locale; |
|
||||||
|
|
||||||
public final class ImagePagerAdapter extends FragmentPagerAdapter { |
|
||||||
|
|
||||||
private List<Attachment> attachments; |
|
||||||
private int initialPosition; |
|
||||||
|
|
||||||
public ImagePagerAdapter(FragmentManager fragmentManager, List<Attachment> attachments, int initialPosition) { |
|
||||||
super(fragmentManager); |
|
||||||
this.attachments = attachments; |
|
||||||
this.initialPosition = initialPosition; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public Fragment getItem(int position) { |
|
||||||
if (position >= 0 && position < attachments.size()) { |
|
||||||
return ViewMediaFragment.newInstance(attachments.get(position), position == initialPosition); |
|
||||||
} else { |
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public int getCount() { |
|
||||||
return attachments.size(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public CharSequence getPageTitle(int position) { |
|
||||||
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments.size()); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,33 @@ |
|||||||
|
package com.keylesspalace.tusky.pager |
||||||
|
|
||||||
|
import android.support.v4.app.Fragment |
||||||
|
import android.support.v4.app.FragmentManager |
||||||
|
import android.support.v4.app.FragmentStatePagerAdapter |
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment |
||||||
|
import com.keylesspalace.tusky.fragment.ViewMediaFragment |
||||||
|
|
||||||
|
import java.util.Locale |
||||||
|
|
||||||
|
class ImagePagerAdapter( |
||||||
|
fragmentManager: FragmentManager, |
||||||
|
private val attachments: List<Attachment>, |
||||||
|
private val initialPosition: Int |
||||||
|
) : FragmentStatePagerAdapter(fragmentManager) { |
||||||
|
|
||||||
|
override fun getItem(position: Int): Fragment? { |
||||||
|
return if (position >= 0 && position < attachments.size) { |
||||||
|
ViewMediaFragment.newInstance(attachments[position], position == initialPosition) |
||||||
|
} else { |
||||||
|
null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun getCount(): Int { |
||||||
|
return attachments.size |
||||||
|
} |
||||||
|
|
||||||
|
override fun getPageTitle(position: Int): CharSequence { |
||||||
|
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments.size) |
||||||
|
} |
||||||
|
} |
@ -1,28 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||||
xmlns:tools="http://schemas.android.com/tools" |
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|
||||||
android:orientation="vertical" |
|
||||||
android:layout_width="match_parent" |
|
||||||
android:layout_height="match_parent" |
|
||||||
android:background="@color/view_video_background" |
|
||||||
android:id="@+id/view_video_container" |
|
||||||
tools:context=".ViewVideoActivity"> |
|
||||||
<VideoView |
|
||||||
android:id="@+id/videoPlayer" |
|
||||||
android:layout_width="match_parent" |
|
||||||
android:layout_height="match_parent" |
|
||||||
android:layout_gravity="center" /> |
|
||||||
<ProgressBar |
|
||||||
android:id="@+id/videoProgressBar" |
|
||||||
android:layout_gravity="center" |
|
||||||
android:layout_width="wrap_content" |
|
||||||
android:layout_height="wrap_content" /> |
|
||||||
<android.support.v7.widget.Toolbar |
|
||||||
android:id="@+id/toolbar" |
|
||||||
android:layout_width="match_parent" |
|
||||||
android:layout_height="?attr/actionBarSize" |
|
||||||
android:theme="@style/AppTheme.Account.AppBarLayout" |
|
||||||
app:popupTheme="?attr/toolbar_popup_theme" |
|
||||||
android:background="@color/toolbar_view_media" /> |
|
||||||
</android.support.design.widget.CoordinatorLayout> |
|
@ -0,0 +1,46 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:tools="http://schemas.android.com/tools" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
|
android:layout_gravity="center" |
||||||
|
android:clickable="true" |
||||||
|
android:focusable="true"> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/mediaDescription" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
app:layout_constraintTop_toBottomOf="@+id/toolbar" |
||||||
|
app:layout_constraintBottom_toTopOf="@+id/videoPlayer" |
||||||
|
android:background="#60000000" |
||||||
|
android:lineSpacingMultiplier="1.1" |
||||||
|
android:padding="8dp" |
||||||
|
android:textAlignment="center" |
||||||
|
android:textColor="#eee" |
||||||
|
android:textSize="?attr/status_text_medium" |
||||||
|
tools:text="Some media description" /> |
||||||
|
|
||||||
|
<VideoView |
||||||
|
android:id="@+id/videoPlayer" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
app:layout_constraintLeft_toLeftOf="parent" |
||||||
|
app:layout_constraintRight_toRightOf="parent" |
||||||
|
app:layout_constraintTop_toTopOf="parent" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
android:layout_gravity="center" |
||||||
|
/> |
||||||
|
|
||||||
|
<ProgressBar |
||||||
|
android:id="@+id/progressBar" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
app:layout_constraintLeft_toLeftOf="parent" |
||||||
|
app:layout_constraintRight_toRightOf="parent" |
||||||
|
app:layout_constraintTop_toTopOf="parent" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
android:layout_gravity="center" /> |
||||||
|
|
||||||
|
</android.support.constraint.ConstraintLayout> |
Loading…
Reference in new issue