Albums scrollable and CW with blurhash
This commit is contained in:
parent
7631e8c1f9
commit
1778f775b2
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.h.pixeldroid.posts
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.FrameLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
|
||||
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
|
||||
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
|
||||
*
|
||||
* This solution has limitations when using multiple levels of nested scrollable elements
|
||||
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
|
||||
*/
|
||||
class NestedScrollableHost : ConstraintLayout {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
private var touchSlop = 0
|
||||
private var initialX = 0f
|
||||
private var initialY = 0f
|
||||
private val parentViewPager: ViewPager2?
|
||||
get() {
|
||||
var v: View? = parent as? View
|
||||
while (v != null && v !is ViewPager2) {
|
||||
v = v.parent as? View
|
||||
}
|
||||
return v as? ViewPager2
|
||||
}
|
||||
|
||||
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
|
||||
|
||||
init {
|
||||
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
}
|
||||
|
||||
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
|
||||
val direction = -delta.sign.toInt()
|
||||
return when (orientation) {
|
||||
0 -> child?.canScrollHorizontally(direction) ?: false
|
||||
1 -> child?.canScrollVertically(direction) ?: false
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||
handleInterceptTouchEvent(e)
|
||||
return super.onInterceptTouchEvent(e)
|
||||
}
|
||||
|
||||
private fun handleInterceptTouchEvent(e: MotionEvent) {
|
||||
val orientation = parentViewPager?.orientation ?: return
|
||||
|
||||
// Early return if child can't scroll in same direction as parent
|
||||
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.action == MotionEvent.ACTION_DOWN) {
|
||||
initialX = e.x
|
||||
initialY = e.y
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
} else if (e.action == MotionEvent.ACTION_MOVE) {
|
||||
val dx = e.x - initialX
|
||||
val dy = e.y - initialY
|
||||
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
|
||||
|
||||
// assuming ViewPager2 touch-slop is 2x touch-slop of child
|
||||
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
|
||||
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
|
||||
|
||||
if (scaledDx > touchSlop || scaledDy > touchSlop) {
|
||||
if (isVpHorizontal == (scaledDy > scaledDx)) {
|
||||
// Gesture is perpendicular, allow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
} else {
|
||||
// Gesture is parallel, query child if movement in that direction is possible
|
||||
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
|
||||
// Child can scroll, disallow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
} else {
|
||||
// Child cannot scroll, allow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.databinding.AlbumImageViewBinding
|
||||
import com.h.pixeldroid.databinding.CommentBinding
|
||||
|
@ -28,7 +27,6 @@ import com.h.pixeldroid.utils.api.PixelfedAPI
|
|||
import com.h.pixeldroid.utils.api.objects.Attachment
|
||||
import com.h.pixeldroid.utils.api.objects.Status
|
||||
import com.h.pixeldroid.utils.db.AppDatabase
|
||||
import com.h.pixeldroid.utils.displayDimensionsInPx
|
||||
import com.karumi.dexter.Dexter
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse
|
||||
import com.karumi.dexter.listener.PermissionGrantedResponse
|
||||
|
@ -60,12 +58,13 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
}?.maxOrNull() ?: 1f
|
||||
|
||||
val (displayWidth, displayHeight) = displayDimensionsInPx
|
||||
val height = if (displayWidth / maxImageRatio > displayHeight * 3/4f) {
|
||||
binding.postPicture.layoutParams.width = ((displayHeight * 3 / 4f) * maxImageRatio).roundToInt()
|
||||
displayHeight * 3 / 4f
|
||||
} else displayWidth / maxImageRatio
|
||||
|
||||
binding.postPicture.layoutParams.height = height.toInt()
|
||||
if (displayWidth / maxImageRatio > displayHeight * 3/4f) {
|
||||
binding.postPager.layoutParams.width = ((displayHeight * 3 / 4f) * maxImageRatio).roundToInt()
|
||||
binding.postPager.layoutParams.height = (displayHeight * 3 / 4f).toInt()
|
||||
} else {
|
||||
binding.postPager.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
binding.postPager.layoutParams.height = (displayWidth / maxImageRatio).toInt()
|
||||
}
|
||||
|
||||
//Setup the post layout
|
||||
val picRequest = Glide.with(itemView)
|
||||
|
@ -128,12 +127,10 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
if(!status?.media_attachments.isNullOrEmpty()) {
|
||||
setupPostPics(binding, request)
|
||||
} else {
|
||||
binding.postPicture.visibility = View.GONE
|
||||
binding.postPager.visibility = View.GONE
|
||||
binding.postTabs.visibility = View.GONE
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
//Set comment initial visibility
|
||||
binding.commentIn.visibility = View.GONE
|
||||
binding.commentContainer.visibility = View.GONE
|
||||
|
@ -145,56 +142,41 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
) {
|
||||
|
||||
// Standard layout
|
||||
binding.postPicture.visibility = View.VISIBLE
|
||||
binding.postPager.visibility = View.GONE
|
||||
binding.postTabs.visibility = View.GONE
|
||||
binding.postPager.visibility = View.VISIBLE
|
||||
|
||||
//Attach the given tabs to the view pager
|
||||
binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList(), status?.sensitive)
|
||||
|
||||
if(status?.media_attachments?.size == 1) {
|
||||
request.placeholder(
|
||||
status?.media_attachments?.get(0).let {
|
||||
it?.blurhash?.let { hash ->
|
||||
BlurHashDecoder.blurHashBitmap(binding.root.resources,
|
||||
hash,
|
||||
it.meta?.original?.width,
|
||||
it.meta?.original?.height
|
||||
)
|
||||
}
|
||||
}
|
||||
).load(status?.getPostUrl()).into(binding.postPicture)
|
||||
val imgDescription = status?.media_attachments?.get(0)?.description.orEmpty().ifEmpty { binding.root.context.getString(
|
||||
R.string.no_description) }
|
||||
binding.postPicture.contentDescription = imgDescription
|
||||
|
||||
binding.postPicture.setOnLongClickListener {
|
||||
Snackbar.make(it, imgDescription, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
} else if(status?.media_attachments?.size!! > 1) {
|
||||
setupTabsLayout(binding, request)
|
||||
if(status?.media_attachments?.size ?: 0 > 1) {
|
||||
binding.postIndicator.setViewPager(binding.postPager)
|
||||
binding.postIndicator.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (status?.sensitive!!) {
|
||||
status?.setupSensitiveLayout(binding)
|
||||
if (status?.sensitive == true) {
|
||||
setupSensitiveLayout()
|
||||
} else {
|
||||
// GONE is the default, but have to set it again because of how RecyclerViews work
|
||||
binding.sensitiveWarning.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTabsLayout(
|
||||
binding: PostFragmentBinding,
|
||||
request: RequestBuilder<Drawable>,
|
||||
) {
|
||||
//Only show the viewPager and tabs
|
||||
binding.postPicture.visibility = View.GONE
|
||||
binding.postPager.visibility = View.VISIBLE
|
||||
binding.postTabs.visibility = View.VISIBLE
|
||||
|
||||
//Attach the given tabs to the view pager
|
||||
binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList())
|
||||
private fun setupSensitiveLayout() {
|
||||
|
||||
TabLayoutMediator(binding.postTabs, binding.postPager) { tab, _ ->
|
||||
tab.icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_dot_blue_12dp)
|
||||
}.attach()
|
||||
// Set dark layout and warning message
|
||||
binding.sensitiveWarning.visibility = View.VISIBLE
|
||||
//binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix)
|
||||
|
||||
fun uncensorPicture(binding: PostFragmentBinding) {
|
||||
binding.sensitiveWarning.visibility = View.GONE
|
||||
(binding.postPager.adapter as AlbumViewPagerAdapter).uncensor()
|
||||
}
|
||||
|
||||
binding.sensitiveWarning.setOnClickListener {
|
||||
uncensorPicture(binding)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDescription(
|
||||
|
@ -459,7 +441,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
|
||||
//Activate double tap liking
|
||||
var clicked = false
|
||||
binding.postPicture.setOnClickListener {
|
||||
binding.postPager.setOnClickListener {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
//Check that the post isn't hidden
|
||||
if(binding.sensitiveWarning.visibility == View.GONE) {
|
||||
|
@ -479,7 +461,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
clicked = true
|
||||
|
||||
//Reset clicked to false after 500ms
|
||||
binding.postPicture.handler.postDelayed(fun() { clicked = false }, 500)
|
||||
binding.postPager.handler.postDelayed(fun() { clicked = false }, 500)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -696,7 +678,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
}
|
||||
}
|
||||
|
||||
class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) :
|
||||
private class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>, private var sensitive: Boolean?) :
|
||||
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
|
@ -708,27 +690,42 @@ class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) :
|
|||
|
||||
override fun getItemCount() = media_attachments.size
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
Glide.with(holder.binding.root)
|
||||
.asDrawable().fitCenter().placeholder(
|
||||
media_attachments[position].blurhash?.let {
|
||||
BlurHashDecoder.blurHashBitmap(
|
||||
holder.binding.root.resources,
|
||||
it,
|
||||
media_attachments[position].meta?.original?.width,
|
||||
media_attachments[position].meta?.original?.height)
|
||||
}
|
||||
media_attachments[position].apply {
|
||||
val blurhashBitMap = blurhash?.let {
|
||||
BlurHashDecoder.blurHashBitmap(
|
||||
holder.binding.root.resources,
|
||||
it,
|
||||
meta?.original?.width,
|
||||
meta?.original?.height
|
||||
)
|
||||
.load(media_attachments[position].url).into(holder.image)
|
||||
}
|
||||
if (sensitive == false) {
|
||||
Glide.with(holder.binding.root)
|
||||
.asDrawable().fitCenter()
|
||||
.placeholder(blurhashBitMap)
|
||||
.load(url).into(holder.image)
|
||||
} else {
|
||||
Glide.with(holder.binding.root)
|
||||
.asDrawable().fitCenter()
|
||||
.load(blurhashBitMap).into(holder.image)
|
||||
}
|
||||
|
||||
val description = media_attachments[position].description
|
||||
.orEmpty().ifEmpty{ holder.binding.root.context.getString(R.string.no_description)}
|
||||
val description = description
|
||||
.orEmpty()
|
||||
.ifEmpty { holder.binding.root.context.getString(R.string.no_description) }
|
||||
|
||||
holder.image.setOnLongClickListener {
|
||||
Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
holder.image.setOnLongClickListener {
|
||||
Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
holder.image.contentDescription = description
|
||||
}
|
||||
}
|
||||
|
||||
holder.image.contentDescription = description
|
||||
fun uncensor(){
|
||||
sensitive = false
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){
|
||||
|
|
|
@ -96,29 +96,6 @@ open class Status(
|
|||
|
||||
}
|
||||
|
||||
fun setupSensitiveLayout(binding: PostFragmentBinding) {
|
||||
|
||||
// Set dark layout and warning message
|
||||
binding.sensitiveWarning.visibility = VISIBLE
|
||||
val array = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f, 0f)
|
||||
val censorMatrix = ColorMatrix(array)
|
||||
binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix)
|
||||
|
||||
fun uncensorPicture(binding: PostFragmentBinding) {
|
||||
binding.sensitiveWarning.visibility = GONE
|
||||
binding.postPicture.clearColorFilter()
|
||||
}
|
||||
|
||||
|
||||
binding.sensitiveWarning.setOnClickListener {
|
||||
uncensorPicture(binding)
|
||||
}
|
||||
|
||||
binding.postPicture.setOnClickListener {
|
||||
uncensorPicture(binding)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadImage(context: Context, url: String, view: View, share: Boolean = false) {
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#BE505050" />
|
||||
|
||||
<padding
|
||||
android:left="4dp"
|
||||
android:right="4dp"
|
||||
android:bottom="4dp"
|
||||
android:top="4dp" />
|
||||
|
||||
<corners android:radius="5dp" />
|
||||
</shape>
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/imageImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:adjustViewBounds="true"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
|
|
|
@ -56,58 +56,59 @@
|
|||
android:layout_marginTop="10dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/profilePic">
|
||||
|
||||
<com.h.pixeldroid.posts.NestedScrollableHost
|
||||
android:id="@+id/postPagerHost"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/postPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/postTabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/postPager"
|
||||
app:tabMode="auto" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/postPicture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@color/browser_actions_bg_grey"
|
||||
tools:ignore="ContentDescription" />
|
||||
android:orientation="horizontal" />
|
||||
|
||||
</com.h.pixeldroid.posts.NestedScrollableHost>
|
||||
|
||||
<me.relex.circleindicator.CircleIndicator3
|
||||
android:id="@+id/postIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="32dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/post_fragment_image_popup_menu_anchor"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="1dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPicture"
|
||||
app:layout_constraintEnd_toEndOf="@+id/postPicture"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||
app:layout_constraintEnd_toEndOf="@+id/postPagerHost"
|
||||
app:layout_constraintHorizontal_bias="0.1"
|
||||
app:layout_constraintStart_toStartOf="@+id/postPicture"
|
||||
app:layout_constraintTop_toTopOf="@+id/postPicture"
|
||||
app:layout_constraintStart_toStartOf="@+id/postPagerHost"
|
||||
app:layout_constraintTop_toTopOf="@+id/postPagerHost"
|
||||
app:layout_constraintVertical_bias="0.1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sensitiveWarning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:gravity="center|center_horizontal|center_vertical"
|
||||
android:text="@string/cw_nsfw_hidden_media_n_click_to_show"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:textColor="@color/ic_launcher_background"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPicture"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/postTabs"
|
||||
app:layout_constraintTop_toTopOf="@+id/postPagerHost"
|
||||
tools:src="@color/browser_actions_bg_grey" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -163,7 +163,7 @@ Following"</item>
|
|||
<string name="accounts">ACCOUNTS</string>
|
||||
<string name="hashtags">HASHTAGS</string>
|
||||
<!-- Sensitive media -->
|
||||
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Hidden Media \n (click to show)</string>
|
||||
<string name="cw_nsfw_hidden_media_n_click_to_show">CW / NSFW / Hidden Media\n (click to show)</string>
|
||||
<!-- Shown when image has finished uploading. {gmd_cloud_done} is an icon, position it as is appropriate in target language -->
|
||||
<string name="media_upload_completed">{gmd_cloud_done} Media upload completed</string>
|
||||
<!-- Shown when image uploading has failed. {gmd_cloud_off} is an icon, position it as is appropriate in target language -->
|
||||
|
|
Loading…
Reference in New Issue