Merge pull request #619 from hjubb/ui
Album, Thumbnail, Progress, Long Message UI for Conversation Screen 2.0pull/620/head
commit
d38b81f222
@ -0,0 +1,169 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import kotlinx.android.synthetic.main.album_thumbnail_view.view.*
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||||
|
import org.thoughtcrime.securesms.components.CornerMask
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
|
import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher
|
||||||
|
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
|
||||||
|
class AlbumThumbnailView : FrameLayout {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_ALBUM_DISPLAY_SIZE = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Lifecycle
|
||||||
|
constructor(context: Context) : super(context) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cornerMask by lazy { CornerMask(this) }
|
||||||
|
private var slides: List<Slide> = listOf()
|
||||||
|
private var slideSize: Int = 0
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispatchDraw(canvas: Canvas?) {
|
||||||
|
super.dispatchDraw(canvas)
|
||||||
|
cornerMask.mask(canvas)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Interaction
|
||||||
|
|
||||||
|
fun calculateHitObject(rawRect: Rect, mms: MmsMessageRecord) {
|
||||||
|
// Z-check in specific order
|
||||||
|
val testRect = Rect()
|
||||||
|
// test "Read More"
|
||||||
|
albumCellBodyTextReadMore.getGlobalVisibleRect(testRect)
|
||||||
|
if (Rect.intersects(rawRect, testRect)) {
|
||||||
|
// dispatch to activity view
|
||||||
|
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||||
|
LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// test each album child
|
||||||
|
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
|
||||||
|
child.getGlobalVisibleRect(testRect)
|
||||||
|
if (Rect.intersects(rawRect, testRect)) {
|
||||||
|
// hit intersects with this particular child
|
||||||
|
val slide = slides.getOrNull(index) ?: return
|
||||||
|
// only open to downloaded images
|
||||||
|
if (slide.isInProgress) return
|
||||||
|
|
||||||
|
ActivityDispatcher.get(context)?.dispatchIntent { context ->
|
||||||
|
MediaPreviewActivity.getPreviewIntent(context, slide, mms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(glideRequests: GlideRequests, message: MmsMessageRecord,
|
||||||
|
isStart: Boolean, isEnd: Boolean) {
|
||||||
|
slides = message.slideDeck.thumbnailSlides
|
||||||
|
if (slides.isEmpty()) {
|
||||||
|
// this should never be encountered because it's checked by parent
|
||||||
|
return
|
||||||
|
}
|
||||||
|
calculateRadius(isStart, isEnd, message.isOutgoing)
|
||||||
|
|
||||||
|
// recreate cell views if different size to what we have already (for recycling)
|
||||||
|
if (slides.size != this.slideSize) {
|
||||||
|
albumCellContainer.removeAllViews()
|
||||||
|
LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer)
|
||||||
|
val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE
|
||||||
|
albumCellContainer.findViewById<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
|
||||||
|
// overflowText will be null if !overflowed
|
||||||
|
overflowText.isVisible = overflowed // more than max album size
|
||||||
|
overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE)
|
||||||
|
}
|
||||||
|
this.slideSize = slides.size
|
||||||
|
}
|
||||||
|
// iterate binding
|
||||||
|
slides.take(5).forEachIndexed { position, slide ->
|
||||||
|
val thumbnailView = getThumbnailView(position)
|
||||||
|
thumbnailView.setImageResource(glideRequests, slide, isPreview = false)
|
||||||
|
}
|
||||||
|
albumCellBodyParent.isVisible = message.body.isNotEmpty()
|
||||||
|
albumCellBodyText.text = message.body
|
||||||
|
post {
|
||||||
|
// post to await layout of text
|
||||||
|
albumCellBodyText.layout?.let { layout ->
|
||||||
|
val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) }
|
||||||
|
?: 0
|
||||||
|
// show read more text if at least one line is ellipsized
|
||||||
|
albumCellBodyTextReadMore.isVisible = maxEllipsis > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
|
fun layoutRes(slideCount: Int) = when (slideCount) {
|
||||||
|
1 -> R.layout.album_thumbnail_1 // single
|
||||||
|
2 -> R.layout.album_thumbnail_2// two sidebyside
|
||||||
|
3 -> R.layout.album_thumbnail_3// three stacked
|
||||||
|
4 -> R.layout.album_thumbnail_4// four square
|
||||||
|
5 -> R.layout.album_thumbnail_5//
|
||||||
|
else -> R.layout.album_thumbnail_many// five or more
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThumbnailView(position: Int): KThumbnailView = when (position) {
|
||||||
|
0 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
|
||||||
|
1 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
|
||||||
|
2 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
|
||||||
|
3 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
|
||||||
|
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
|
||||||
|
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun calculateRadius(isStart: Boolean, isEnd: Boolean, outgoing: Boolean) {
|
||||||
|
val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).toInt()
|
||||||
|
val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).toInt()
|
||||||
|
val (startTop, endTop, startBottom, endBottom) = when {
|
||||||
|
// single message, consistent dimen
|
||||||
|
isStart && isEnd -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||||
|
// start of message cluster, collapsed BL
|
||||||
|
isStart -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||||
|
// end of message cluster, collapsed TL
|
||||||
|
isEnd -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen)
|
||||||
|
// else in the middle, no rounding left side
|
||||||
|
else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen)
|
||||||
|
}
|
||||||
|
// TL, TR, BR, BL (CW direction)
|
||||||
|
cornerMask.setRadii(
|
||||||
|
if (!outgoing) startTop else endTop, // TL
|
||||||
|
if (!outgoing) endTop else startTop, // TR
|
||||||
|
if (!outgoing) endBottom else startBottom, // BR
|
||||||
|
if (!outgoing) startBottom else endBottom // BL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import kotlinx.android.synthetic.main.thumbnail_view.view.*
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
|
import org.session.libsession.utilities.Util.equals
|
||||||
|
import org.session.libsignal.utilities.ListenableFuture
|
||||||
|
import org.session.libsignal.utilities.SettableFuture
|
||||||
|
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
||||||
|
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
||||||
|
import org.thoughtcrime.securesms.mms.*
|
||||||
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||||
|
|
||||||
|
open class KThumbnailView: FrameLayout {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val WIDTH = 0
|
||||||
|
private const val HEIGHT = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Lifecycle
|
||||||
|
constructor(context: Context) : super(context) { initialize(null) }
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
||||||
|
|
||||||
|
private val image by lazy { thumbnail_image }
|
||||||
|
private val playOverlay by lazy { play_overlay }
|
||||||
|
private val captionIcon by lazy { thumbnail_caption_icon }
|
||||||
|
val loadIndicator: View by lazy { thumbnail_load_indicator }
|
||||||
|
|
||||||
|
private val dimensDelegate = ThumbnailDimensDelegate()
|
||||||
|
|
||||||
|
var thumbnailClickListener: SlideClickListener? = null
|
||||||
|
|
||||||
|
private var slide: Slide? = null
|
||||||
|
private var radius: Int = 0
|
||||||
|
|
||||||
|
private fun initialize(attrs: AttributeSet?) {
|
||||||
|
inflate(context, R.layout.thumbnail_view, this)
|
||||||
|
if (attrs != null) {
|
||||||
|
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
||||||
|
|
||||||
|
dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0),
|
||||||
|
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0),
|
||||||
|
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0),
|
||||||
|
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0))
|
||||||
|
|
||||||
|
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0)
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val adjustedDimens = dimensDelegate.resourceSize()
|
||||||
|
if (adjustedDimens[WIDTH] == 0 && adjustedDimens[HEIGHT] == 0) {
|
||||||
|
return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalWidth: Int = adjustedDimens[WIDTH] + paddingLeft + paddingRight
|
||||||
|
val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom
|
||||||
|
|
||||||
|
super.onMeasure(
|
||||||
|
MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
||||||
|
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0)
|
||||||
|
private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0)
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Interaction
|
||||||
|
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean): ListenableFuture<Boolean> {
|
||||||
|
return setImageResource(glide, slide, isPreview, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setImageResource(glide: GlideRequests, slide: Slide,
|
||||||
|
isPreview: Boolean, naturalWidth: Int,
|
||||||
|
naturalHeight: Int): ListenableFuture<Boolean> {
|
||||||
|
|
||||||
|
val currentSlide = this.slide
|
||||||
|
|
||||||
|
playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
||||||
|
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||||
|
|
||||||
|
if (equals(currentSlide, slide)) {
|
||||||
|
// don't re-load slide
|
||||||
|
return SettableFuture(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) {
|
||||||
|
// not reloading slide for fast preflight
|
||||||
|
this.slide = slide
|
||||||
|
}
|
||||||
|
|
||||||
|
this.slide = slide
|
||||||
|
|
||||||
|
captionIcon.isVisible = slide.caption.isPresent
|
||||||
|
loadIndicator.isVisible = slide.isInProgress
|
||||||
|
|
||||||
|
dimensDelegate.setDimens(naturalWidth, naturalHeight)
|
||||||
|
invalidate()
|
||||||
|
|
||||||
|
val result = SettableFuture<Boolean>()
|
||||||
|
|
||||||
|
when {
|
||||||
|
slide.thumbnailUri != null -> {
|
||||||
|
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result))
|
||||||
|
}
|
||||||
|
slide.hasPlaceholder() -> {
|
||||||
|
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
glide.clear(image)
|
||||||
|
result.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Drawable> {
|
||||||
|
|
||||||
|
val dimens = dimensDelegate.resourceSize()
|
||||||
|
|
||||||
|
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
|
.let { request ->
|
||||||
|
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
||||||
|
request.override(getDefaultWidth(), getDefaultHeight())
|
||||||
|
} else {
|
||||||
|
request.override(dimens[WIDTH], dimens[HEIGHT])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
|
.centerCrop()
|
||||||
|
|
||||||
|
return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Bitmap> {
|
||||||
|
|
||||||
|
val dimens = dimensDelegate.resourceSize()
|
||||||
|
|
||||||
|
return glide.asBitmap()
|
||||||
|
.load(slide.getPlaceholderRes(context.theme))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.let { request ->
|
||||||
|
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
||||||
|
request.override(getDefaultWidth(), getDefaultHeight())
|
||||||
|
} else {
|
||||||
|
request.override(dimens[WIDTH], dimens[HEIGHT])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fitCenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun clear(glideRequests: GlideRequests) {
|
||||||
|
glideRequests.clear(image)
|
||||||
|
slide = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture<Boolean> {
|
||||||
|
val future = SettableFuture<Boolean>()
|
||||||
|
|
||||||
|
var request: GlideRequest<Drawable> = glideRequests.load(DecryptableUri(uri))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
|
|
||||||
|
request = if (radius > 0) {
|
||||||
|
request.transforms(CenterCrop(), RoundedCorners(radius))
|
||||||
|
} else {
|
||||||
|
request.transforms(CenterCrop())
|
||||||
|
}
|
||||||
|
|
||||||
|
request.into(GlideDrawableListeningTarget(image, future))
|
||||||
|
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
|
class ThumbnailDimensDelegate {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// dimens array constants
|
||||||
|
private const val WIDTH = 0
|
||||||
|
private const val HEIGHT = 1
|
||||||
|
private const val DIMENS_ARRAY_SIZE = 2
|
||||||
|
|
||||||
|
// bounds array constants
|
||||||
|
private const val MIN_WIDTH = 0
|
||||||
|
private const val MIN_HEIGHT = 1
|
||||||
|
private const val MAX_WIDTH = 2
|
||||||
|
private const val MAX_HEIGHT = 3
|
||||||
|
private const val BOUNDS_ARRAY_SIZE = 4
|
||||||
|
|
||||||
|
// const zero int array
|
||||||
|
private val EMPTY_DIMENS = intArrayOf(0,0)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val measured: IntArray = IntArray(DIMENS_ARRAY_SIZE)
|
||||||
|
private val dimens: IntArray = IntArray(DIMENS_ARRAY_SIZE)
|
||||||
|
private val bounds: IntArray = IntArray(BOUNDS_ARRAY_SIZE)
|
||||||
|
|
||||||
|
fun resourceSize(): IntArray {
|
||||||
|
if (dimens.all { it == 0 }) {
|
||||||
|
// dimens are (0, 0), don't go any further
|
||||||
|
return EMPTY_DIMENS
|
||||||
|
}
|
||||||
|
|
||||||
|
val naturalWidth = dimens[WIDTH].toDouble()
|
||||||
|
val naturalHeight = dimens[HEIGHT].toDouble()
|
||||||
|
val minWidth = dimens[MIN_WIDTH]
|
||||||
|
val maxWidth = dimens[MAX_WIDTH]
|
||||||
|
val minHeight = dimens[MIN_HEIGHT]
|
||||||
|
val maxHeight = dimens[MAX_HEIGHT]
|
||||||
|
|
||||||
|
// calculate actual measured
|
||||||
|
var measuredWidth: Double = naturalWidth
|
||||||
|
var measuredHeight: Double = naturalHeight
|
||||||
|
|
||||||
|
val widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth
|
||||||
|
val heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight
|
||||||
|
|
||||||
|
if (!widthInBounds || !heightInBounds) {
|
||||||
|
val minWidthRatio: Double = naturalWidth / minWidth
|
||||||
|
val maxWidthRatio: Double = naturalWidth / maxWidth
|
||||||
|
val minHeightRatio: Double = naturalHeight / minHeight
|
||||||
|
val maxHeightRatio: Double = naturalHeight / maxHeight
|
||||||
|
if (maxWidthRatio > 1 || maxHeightRatio > 1) {
|
||||||
|
if (maxWidthRatio >= maxHeightRatio) {
|
||||||
|
measuredWidth /= maxWidthRatio
|
||||||
|
measuredHeight /= maxWidthRatio
|
||||||
|
} else {
|
||||||
|
measuredWidth /= maxHeightRatio
|
||||||
|
measuredHeight /= maxHeightRatio
|
||||||
|
}
|
||||||
|
measuredWidth = Math.max(measuredWidth, minWidth.toDouble())
|
||||||
|
measuredHeight = Math.max(measuredHeight, minHeight.toDouble())
|
||||||
|
} else if (minWidthRatio < 1 || minHeightRatio < 1) {
|
||||||
|
if (minWidthRatio <= minHeightRatio) {
|
||||||
|
measuredWidth /= minWidthRatio
|
||||||
|
measuredHeight /= minWidthRatio
|
||||||
|
} else {
|
||||||
|
measuredWidth /= minHeightRatio
|
||||||
|
measuredHeight /= minHeightRatio
|
||||||
|
}
|
||||||
|
measuredWidth = Math.min(measuredWidth, maxWidth.toDouble())
|
||||||
|
measuredHeight = Math.min(measuredHeight, maxHeight.toDouble())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measured[WIDTH] = measuredWidth.toInt()
|
||||||
|
measured[HEIGHT] = measuredHeight.toInt()
|
||||||
|
return measured
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBounds(minWidth: Int, minHeight: Int, maxWidth: Int, maxHeight: Int) {
|
||||||
|
bounds[MIN_WIDTH] = minWidth
|
||||||
|
bounds[MIN_HEIGHT] = minHeight
|
||||||
|
bounds[MAX_WIDTH] = maxWidth
|
||||||
|
bounds[MAX_HEIGHT] = maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDimens(width: Int, height: Int) {
|
||||||
|
dimens[WIDTH] = width
|
||||||
|
dimens[HEIGHT] = height
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Interpolator
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationSet
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
class ThumbnailProgressBar: View {
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
private val firstX: Double
|
||||||
|
get() = sin(SystemClock.elapsedRealtime() / 300.0) * 1.5
|
||||||
|
|
||||||
|
private val secondX: Double
|
||||||
|
get() = sin(SystemClock.elapsedRealtime() / 300.0 + (Math.PI/4)) * 1.5
|
||||||
|
|
||||||
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
color = ResourcesCompat.getColor(resources, R.color.accent, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val objectRect = Rect()
|
||||||
|
private val drawingRect = Rect()
|
||||||
|
|
||||||
|
override fun dispatchDraw(canvas: Canvas?) {
|
||||||
|
if (canvas == null) return
|
||||||
|
|
||||||
|
getDrawingRect(objectRect)
|
||||||
|
drawingRect.set(objectRect)
|
||||||
|
|
||||||
|
val coercedFX = firstX
|
||||||
|
val coercedSX = secondX
|
||||||
|
|
||||||
|
val firstMeasuredX = objectRect.left + (objectRect.width() * coercedFX)
|
||||||
|
val secondMeasuredX = objectRect.left + (objectRect.width() * coercedSX)
|
||||||
|
|
||||||
|
drawingRect.set(
|
||||||
|
(if (firstMeasuredX < secondMeasuredX) firstMeasuredX else secondMeasuredX).toInt(),
|
||||||
|
objectRect.top,
|
||||||
|
(if (firstMeasuredX < secondMeasuredX) secondMeasuredX else firstMeasuredX).toInt(),
|
||||||
|
objectRect.bottom
|
||||||
|
)
|
||||||
|
|
||||||
|
canvas.drawRect(drawingRect, paint)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/album_thumbnail_root"
|
||||||
|
android:layout_width="@dimen/media_bubble_default_dimens"
|
||||||
|
android:layout_height="@dimen/media_bubble_default_dimens">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
|
||||||
|
android:id="@+id/album_cell_1"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:minWidth="@dimen/media_bubble_min_width"
|
||||||
|
app:maxWidth="@dimen/media_bubble_max_width"
|
||||||
|
app:minHeight="@dimen/media_bubble_min_height"
|
||||||
|
app:maxHeight="@dimen/media_bubble_max_height"
|
||||||
|
app:thumbnail_radius="1dp"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
@ -1,21 +1,80 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="vertical"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/album_cell_container"
|
android:id="@+id/albumCellContainer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?conversation_background"/>
|
/>
|
||||||
|
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/album_transfer_controls_stub"
|
android:layout_alignTop="@+id/albumCellContainer"
|
||||||
|
android:layout_alignStart="@+id/albumCellContainer"
|
||||||
|
android:layout_alignEnd="@+id/albumCellContainer"
|
||||||
|
android:layout_alignBottom="@+id/albumCellContainer"
|
||||||
|
android:id="@+id/albumTransferControlsStub"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout="@layout/transfer_controls_stub" />
|
android:layout="@layout/transfer_controls_stub" />
|
||||||
|
|
||||||
</merge>
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_alignTop="@+id/albumCellContainer"
|
||||||
|
android:layout_alignStart="@+id/albumCellContainer"
|
||||||
|
android:layout_alignEnd="@+id/albumCellContainer"
|
||||||
|
android:layout_alignBottom="@+id/albumCellContainer"
|
||||||
|
tools:visibility="visible"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:id="@+id/albumCellBodyParent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/albumCellShade"
|
||||||
|
android:src="@drawable/image_shade"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/albumCellBodyTextParent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
/>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/albumCellBodyTextParent"
|
||||||
|
android:paddingHorizontal="@dimen/small_spacing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/albumCellBodyTextReadMore"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<View
|
||||||
|
android:layout_marginStart="@dimen/small_spacing"
|
||||||
|
android:layout_width="@dimen/accent_line_thickness"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginVertical="@dimen/small_spacing"
|
||||||
|
android:background="@color/accent"/>
|
||||||
|
<TextView
|
||||||
|
android:maxLines="4"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:id="@+id/albumCellBodyText"
|
||||||
|
android:textColor="@color/core_white"
|
||||||
|
android:layout_margin="@dimen/small_spacing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
<TextView
|
||||||
|
tools:visibility="visible"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:id="@+id/albumCellBodyTextReadMore"
|
||||||
|
android:textColor="@color/core_white"
|
||||||
|
android:padding="@dimen/small_spacing"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ConversationItem_read_more"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
Loading…
Reference in New Issue