@@ -7,6 +7,7 @@ import android.view.View
77import android.view.ViewGroup
88import android.widget.LinearLayout
99import androidx.core.view.isVisible
10+ import androidx.core.view.doOnPreDraw
1011import androidx.databinding.DataBindingUtil
1112import androidx.fragment.app.Fragment
1213import androidx.fragment.app.activityViewModels
@@ -282,13 +283,80 @@ class UserV3InfoFragment : Fragment() {
282283 chips.add(Triple (" Premium" , if (user.is_premium == true ) " ★ Premium User" else " Standard" , false ))
283284 chips.add(Triple (" Pixiv URL" , " https://www.pixiv.net/users/${user.id} " , true ))
284285
285- val grid = binding.profileGrid
286+ renderChipGrid(
287+ binding.profileGrid,
288+ chips.size,
289+ createChip = { createProfileChip(chips[it]) },
290+ isFullWidth = { chips[it].first == " Pixiv URL" }
291+ )
292+ }
293+
294+ private fun createProfileChip (data : Triple <String , String , Boolean >): View {
295+ val bd = ItemV3ProfileChipBinding .inflate(LayoutInflater .from(requireContext()))
296+ bd.chipLabel.text = data.first
297+ bd.chipValue.text = data.second
298+ if (data.second == " ★ Premium User" ) {
299+ bd.chipValue.setTextColor(0xFFFFC233 .toInt())
300+ }
301+ if (data.second.startsWith(" http://" ) || data.second.startsWith(" https://" )) {
302+ // 不用 autoLinkMask/LinkMovementMethod:LinkMovementMethod 继承
303+ // ScrollingMovementMethod,横拖 URL 会跟外层 ViewPager2 抢手势,整页
304+ // 跟着滑(issue #917)。改成整块点击打开 + 复用 textColorLink 的链接色,
305+ // 跟 addSocialChip 的 tap-to-open 一致。
306+ bd.chipValue.setTextColor(bd.chipValue.linkTextColors)
307+ bd.root.setOnClickListener { openUrl(data.second) }
308+ }
309+ return bd.root
310+ }
311+
312+ /* *
313+ * 子卡片先按「半行宽度」量自己的期望宽度,两个都放得下才并列,否则独占一行,而不是强行
314+ * 并列截断。判断依赖 grid 的真实宽度:首次绑定时父卡片刚从 gone 切 visible,grid 还没量过,
315+ * 要等这一次 measure/layout 跑完才有数,所以放到 preDraw 里摆(layout 已完成、还没画)。
316+ * 不能拿屏幕宽度估算——平板双栏 / 分屏下 widthPixels 是整屏宽,估出来比实际宽一倍,长文本
317+ * 照样被并列截断,等于没修。
318+ *
319+ * 也不能用 doOnLayout:OnLayoutChangeListener 是在 grid 自己的 layout() 里回调的,回调里
320+ * addView 打上的 FORCE_LAYOUT 标记会在 layout() 收尾时被清掉,ViewRootImpl 判定「无需第二趟」,
321+ * 首帧 grid 是空的,要等别的东西触发下一次 layout 才出现。preDraw 里 addView 走的是正常的
322+ * requestLayout → 下一帧重排,OneShotPreDrawListener 随 view detach 自动摘除。
323+ */
324+ private fun renderChipGrid (
325+ grid : LinearLayout ,
326+ chipCount : Int ,
327+ createChip : (Int ) -> View ,
328+ isFullWidth : (Int ) -> Boolean = { false },
329+ ) {
330+ grid.doOnPreDraw { layoutChipGrid(grid, chipCount, createChip, isFullWidth) }
331+ }
332+
333+ private fun layoutChipGrid (
334+ grid : LinearLayout ,
335+ chipCount : Int ,
336+ createChip : (Int ) -> View ,
337+ isFullWidth : (Int ) -> Boolean ,
338+ ) {
286339 grid.removeAllViews()
287340 val density = resources.displayMetrics.density
288341 val rowGap = (10 * density).toInt()
289342 val chipGap = (5 * density).toInt()
343+
344+ // 并列时 chip1 有 marginEnd、chip2 有 marginStart,一行让出两份 chipGap。
345+ val halfSlotWidth = ((grid.width - chipGap * 2 ) / 2 ).coerceAtLeast(0 )
346+
347+ fun fitsHalfSlot (chip : View ): Boolean {
348+ val bd = ItemV3ProfileChipBinding .bind(chip)
349+ val labelWidth = bd.chipLabel.paint.measureText(
350+ bd.chipLabel.text.toString().uppercase()
351+ )
352+ val valueWidth = bd.chipValue.paint.measureText(bd.chipValue.text.toString())
353+ val desiredWidth = maxOf(labelWidth, valueWidth).toInt() +
354+ bd.root.paddingLeft + bd.root.paddingRight
355+ return desiredWidth <= halfSlotWidth
356+ }
357+
290358 var i = 0
291- while (i < chips.size ) {
359+ while (i < chipCount ) {
292360 val row = LinearLayout (requireContext()).apply {
293361 orientation = LinearLayout .HORIZONTAL
294362 layoutParams = LinearLayout .LayoutParams (
@@ -297,50 +365,45 @@ class UserV3InfoFragment : Fragment() {
297365 ).apply { bottomMargin = rowGap }
298366 }
299367
300- val chip1 = createProfileChip(chips[i])
301- val isLastSingle = i + 1 >= chips.size
302- val isFullWidth = chips[i].first == " Pixiv URL"
368+ val chip1 = createChip(i)
369+ val isLastSingle = i + 1 >= chipCount
370+ val fullWidth = isFullWidth(i)
371+ val chip1Fits = ! fullWidth && ! isLastSingle && fitsHalfSlot(chip1)
372+ val chip2 = if (chip1Fits) createChip(i + 1 ) else null
373+ val canPair = chip1Fits && chip2 != null && fitsHalfSlot(chip2)
303374
304- if (isFullWidth || isLastSingle) {
305- chip1.layoutParams =
306- LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f )
307- row.addView(chip1)
308- } else {
375+ if (canPair) {
309376 chip1.layoutParams =
310377 LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f ).apply {
311378 marginEnd = chipGap
312379 }
313380 row.addView(chip1)
314381
315- val chip2 = createProfileChip(chips[i + 1 ] )
316- chip2 .layoutParams =
382+ val chip2View = checkNotNull(chip2 )
383+ chip2View .layoutParams =
317384 LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f ).apply {
318385 marginStart = chipGap
319386 }
320- row.addView(chip2 )
387+ row.addView(chip2View )
321388 i++
389+ } else {
390+ // 独占一行时允许文本换行,长文本不再被单行省略
391+ makeChipWrappable(chip1)
392+ chip1.layoutParams =
393+ LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f )
394+ row.addView(chip1)
322395 }
323396 grid.addView(row)
324397 i++
325398 }
326399 }
327400
328- private fun createProfileChip (data : Triple <String , String , Boolean >): View {
329- val bd = ItemV3ProfileChipBinding .inflate(LayoutInflater .from(requireContext()))
330- bd.chipLabel.text = data.first
331- bd.chipValue.text = data.second
332- if (data.second == " ★ Premium User" ) {
333- bd.chipValue.setTextColor(0xFFFFC233 .toInt())
334- }
335- if (data.second.startsWith(" http://" ) || data.second.startsWith(" https://" )) {
336- // 不用 autoLinkMask/LinkMovementMethod:LinkMovementMethod 继承
337- // ScrollingMovementMethod,横拖 URL 会跟外层 ViewPager2 抢手势,整页
338- // 跟着滑(issue #917)。改成整块点击打开 + 复用 textColorLink 的链接色,
339- // 跟 addSocialChip 的 tap-to-open 一致。
340- bd.chipValue.setTextColor(bd.chipValue.linkTextColors)
341- bd.root.setOnClickListener { openUrl(data.second) }
342- }
343- return bd.root
401+ @android.annotation.SuppressLint (" InlinedApi" )
402+ private fun makeChipWrappable (chip : View ) {
403+ val bd = ItemV3ProfileChipBinding .bind(chip)
404+ bd.chipValue.maxLines = Int .MAX_VALUE
405+ bd.chipValue.ellipsize = null
406+ bd.chipValue.breakStrategy = android.graphics.text.LineBreaker .BREAK_STRATEGY_HIGH_QUALITY
344407 }
345408
346409 private fun setupWorkspaceCard (workspace : WorkspaceBean ? ) {
@@ -363,47 +426,11 @@ class UserV3InfoFragment : Fragment() {
363426 if (items.isEmpty()) return
364427
365428 binding.workspaceCard.visibility = View .VISIBLE
366- val grid = binding.workspaceGrid
367- grid.removeAllViews()
368- val density = resources.displayMetrics.density
369- val rowGap = (10 * density).toInt()
370- val chipGap = (5 * density).toInt()
371-
372- var i = 0
373- while (i < items.size) {
374- val row = LinearLayout (requireContext()).apply {
375- orientation = LinearLayout .HORIZONTAL
376- layoutParams = LinearLayout .LayoutParams (
377- LinearLayout .LayoutParams .MATCH_PARENT ,
378- LinearLayout .LayoutParams .WRAP_CONTENT
379- ).apply { bottomMargin = rowGap }
380- }
381-
382- val isLastSingle = i + 1 >= items.size
383- val chip1 = createWorkspaceChip(items[i])
384-
385- if (isLastSingle) {
386- chip1.layoutParams =
387- LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f )
388- row.addView(chip1)
389- } else {
390- chip1.layoutParams =
391- LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f ).apply {
392- marginEnd = chipGap
393- }
394- row.addView(chip1)
395-
396- val chip2 = createWorkspaceChip(items[i + 1 ])
397- chip2.layoutParams =
398- LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f ).apply {
399- marginStart = chipGap
400- }
401- row.addView(chip2)
402- i++
403- }
404- grid.addView(row)
405- i++
406- }
429+ renderChipGrid(
430+ binding.workspaceGrid,
431+ items.size,
432+ createChip = { createWorkspaceChip(items[it]) }
433+ )
407434
408435 binding.workspaceHeaderToggle.setOnClickListener {
409436 val isVisible = binding.workspaceGrid.visibility == View .VISIBLE
0 commit comments