投稿画面で添付メディアを並べ替え

This commit is contained in:
tateisu 2023-11-26 09:47:24 +09:00
parent b93d912336
commit 729b10143f
11 changed files with 399 additions and 7 deletions

View File

@ -22,6 +22,7 @@ import jp.juggler.subwaytooter.actpost.CompletionHelper
import jp.juggler.subwaytooter.actpost.FeaturedTagCache
import jp.juggler.subwaytooter.actpost.addAttachment
import jp.juggler.subwaytooter.actpost.applyMushroomText
import jp.juggler.subwaytooter.actpost.rearrangeAttachments
import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl
import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl
import jp.juggler.subwaytooter.actpost.openAttachment
@ -314,7 +315,7 @@ class ActPost : AppCompatActivity(),
R.id.btnFeaturedTag -> completionHelper.openFeaturedTagList(
featuredTagCache[account?.acct?.ascii ?: ""]?.list
)
R.id.btnAttachmentsRearrange -> rearrangeAttachments()
R.id.ibSchedule -> performSchedule()
R.id.ibScheduleReset -> resetSchedule()
}
@ -445,6 +446,7 @@ class ActPost : AppCompatActivity(),
views.btnEmojiPicker,
views.btnMore,
views.ivAccount,
views.btnAttachmentsRearrange,
).forEach { it.setOnClickListener(this) }
ivMedia.forEach { it.setOnClickListener(this) }

View File

@ -5,6 +5,7 @@ import android.net.Uri
import android.text.InputType
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiTask
@ -20,6 +21,7 @@ import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.defaultColorIcon
import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.decodeAttachmentBitmap
import jp.juggler.subwaytooter.dialog.dialogArrachmentRearrange
import jp.juggler.subwaytooter.dialog.focusPointDialog
import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.pref.PrefB
@ -40,6 +42,8 @@ import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.isLiveActivity
import jp.juggler.util.ui.vg
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlin.math.min
private val log = LogCategory("ActPostAttachment")
@ -64,13 +68,23 @@ fun ActPost.decodeAttachments(sv: String) {
}
}
fun ActPost.showAttachmentRearrangeButton() {
views.btnAttachmentsRearrange.vg(
attachmentList.size >= 2 &&
attachmentList.none { it.status == PostAttachment.Status.Progress }
)
}
fun ActPost.showMediaAttachment() {
if (isFinishing) return
views.llAttachment.vg(attachmentList.isNotEmpty())
ivMedia.forEachIndexed { i, v -> showMediaAttachmentOne(v, i) }
showAttachmentRearrangeButton()
}
fun ActPost.showMediaAttachmentProgress() {
if (isFinishing) return
showAttachmentRearrangeButton()
val mergedProgress = attachmentList
.mapNotNull { it.progress.notEmpty() }
.joinToString("\n")
@ -436,3 +450,32 @@ fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultE
}
}
}
fun ActPost.rearrangeAttachments() = lifecycleScope.launch {
try {
val rearranged = dialogArrachmentRearrange(attachmentList)
// 入れ替え中にアップロード失敗などで要素が消えることがあるので
// 最新のattachmentListを指定順に並べ替える
val remain = ArrayList(attachmentList)
val newList = buildList {
rearranged.map { a ->
val pa = remain.find { it === a }
if (pa != null) {
add(pa)
remain.remove(pa)
}
}
addAll(remain)
}
// attachmentListを更新して表示し直す
attachmentList.clear()
attachmentList.addAll(newList)
showMediaAttachment()
showMediaAttachmentProgress()
} catch (ex: Throwable) {
log.e(ex, "attachmentRearrange failed.")
if (ex !is CancellationException) {
dialogOrToast(ex.withCaption("attachmentRearrange failed."))
}
}
}

View File

