diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt index c300237762..451182746c 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt @@ -9,16 +9,21 @@ import android.text.SpannableString import android.text.Spanned import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView import androidx.compose.runtime.Composable +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.FileProvider import androidx.core.content.ContextCompat import androidx.core.view.doOnPreDraw import androidx.core.view.drawToBitmap import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams +import androidx.core.widget.TextViewCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 @@ -69,6 +74,8 @@ import java.math.BigDecimal import java.math.RoundingMode import javax.inject.Inject import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.roundToInt @AndroidEntryPoint class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() { @@ -79,6 +86,8 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() private const val ARGS_CLOSE_LEVERAGE = "args_close_leverage" private const val SHARE_QR_URL = "https://mixin.one/mm" private const val SHARE_CARD_COVER_URL = "https://dl.mixinpay.com/perps-share-card.png" + private const val SHARE_POSTER_BASE_WIDTH_DP = 319 + private const val SHARE_OUTPUT_SIZE_PX = 1024 private val MIN_DISPLAY_PNL_PERCENT = BigDecimal("-100") fun newInstance(position: PerpsPositionItem) = PerpsPositionShareBottomFragment().withArgs { @@ -114,6 +123,7 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() private var isLoading = false private var currentDisplayMetric = ShareDisplayMetric.PNL private var currentPosterStyle = SharePosterStyle.CLASSIC + private var posterItemSize = 0 private lateinit var shareData: ShareCardData private lateinit var posterAdapter: PosterAdapter @@ -375,7 +385,10 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() clipToPadding = false clipChildren = false setPadding(28.dp, 0, 28.dp, 0) - (getChildAt(0) as? RecyclerView)?.clipToPadding = false + (getChildAt(0) as? RecyclerView)?.apply { + clipToPadding = false + isNestedScrollingEnabled = false + } setPageTransformer { page, position -> val factor = (1f - position.absoluteValue).coerceIn(0f, 1f) page.alpha = 0.6f + factor * 0.4f @@ -404,21 +417,29 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() private fun updatePosterPagerHeight(force: Boolean = false) { val recyclerView = binding.posterPager.getChildAt(0) as? RecyclerView ?: return - val itemView = recyclerView.findViewHolderForAdapterPosition(binding.posterPager.currentItem)?.itemView ?: return + val holder = recyclerView.findViewHolderForAdapterPosition(binding.posterPager.currentItem) as? PosterViewHolder ?: return + val itemView = holder.itemView val width = itemView.width.takeIf { it > 0 } ?: (binding.posterPager.width - binding.posterPager.paddingStart - binding.posterPager.paddingEnd).takeIf { it > 0 } ?: return + posterItemSize = width + applyPosterItemSizeToVisibleHolders(recyclerView, width) itemView.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), ) - val measuredHeight = itemView.measuredHeight.takeIf { it > 0 } ?: return - if (!force && binding.posterPager.layoutParams.height == measuredHeight && binding.posterContainer.layoutParams.height == measuredHeight) return + if (!force && binding.posterPager.layoutParams.height == width && binding.posterContainer.layoutParams.height == width) return binding.posterPager.updateLayoutParams { - height = measuredHeight + height = width } binding.posterContainer.updateLayoutParams { - height = measuredHeight + height = width + } + } + + private fun applyPosterItemSizeToVisibleHolders(recyclerView: RecyclerView, itemSize: Int) { + repeat(posterAdapter.itemCount) { index -> + (recyclerView.findViewHolderForAdapterPosition(index) as? PosterViewHolder)?.applyItemSize(itemSize) } } @@ -507,7 +528,14 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() return file } - private fun createShareBitmap(): Bitmap = currentPosterView()?.drawToBitmap() ?: binding.posterPager.drawToBitmap() + private fun createShareBitmap(): Bitmap { + val bitmap = currentPosterView()?.drawToBitmap() ?: binding.posterPager.drawToBitmap() + return if (bitmap.width == SHARE_OUTPUT_SIZE_PX && bitmap.height == SHARE_OUTPUT_SIZE_PX) { + bitmap + } else { + Bitmap.createScaledBitmap(bitmap, SHARE_OUTPUT_SIZE_PX, SHARE_OUTPUT_SIZE_PX, true) + } + } private fun currentPosterView(): View? { val recyclerView = binding.posterPager.getChildAt(0) as? RecyclerView ?: return null @@ -630,6 +658,65 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() itemBinding.latestLabelTv.text = data.latestLabel itemBinding.latestValueTv.text = formatFiat(data.latestPrice) bindPosterFooter(itemBinding) + if (posterItemSize > 0) { + applyItemSize(posterItemSize) + } + } + + fun applyItemSize(itemSize: Int) { + val scale = itemSize.toFloat() / SHARE_POSTER_BASE_WIDTH_DP.dp + itemBinding.root.updateLayoutParams { + width = itemSize + height = itemSize + } + itemBinding.topCard.updateLayoutParams { + height = itemSize + } + itemBinding.cardContent.setPadding(20.scaledDp(scale), 16.scaledDp(scale), 0, 0) + itemBinding.cardContent.updateLayoutParams { + height = 0 + } + + itemBinding.header.setScaledDrawableSize(scale) + itemBinding.pnlTv.updateMargins(top = 18.scaledDp(scale)) + itemBinding.pnlTv.setPosterAutoTextSize(16f, 32f, scale) + itemBinding.assetIcon.updateLayoutParams { + width = 20.scaledDp(scale) + height = 20.scaledDp(scale) + topMargin = 8.scaledDp(scale) + } + itemBinding.sideTagTv.updateMargins(start = 10.scaledDp(scale)) + itemBinding.sideTagTv.setPadding(6.scaledDp(scale), 3.scaledDp(scale), 6.scaledDp(scale), 3.scaledDp(scale)) + itemBinding.sideTagTv.setPosterTextSize(12f, scale) + itemBinding.leverageTagTv.updateMargins(start = 8.scaledDp(scale)) + itemBinding.leverageTagTv.setPadding(6.scaledDp(scale), 3.scaledDp(scale), 6.scaledDp(scale), 3.scaledDp(scale)) + itemBinding.leverageTagTv.setPosterTextSize(12f, scale) + itemBinding.priceSection.updateMargins(top = 20.scaledDp(scale), bottom = 16.scaledDp(scale)) + itemBinding.entryLabelTv.setPosterTextSize(12f, scale) + itemBinding.entryValueTv.updateMargins(top = 2.scaledDp(scale)) + itemBinding.entryValueTv.setPosterTextSize(14f, scale) + itemBinding.latestLabelTv.updateMargins(top = 10.scaledDp(scale)) + itemBinding.latestLabelTv.setPosterTextSize(12f, scale) + itemBinding.latestValueTv.updateMargins(top = 2.scaledDp(scale), bottom = 8.scaledDp(scale)) + itemBinding.latestValueTv.setPosterTextSize(14f, scale) + itemBinding.trendImage.updateLayoutParams { + marginEnd = 16.scaledDp(scale) + matchConstraintMaxWidth = 112.scaledDp(scale) + matchConstraintMaxHeight = 112.scaledDp(scale) + } + + itemBinding.footer.setPadding(20.scaledDp(scale), 12.scaledDp(scale), 20.scaledDp(scale), 12.scaledDp(scale)) + itemBinding.referralTitle.setPosterTextSize(16f, scale) + itemBinding.shareDescTv.updateMargins(top = 6.scaledDp(scale), end = 18.scaledDp(scale)) + itemBinding.shareDescTv.setPosterTextSize(12f, scale) + itemBinding.iconFl.updateLayoutParams { + width = 72.scaledDp(scale) + height = 72.scaledDp(scale) + } + itemBinding.qrLogo.updateLayoutParams { + width = 16.scaledDp(scale) + height = 16.scaledDp(scale) + } } private fun bindPosterFooter(itemBinding: ItemPerpsPositionSharePosterBinding) { @@ -656,6 +743,50 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment() } } + private fun Int.scaledDp(scale: Float): Int = (dp * scale).roundToInt().coerceAtLeast(1) + + private fun TextView.setPosterTextSize(size: Float, scale: Float) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, size * resources.displayMetrics.density * scale) + } + + private fun ImageView.setScaledDrawableSize(scale: Float) { + val drawable = drawable ?: return + val width = (drawable.intrinsicWidth * scale).roundToInt().coerceAtLeast(1) + val height = (drawable.intrinsicHeight * scale).roundToInt().coerceAtLeast(1) + updateLayoutParams { + this.width = width + this.height = height + } + } + + private fun TextView.setPosterAutoTextSize(minSize: Float, maxSize: Float, scale: Float) { + val density = resources.displayMetrics.density + val minPx = (minSize * density * scale).roundToInt().coerceAtLeast(1) + val maxPx = max(minPx + 1, (maxSize * density * scale).roundToInt()) + TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration( + this, + minPx, + maxPx, + density.roundToInt().coerceAtLeast(1), + TypedValue.COMPLEX_UNIT_PX, + ) + setPosterTextSize(maxSize, scale) + } + + private fun View.updateMargins( + start: Int? = null, + top: Int? = null, + end: Int? = null, + bottom: Int? = null, + ) { + updateLayoutParams { + start?.let { marginStart = it } + top?.let { topMargin = it } + end?.let { marginEnd = it } + bottom?.let { bottomMargin = it } + } + } + private fun bindDefaultPosterFooter(itemBinding: ItemPerpsPositionSharePosterBinding) { itemBinding.shareDescTv.isVisible = true itemBinding.shareDescTv.minLines = 1 diff --git a/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt index 17d6140fc4..2fd01adcc2 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/MarketShareBottomFragment.kt @@ -10,16 +10,25 @@ import android.text.SpannableString import android.text.Spanned import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan +import android.util.TypedValue +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.RelativeLayout +import android.widget.TextView import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.view.doOnAttach +import androidx.core.view.doOnLayout import androidx.core.view.drawToBitmap import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers @@ -71,6 +80,7 @@ import java.io.File import java.io.FileOutputStream import java.math.BigDecimal import javax.inject.Inject +import kotlin.math.roundToInt @AndroidEntryPoint class MarketShareBottomFragment : MixinComposeBottomSheetDialogFragment() { @@ -80,6 +90,7 @@ class MarketShareBottomFragment : MixinComposeBottomSheetDialogFragment() { private const val ARGS_TYPE = "type" private const val SHARE_QR_URL = "https://mixin.one/mm" private const val SHARE_CARD_COVER_URL = "https://dl.mixinpay.com/share-market-card.png" + private const val SHARE_CARD_MAX_SIZE_DP = 319 fun newInstance( marketItem: MarketItem, @@ -110,6 +121,7 @@ class MarketShareBottomFragment : MixinComposeBottomSheetDialogFragment() { private var isLoading = false private var isChartLoading = true private var isChartContentSet = false + private var appliedShareCardSize = 0 override fun getTheme() = R.style.AppTheme_Dialog @@ -129,6 +141,7 @@ class MarketShareBottomFragment : MixinComposeBottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) selectedType = arguments?.getString(ARGS_TYPE) ?: "1D" + setupShareCardLayout() bindMarketCard() setupMarketChart() bindMixinContact() @@ -199,6 +212,132 @@ class MarketShareBottomFragment : MixinComposeBottomSheetDialogFragment() { binding.marketChart.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) } + private fun setupShareCardLayout() { + applyShareCardScale(1f) + binding.content.doOnLayout { + updateShareCardLayout() + } + binding.content.addOnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ -> + if (right - left != oldRight - oldLeft) { + updateShareCardLayout() + } + } + } + + private fun updateShareCardLayout() { + val availableWidth = binding.content.width - binding.content.paddingStart - binding.content.paddingEnd + if (availableWidth <= 0) return + + val maxCardSize = SHARE_CARD_MAX_SIZE_DP.dp + val cardSize = minOf(availableWidth, maxCardSize) + val scale = cardSize.toFloat() / maxCardSize + applyShareCardScale(scale) + + if (cardSize != appliedShareCardSize) { + appliedShareCardSize = cardSize + binding.llMarketShare.updateLayoutParams { + width = cardSize + height = ViewGroup.LayoutParams.WRAP_CONTENT + gravity = Gravity.CENTER_HORIZONTAL + } + binding.cardTop.updateLayoutParams { + width = ViewGroup.LayoutParams.MATCH_PARENT + height = cardSize + } + } + binding.cardTop.post { + updateMarketChartHeight(cardSize, scale) + } + } + + private fun updateMarketChartHeight(cardSize: Int, scale: Float) { + val headerHeight = binding.cardHeader.measuredHeight + val titleHeight = binding.marketTitleRl.measuredHeight + if (headerHeight <= 0 || titleHeight <= 0) return + + val usedHeight = headerHeight + + binding.cardHeader.verticalMargins() + + titleHeight + + binding.marketChartContainer.verticalMargins() + + binding.marketCard.paddingTop + + binding.marketCard.paddingBottom + val chartHeight = (cardSize - usedHeight).coerceAtLeast(72.scaledDp(scale)) + binding.marketChartContainer.updateLayoutParams { + height = chartHeight + } + } + + private fun applyShareCardScale(scale: Float) { + binding.cardHeader.updateMargins(top = 16.scaledDp(scale), bottom = 20.scaledDp(scale)) + binding.logo.updateMargins(start = 20.scaledDp(scale)) + binding.logo.setScaledDrawableSize(scale) + + binding.marketCard.setPaddingRelative(20.scaledDp(scale), 0, 20.scaledDp(scale), 18.scaledDp(scale)) + binding.assetSymbol.setShareCardTextSize(14f, scale) + binding.assetRank.updateMargins(start = 4.scaledDp(scale)) + binding.assetRank.setPaddingRelative(4.scaledDp(scale), 1.scaledDp(scale), 4.scaledDp(scale), 1.scaledDp(scale)) + binding.assetRank.setShareCardTextSize(12f, scale) + binding.icon.updateLayoutParams { + width = 48.scaledDp(scale) + height = 48.scaledDp(scale) + } + binding.priceValue.updateMargins(top = 8.scaledDp(scale)) + binding.priceValue.setShareCardTextSize(22f, scale) + binding.priceRise.updateMargins(top = 8.scaledDp(scale)) + binding.priceRise.setPaddingRelative(6.scaledDp(scale), 3.scaledDp(scale), 6.scaledDp(scale), 3.scaledDp(scale)) + binding.priceRise.setShareCardTextSize(14f, scale) + binding.marketChartContainer.updateMargins(top = 18.scaledDp(scale)) + + binding.footer.setPaddingRelative(20.scaledDp(scale), 12.scaledDp(scale), 20.scaledDp(scale), 12.scaledDp(scale)) + binding.title.setShareCardTextSize(16f, scale) + binding.shareDesc.updateMargins(top = 6.scaledDp(scale), end = 18.scaledDp(scale)) + binding.shareDesc.setShareCardTextSize(12f, scale) + binding.iconFl.updateLayoutParams { + width = 72.scaledDp(scale) + height = 72.scaledDp(scale) + } + binding.qrLogo.updateLayoutParams { + width = 16.scaledDp(scale) + height = 16.scaledDp(scale) + } + } + + private fun View.verticalMargins(): Int { + val params = layoutParams as? ViewGroup.MarginLayoutParams ?: return 0 + return params.topMargin + params.bottomMargin + } + + private fun Int.scaledDp(scale: Float): Int = (dp * scale).roundToInt().coerceAtLeast(1) + + private fun TextView.setShareCardTextSize(size: Float, scale: Float) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, size * resources.displayMetrics.density * scale) + } + + private fun ImageView.setScaledDrawableSize(scale: Float) { + val drawable = drawable ?: return + if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) return + val width = (drawable.intrinsicWidth * scale).roundToInt().coerceAtLeast(1) + val height = (drawable.intrinsicHeight * scale).roundToInt().coerceAtLeast(1) + updateLayoutParams { + this.width = width + this.height = height + } + } + + private fun View.updateMargins( + start: Int? = null, + top: Int? = null, + end: Int? = null, + bottom: Int? = null, + ) { + updateLayoutParams { + start?.let { marginStart = it } + top?.let { topMargin = it } + end?.let { marginEnd = it } + bottom?.let { bottomMargin = it } + } + } + override fun onStart() { super.onStart() binding.marketChart.doOnAttach { diff --git a/app/src/main/res/layout/fragment_market_share_bottom.xml b/app/src/main/res/layout/fragment_market_share_bottom.xml index 6beffc8948..d0e5c3adf1 100644 --- a/app/src/main/res/layout/fragment_market_share_bottom.xml +++ b/app/src/main/res/layout/fragment_market_share_bottom.xml @@ -3,11 +3,12 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" android:background="@drawable/bg_upper_round" android:orientation="vertical"> @@ -35,17 +36,17 @@ android:src="@drawable/ic_circle_close" /> - @@ -149,7 +149,6 @@ android:text="@string/NA" android:textColor="?attr/text_minor" android:textFontWeight="600" - android:textSize="22sp" tools:targetApi="p" /> @@ -222,7 +220,6 @@ android:includeFontPadding="false" android:text="@string/mixin_messenger" android:textColor="@color/white" - android:textSize="16sp" android:textStyle="bold" /> + android:textColor="@color/white" /> - + diff --git a/app/src/main/res/layout/fragment_perps_position_share_bottom.xml b/app/src/main/res/layout/fragment_perps_position_share_bottom.xml index 22e24c9020..7c50fa4e0e 100644 --- a/app/src/main/res/layout/fragment_perps_position_share_bottom.xml +++ b/app/src/main/res/layout/fragment_perps_position_share_bottom.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" android:background="@drawable/bg_upper_round" android:clipChildren="false" android:clipToPadding="false" @@ -37,6 +37,23 @@ android:src="@drawable/ic_circle_close" /> + + + + + + + diff --git a/app/src/main/res/layout/item_perps_position_share_poster.xml b/app/src/main/res/layout/item_perps_position_share_poster.xml index 93b224dbe7..6daef296a3 100644 --- a/app/src/main/res/layout/item_perps_position_share_poster.xml +++ b/app/src/main/res/layout/item_perps_position_share_poster.xml @@ -9,7 +9,7 @@ @@ -17,7 +17,8 @@ @@ -51,12 +52,7 @@ android:includeFontPadding="false" android:maxLines="1" android:textColor="@color/white" - android:textSize="32sp" android:textStyle="bold" - app:autoSizeMaxTextSize="32sp" - app:autoSizeMinTextSize="16sp" - app:autoSizeStepGranularity="1sp" - app:autoSizeTextType="uniform" app:layout_constraintEnd_toStartOf="@id/trend_image" app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toStartOf="parent" @@ -93,7 +89,6 @@ android:textColor="@color/white" android:paddingTop="3dp" android:paddingBottom="3dp" - android:textSize="12sp" app:layout_constraintStart_toEndOf="@id/asset_icon" app:layout_constraintTop_toTopOf="@id/asset_icon" app:layout_constraintBottom_toBottomOf="@id/asset_icon" @@ -112,7 +107,6 @@ android:paddingTop="3dp" android:paddingBottom="3dp" android:textColor="@color/white" - android:textSize="12sp" app:layout_constraintStart_toEndOf="@id/side_tag_tv" app:layout_constraintTop_toTopOf="@id/side_tag_tv" app:layout_constraintBottom_toBottomOf="@id/side_tag_tv" @@ -135,8 +129,7 @@ android:layout_height="wrap_content" android:includeFontPadding="false" android:text="@string/Entry_Price" - android:textColor="@color/white" - android:textSize="12sp" /> + android:textColor="@color/white" /> @@ -156,8 +148,7 @@ android:layout_marginTop="10dp" android:includeFontPadding="false" android:text="@string/perps_current_price" - android:textColor="@color/white" - android:textSize="12sp" /> + android:textColor="@color/white" /> @@ -176,16 +166,14 @@ android:id="@+id/trend_image" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginBottom="24dp" + android:layout_marginEnd="16dp" android:adjustViewBounds="true" android:contentDescription="@null" android:scaleType="fitCenter" android:src="@drawable/ic_perps_position_share_loss_classic" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toBottomOf="@id/price_section" app:layout_constraintDimensionRatio="1:1" - app:layout_constraintTop_toTopOf="@id/pnl_tv" - android:layout_marginTop="26dp" - android:layout_marginEnd="16dp" + app:layout_constraintTop_toTopOf="@id/price_section" app:layout_constraintEnd_toEndOf="parent" /> @@ -215,7 +203,6 @@ android:includeFontPadding="false" android:text="@string/mixin_messenger" android:textColor="@color/white" - android:textSize="16sp" android:textStyle="bold" /> + android:textColor="@color/white" />