Albums scrollable and CW with blurhash

This commit is contained in:
Matthieu 2021-03-03 14:00:44 +01:00
parent 7631e8c1f9
commit 1778f775b2
7 changed files with 224 additions and 123 deletions

View File

@ -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)
}
}
}
}
}
}

View File

@ -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
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?.sensitive!!) {
status?.setupSensitiveLayout(binding)
}
}
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())
binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList(), status?.sensitive)
TabLayoutMediator(binding.postTabs, binding.postPager) { tab, _ ->
tab.icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_dot_blue_12dp)
}.attach()
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 == 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 setupSensitiveLayout() {
// 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,20 +690,29 @@ 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 {
media_attachments[position].apply {
val blurhashBitMap = blurhash?.let {
BlurHashDecoder.blurHashBitmap(
holder.binding.root.resources,
it,
media_attachments[position].meta?.original?.width,
media_attachments[position].meta?.original?.height)
}
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()
@ -730,6 +721,12 @@ class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) :
holder.image.contentDescription = description
}
}
fun uncensor(){
sensitive = false
notifyDataSetChanged()
}
class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){
val image: ImageView = binding.imageImageView

View File

@ -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

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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 -->