@ -0,0 +1,205 @@
package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.databinding.AttachmentRearrangeDialogBinding
import jp.juggler.subwaytooter.databinding.AttachmentsRearrangeItemBinding
import jp.juggler.subwaytooter.defaultColorIcon
import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.util.data.ellipsizeDot3
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.dp
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import org.jetbrains.anko.backgroundColor
import kotlin.coroutines.resumeWithException
suspend fun ActPost.dialogArrachmentRearrange(
initialList: List<PostAttachment>,
): List<PostAttachment> = suspendCancellableCoroutine { cont ->
val views = AttachmentRearrangeDialogBinding.inflate(layoutInflater)
val dialog = Dialog(this)
dialog.setContentView(views.root)
cont.invokeOnCancellation { dialog.dismissSafe() }
dialog.setOnDismissListener {
if (cont.isActive) cont.resumeWithException(CancellationException())
}
val rearrangeAdapter = RearrangeAdapter(layoutInflater, initialList)
views.btnCancel.setOnClickListener {
dialog.dismissSafe()
}
views.btnOk.setOnClickListener {
if (cont.isActive) {
cont.resume(rearrangeAdapter.list) {}
}
dialog.dismissSafe()
}
views.listView.apply {
layoutManager = LinearLayoutManager(context)
adapter = rearrangeAdapter
rearrangeAdapter.itemTouchHelper.attachToRecyclerView(this)
}
dialog.window?.setLayout(dp(300), dp(440))
dialog.show()
}
private class RearrangeAdapter(
private val inflater: LayoutInflater,
initialList: List<PostAttachment>,
) : RecyclerView.Adapter<RearrangeAdapter.MyViewHolder>(),
MyDragCallback.Changer {
val list = ArrayList(initialList)
private var lastStateViewHolder: MyViewHolder? = null
private var draggingItem: PostAttachment? = null
val itemTouchHelper by lazy {
ItemTouchHelper(MyDragCallback(this))
}
override fun getItemCount() = list.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
MyViewHolder(parent)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(list.elementAtOrNull(position))
}
override fun onMove(posFrom: Int, posTo: Int): Boolean {
val item = list.removeAt(posFrom)
list.add(posTo, item)
notifyItemMoved(posFrom, posTo)
return true
}
override fun onState(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
val holder = (viewHolder as? MyViewHolder)
holder?.let { lastStateViewHolder = it }
val pa = holder?.lastItem
draggingItem = when {
pa != null && actionState == ItemTouchHelper.ACTION_STATE_DRAG -> pa
else -> null
}
holder?.bind()
lastStateViewHolder?.takeIf { it != holder }?.bind()
}
@SuppressLint("ClickableViewAccessibility")
inner class MyViewHolder(
parent: ViewGroup,
val views: AttachmentsRearrangeItemBinding =
AttachmentsRearrangeItemBinding.inflate(inflater, parent, false),
) : RecyclerView.ViewHolder(views.root) {
var lastItem: PostAttachment? = null
init {
views.root.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
itemTouchHelper.startDrag(this)
}
false
}
}
fun bind(pa: PostAttachment? = lastItem) {
pa ?: return
lastItem = pa
val context = views.root.context
views.root.apply {
when {
draggingItem === pa -> backgroundColor =
context.attrColor(R.attr.colorSearchFormBackground)
else -> background = null
}
}
views.ivThumbnail.apply {
val imageUrl = pa.attachment?.preview_url
if (imageUrl.isNullOrEmpty()) {
val imageId = when (pa.status) {
PostAttachment.Status.Progress -> R.drawable.ic_upload
PostAttachment.Status.Error -> R.drawable.ic_error
else -> R.drawable.ic_clip
}
Glide.with(context).clear(this)
setImageDrawable(defaultColorIcon(context, imageId))
} else {
Glide.with(context)
.load(imageUrl)
.placeholder(defaultColorIcon(context, R.drawable.ic_hourglass))
.error(defaultColorIcon(context, R.drawable.ic_error))
.fallback(defaultColorIcon(context, R.drawable.ic_clip))
.into(this)
}
}
views.tvText.text = pa.attachment?.run {
"$type ${description?.ellipsizeDot3(40) ?: ""}"
} ?: context.getString(R.string.attachment_uploading)
}
}
}
private class MyDragCallback(
private val changer: Changer,
) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
0 // no swipe
) {
interface Changer {
fun onMove(posFrom: Int, posTo: Int): Boolean
fun onState(viewHolder: RecyclerView.ViewHolder?, actionState: Int)
}
override fun isLongPressDragEnabled() = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean = changer.onMove(
// position of drag from
viewHolder.bindingAdapterPosition,
// position of drag to
target.bindingAdapterPosition,
)
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int,
) {
super.onSelectedChanged(viewHolder, actionState)
changer.onState(viewHolder, actionState)
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
) {
super.clearView(recyclerView, viewHolder)
changer.onState(null, ItemTouchHelper.ACTION_STATE_IDLE)
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,800L80,600L280,400L336,457L233,560L520,560L520,640L233,640L336,743L280,800ZM680,560L624,503L727,400L440,400L440,320L727,320L624,217L680,160L880,360L680,560Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M320,520L320,233L217,336L160,280L360,80L560,280L503,336L400,233L400,520L320,520ZM600,880L400,680L457,624L560,727L560,440L640,440L640,727L743,624L800,680L600,880Z"/>
</vector>

View File

@ -116,14 +116,13 @@
android:textAllCaps="false" />
</LinearLayout>
<LinearLayout
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/llAttachment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:baselineAligned="false"
android:gravity="top|start"
android:orientation="horizontal">
app:flexWrap="wrap">
<jp.juggler.subwaytooter.view.MyNetworkImageView
android:id="@+id/ivMedia1"
@ -161,16 +160,29 @@
android:scaleType="fitCenter"
tools:src="@drawable/ic_videocam" />
<ImageButton
android:id="@+id/btnAttachmentsRearrange"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:background="@drawable/btn_bg_transparent_round6dp"
android:contentDescription="@string/rearrange"
android:src="@drawable/swap_horiz_24px"
android:visibility="gone"
app:tint="?attr/colorTextContent"
tools:visibility="visible" />
<TextView
android:id="@+id/tvAttachmentProgress"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxWidth="160dp"
android:textSize="11sp"
android:visibility="gone"
tools:text="アップロード中です\nアップロード中です\nアップロード中です\nアップロード中です\nアップロード中です"
tools:visibility="visible" />
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/colorTextContent"
android:padding="8dp"
android:text="@string/attachment_rearrange_desc"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorSettingDivider"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:background="@drawable/btn_bg_transparent_round6dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/btnCancel"
android:text="@string/cancel"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="?attr/colorSettingDivider"/>
<Button
android:background="@drawable/btn_bg_transparent_round6dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/btnOk"
android:text="@string/ok"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/llColumn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="3dp">
<ImageView
android:id="@+id/ivThumbnail"
android:layout_width="80dp"
android:layout_height="80dp"
android:background="#80808080"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
tools:src="@drawable/ic_face" />
<TextView
android:id="@+id/tvText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:minHeight="60dp"
android:textColor="?attr/colorTextContent"
tools:text="aaa" />
</LinearLayout>

View File

@ -1290,4 +1290,6 @@
<string name="detail">詳細</string>
<string name="emoji_detail">絵文字の詳細</string>
<string name="post_404_desc">(もしくは、フォロー限定投稿にフォロー外から返信しようとしました)</string>
<string name="rearrange">並べ替え</string>
<string name="attachment_rearrange_desc">ドラッグで並べ替え</string>
</resources>

View File

@ -1302,4 +1302,6 @@
<string name="detail">Detail</string>
<string name="emoji_detail">Emoji detail</string>
<string name="post_404_desc">(Or, you reply to follower-only post from other account)</string>
<string name="rearrange">rearrange</string>
<string name="attachment_rearrange_desc">Drag to rearrange.</string>
</resources>

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.ColorFilter
@ -19,6 +20,7 @@ import android.media.RingtoneManager
import android.os.SystemClock
import android.text.Editable
import android.text.TextWatcher
import android.util.DisplayMetrics
import android.util.SparseArray
import android.view.View
import android.widget.ImageButton
@ -389,3 +391,19 @@ fun AppCompatActivity.setNavigationBack(toolbar: Toolbar) =
toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
val Float.roundPixels get()= (this+0.5f).toInt()
fun DisplayMetrics.dpFloat(src:Float) = (density* src)
fun DisplayMetrics.dpFloat(src:Int) = (density*src.toFloat())
fun Resources.dpFloat(src:Float) = displayMetrics.dpFloat(src)
fun Resources.dpFloat(src:Int) = displayMetrics.dpFloat(src)
fun Context.dpFloat(src:Float) = resources.dpFloat(src)
fun Context.dpFloat(src:Int) = resources.dpFloat(src)
fun DisplayMetrics.dp(src:Float) = (density* src).roundPixels
fun DisplayMetrics.dp(src:Int) = (density*src.toFloat()).roundPixels
fun Resources.dp(src:Float) = displayMetrics.dp(src)
fun Resources.dp(src:Int) = displayMetrics.dp(src)
fun Context.dp(src:Float) = resources.dp(src)
fun Context.dp(src:Int) = resources.dp(src)