• 自定义卡包效果实现


            最近在工作中发现UI的脑洞越来越大了,需要实现一个卡包的效果,虽然不是我的工作,但是工作之余也探索着实现了一下类似的效果。

            卡包其实就是多张卡片的集合,围绕着它要实现以下一些炫酷的效果:

    1.用户可能有多张卡片,未选中卡片的时候有卡片画廊效果,可以左右自由滑动,其实就是类似ViewPager的效果。这里的滑动不是随便滑动,小幅度的滑动会回弹,大幅度的滑动达到阈值的时候会滑动到左边或者右边的一张卡片,且滑动结束后新的卡片始终在中间位置。

    2.最开始卡包是展开态,此时可滑动。当点击中间卡的时候,两边的卡片逐个收起在选中卡的下方(有动画效果),实现一个层叠展示的折叠态,当然你点击的卡片可能本来就在首或尾,那其实就是它边上的其他所有卡片收起堆叠。

    3.卡包处于折叠态时,只有中间的卡可点击,其他边上层叠收起的卡不能点击。此时点击中间卡片,整个卡包以点击的卡片为中心展开(有动画效果),同时又恢复到了展开态可左右滑动。相关的滑动切换以及点击事件暴露给外部方便做逻辑。

            听起来是不是很炫酷,我们直接看效果。

    cards

            下面分别是最左边、中间、最右边卡片收起时候的效果,收起时就不能再左右滑动了。

             下面是正常展开时的效果,如果左右还有卡片那大幅度滑动就会切换,小幅度滑动会回弹到当前卡片。

             简单说下我的实现思路和踩过的坑,大家可以参考,达到抛砖引玉的效果。

            最开始因为有卡片画廊效果,首先想到的是用ViewPager做,但是写了发现点击卡片后的卡包展开、收起效果不好实现,即便是动态改变ViewPager中Page之间的Margin或者设置新的PageTransformer都没法做到丝滑连贯的展开收起效果。因此含泪放弃,那没有捷径可走只好自己想办法了。

            这个时候想到可以在外层使用自定义的一个CardsHorizontalScrollView(继承自HorizontalScrollView),这个最外层的布局负责数据绑定,滑动处理,以便最终达到画廊的效果。即卡包展开态时可以像ViewPager一样滑动,大于滑动距离阈值就切换当前卡片,小于就回弹当前卡片。

            在CardsHorizontalScrollView中嵌套一个自定义的CardContainerLayout(继承自LinearLayout),在该布局中动态添加卡片布局,然后处理具体卡片的点击,这个时候点击可能是展开卡包也可能是折叠卡包。根据点击前的状态决定。

            单个卡片布局这里加上左、上、右、下的边距,水平方向让卡片恰好占满屏幕宽度,那在滑动切换卡片的时候每次滚动布局只需要滚动屏幕宽度的倍数即可。折叠效果中卡片一个压着一个的效果需要动态设置具体卡片的elevation属性来达到,另外要设置点横向偏移才能使一张卡相对另一张卡露出一点。这里用到了许多属性动画的操作。

            下面贴下核心代码。

            横向可滚动自定义布局

    1. package com.openld.seniorui.testcards
    2. import android.content.Context
    3. import android.graphics.ColorMatrix
    4. import android.graphics.ColorMatrixColorFilter
    5. import android.util.AttributeSet
    6. import android.view.MotionEvent
    7. import android.widget.HorizontalScrollView
    8. /**
    9. * author: lllddd
    10. * created on: 2022/7/30 21:45
    11. * description:卡片横向可滚动布局
    12. */
    13. class CardsHorizontalScrollView @JvmOverloads constructor(
    14. context: Context, attrs: AttributeSet? = null
    15. ) : HorizontalScrollView(context, attrs) {
    16. private var mScreenWidth = 0
    17. private var mWidth = 0
    18. var mIsFold = false
    19. var mOnCardScrollListener: OnCardScrollListener? = null
    20. private var mCardCounts = 0
    21. private lateinit var mCardList: List
    22. init {
    23. mScreenWidth = context.resources.displayMetrics.widthPixels
    24. }
    25. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    26. super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    27. mWidth = measuredWidth
    28. }
    29. private var downX: Float = 0F
    30. private var downScrollX = 0
    31. override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    32. if (ev!!.action == MotionEvent.ACTION_DOWN) {
    33. downX = ev.x
    34. downScrollX = scrollX
    35. } else if (ev.action == MotionEvent.ACTION_UP) {
    36. if (mIsFold) {
    37. return super.dispatchTouchEvent(ev)
    38. }
    39. val offsetX = ev.x - downX
    40. if (offsetX > 0F) {// 右滑
    41. val index = downScrollX / mScreenWidth
    42. if (offsetX > mScreenWidth / 5) {
    43. smoothScrollTo((index - 1) * mScreenWidth, 0)
    44. changeBackground(index - 1)
    45. if (index - 1 in 0 until mCardCounts) {
    46. mOnCardScrollListener?.onCardScrolled(index - 1)
    47. }
    48. } else {
    49. smoothScrollTo(index * mScreenWidth, 0)
    50. changeBackground(index)
    51. mOnCardScrollListener?.onCardScrolled(index)
    52. }
    53. return true
    54. } else if (offsetX < 0F) {// 左滑
    55. val index = downScrollX / mScreenWidth
    56. if (offsetX < -mScreenWidth / 5) {
    57. smoothScrollTo((index + 1) * mScreenWidth, 0)
    58. changeBackground(index + 1)
    59. if (index + 1 in 0 until mCardCounts) {
    60. mOnCardScrollListener?.onCardScrolled(index + 1)
    61. }
    62. } else {
    63. smoothScrollTo(index * mScreenWidth, 0)
    64. changeBackground(index)
    65. mOnCardScrollListener?.onCardScrolled(index)
    66. }
    67. return true
    68. } else {// 滑动距离过小
    69. }
    70. }
    71. return super.dispatchTouchEvent(ev)
    72. }
    73. private fun changeBackground(index: Int) {
    74. if (index in 0 until mCardCounts) {
    75. setBackgroundResource(mCardList[index].image)
    76. background.mutate().colorFilter =
    77. ColorMatrixColorFilter(ColorMatrix().apply {
    78. setScale(0.3F, 0.3F, 0.3F, 1F)
    79. })
    80. }
    81. }
    82. fun setCards(cardList: List<CardBean>) {
    83. if (cardList.isEmpty()) {
    84. return
    85. }
    86. this.mCardList = cardList
    87. this.mCardCounts = cardList.size
    88. if (childCount == 1 && getChildAt(0) is CardsContainerLayout) {
    89. (getChildAt(0) as CardsContainerLayout).setCards(mCardList)
    90. }
    91. changeBackground(0)
    92. }
    93. }

             卡片容器布局

    1. package com.openld.seniorui.testcards
    2. import android.animation.AnimatorSet
    3. import android.animation.ObjectAnimator
    4. import android.annotation.SuppressLint
    5. import android.content.Context
    6. import android.util.AttributeSet
    7. import android.view.LayoutInflater
    8. import android.view.animation.AccelerateDecelerateInterpolator
    9. import android.view.animation.AccelerateInterpolator
    10. import android.widget.ImageView
    11. import android.widget.LinearLayout
    12. import android.widget.TextView
    13. import android.widget.Toast
    14. import androidx.annotation.NonNull
    15. import com.openld.seniorui.R
    16. import kotlin.math.abs
    17. /**
    18. * author: lllddd
    19. * created on: 2022/7/29 22:51
    20. * description:卡片容器布局
    21. */
    22. class CardsContainerLayout @JvmOverloads constructor(
    23. context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    24. ) : LinearLayout(context, attrs, defStyleAttr) {
    25. private var mDensity = 0F
    26. private var mScreenWidth = 0
    27. private var mWidth = 0
    28. private var mHeight = 0
    29. private var mCardsCount = 0
    30. private var mCurrentIndex = 0
    31. private var mIsFold = false;
    32. private val DURATION = 600L
    33. private val DELAY = 60L
    34. var mOnCardClickListener: OnCardClickListener? = null
    35. init {
    36. orientation = LinearLayout.HORIZONTAL
    37. mDensity = context.resources.displayMetrics.density
    38. mScreenWidth = context.resources.displayMetrics.widthPixels
    39. }
    40. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    41. super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    42. mWidth = MeasureSpec.getSize(widthMeasureSpec)
    43. mHeight = MeasureSpec.getSize(heightMeasureSpec)
    44. }
    45. @SuppressLint("UseCompatLoadingForDrawables")
    46. fun setCards(@NonNull cards: List<CardBean>) {
    47. removeAllViews()
    48. for (index in cards.indices) {
    49. val childView = LayoutInflater.from(context).inflate(R.layout.item_card, this, false)
    50. val params =
    51. LinearLayout.LayoutParams((mWidth * 0.9F).toInt(), (mHeight * 0.9F).toInt())
    52. params.setMargins(
    53. (mWidth * 0.05F).toInt(),
    54. (mHeight * 0.05F).toInt(),
    55. (mWidth * 0.05F).toInt(),
    56. (mHeight * 0.05F).toInt()
    57. )
    58. childView.layoutParams = params
    59. val imageCard = childView.findViewById(R.id.img_card)
    60. imageCard.setImageResource(cards[index].image)
    61. val txtCard = childView.findViewById(R.id.txt_card)
    62. txtCard.text = cards[index].title
    63. childView.setOnClickListener {
    64. Toast.makeText(context, "点击了第${index}个卡片", Toast.LENGTH_SHORT).show()
    65. childView.elevation = 10F
    66. childView.isClickable = false
    67. mCurrentIndex = index
    68. if (mIsFold) {// 当前是折叠态
    69. // 点击展开
    70. clickToUnFold(index)
    71. } else {// 当前是展开态
    72. // 点击折叠
    73. clickToFold(index)
    74. }
    75. mIsFold = !mIsFold
    76. mOnCardClickListener?.onCardClicked(index, mIsFold)
    77. }
    78. addView(childView)
    79. }
    80. }
    81. /**
    82. * 折叠,当前点击了第index个卡片
    83. */
    84. @SuppressLint("Recycle")
    85. private fun clickToFold(index: Int) {
    86. val totalDelay =
    87. abs(index - 0).coerceAtLeast(abs(index - (mCardsCount - 1))) * DELAY + DURATION
    88. for (i in 0 until childCount) {
    89. getChildAt(i).isClickable = false
    90. var left = index - 1
    91. var right = index + 1
    92. while (left >= 0 || right < childCount) {
    93. if (left >= 0 && right < childCount) {
    94. val leftChild = getChildAt(left)
    95. val rightChild = getChildAt(right)
    96. leftChild.elevation = 10F - abs(index - left) * 0.1F
    97. rightChild.elevation = 10F - abs(index - right) * 0.1F
    98. val leftTranslationX = abs(index - left) * (mWidth - 100F)
    99. val animLeft =
    100. ObjectAnimator.ofFloat(leftChild, "translationX", 0F, leftTranslationX)
    101. val animLeftScaleX =
    102. ObjectAnimator.ofFloat(
    103. leftChild,
    104. "scaleX",
    105. 1F,
    106. 1F - abs(index - left) * 0.1F
    107. )
    108. val animLeftScaleY =
    109. ObjectAnimator.ofFloat(
    110. leftChild,
    111. "scaleY",
    112. 1F,
    113. 1F - abs(index - left) * 0.1F
    114. )
    115. val rightTranslationX = abs(index - right) * (-mWidth + 100F)
    116. val animRight =
    117. ObjectAnimator.ofFloat(rightChild, "translationX", 0F, rightTranslationX)
    118. val animRightScaleX = ObjectAnimator.ofFloat(
    119. rightChild,
    120. "scaleX",
    121. 1F,
    122. 1F - abs(index - left) * 0.1F
    123. )
    124. val animRightScaleY = ObjectAnimator.ofFloat(
    125. rightChild,
    126. "scaleY",
    127. 1F,
    128. 1F - abs(index - left) * 0.1F
    129. )
    130. val animSet = AnimatorSet().apply {
    131. duration = DURATION
    132. interpolator = AccelerateDecelerateInterpolator()
    133. playTogether(
    134. animLeft,
    135. animLeftScaleX,
    136. animLeftScaleY,
    137. animRight,
    138. animRightScaleX,
    139. animRightScaleY
    140. )
    141. startDelay = (abs(index - left) * DELAY).toLong()
    142. start()
    143. }
    144. left--;
    145. right++;
    146. } else if (left >= 0) {
    147. val leftChild = getChildAt(left)
    148. leftChild.elevation = 10F - abs(index - left) * 0.1F
    149. val leftTranslationX = abs(index - left) * (mWidth - 100F)
    150. val animLeft =
    151. ObjectAnimator.ofFloat(leftChild, "translationX", 0F, leftTranslationX)
    152. val animLeftScaleX =
    153. ObjectAnimator.ofFloat(
    154. leftChild,
    155. "scaleX",
    156. 1F,
    157. 1F - abs(index - left) * 0.1F
    158. )
    159. val animLeftScaleY =
    160. ObjectAnimator.ofFloat(
    161. leftChild,
    162. "scaleY",
    163. 1F,
    164. 1F - abs(index - left) * 0.1F
    165. )
    166. val animSet = AnimatorSet().apply {
    167. duration = DURATION
    168. interpolator = AccelerateDecelerateInterpolator()
    169. playTogether(animLeft, animLeftScaleX, animLeftScaleY)
    170. startDelay = (abs(index - left) * DELAY).toLong()
    171. start()
    172. }
    173. left--
    174. } else if (right < childCount) {
    175. val rightChild = getChildAt(right)
    176. rightChild.elevation = 10F - abs(index - right) * 0.1F
    177. val rightTranslationX = abs(index - right) * (-mWidth + 100F)
    178. val animRight =
    179. ObjectAnimator.ofFloat(rightChild, "translationX", 0F, rightTranslationX)
    180. val animRightScaleX = ObjectAnimator.ofFloat(
    181. rightChild,
    182. "scaleX",
    183. 1F,
    184. 1F - abs(index - right) * 0.1F
    185. )
    186. val animRightScaleY = ObjectAnimator.ofFloat(
    187. rightChild,
    188. "scaleY",
    189. 1F,
    190. 1F - abs(index - right) * 0.1F
    191. )
    192. val animSet = AnimatorSet().apply {
    193. duration = DURATION
    194. interpolator = AccelerateDecelerateInterpolator()
    195. playTogether(animRight, animRightScaleX, animRightScaleY)
    196. startDelay = (abs(index - left) * DELAY).toLong()
    197. start()
    198. }
    199. right++;
    200. } else {
    201. break
    202. }
    203. }
    204. postDelayed({
    205. getChildAt(index).isClickable = true
    206. }, totalDelay.toLong())
    207. }
    208. }
    209. /**
    210. * 展开,当前点击了第index个卡片
    211. */
    212. @SuppressLint("Recycle")
    213. private fun clickToUnFold(index: Int) {
    214. var left = index - 1
    215. var right = index + 1
    216. val totalDelay =
    217. abs(index - 0).coerceAtLeast(abs(1 + index - mCardsCount)) * DELAY + DURATION
    218. while (left >= 0 || right < childCount) {
    219. if (left >= 0 && right < childCount) {
    220. val leftChild = getChildAt(left)
    221. val rightChild = getChildAt(right)
    222. val animLeft =
    223. ObjectAnimator.ofFloat(leftChild, "translationX", 0F)
    224. val animLeftScaleX = ObjectAnimator.ofFloat(leftChild, "scaleX", 1F)
    225. val animLeftScaleY = ObjectAnimator.ofFloat(leftChild, "scaleY", 1F)
    226. val animRight =
    227. ObjectAnimator.ofFloat(rightChild, "translationX", 0F)
    228. val animRightScaleX = ObjectAnimator.ofFloat(rightChild, "scaleX", 1F)
    229. val animRightScaleY = ObjectAnimator.ofFloat(rightChild, "scaleY", 1F)
    230. val animSet = AnimatorSet().apply {
    231. duration = DURATION
    232. interpolator = AccelerateInterpolator()
    233. playTogether(
    234. animLeft,
    235. animLeftScaleX,
    236. animLeftScaleY,
    237. animRight,
    238. animRightScaleX,
    239. animRightScaleY
    240. )
    241. startDelay = (abs(index - left) * DELAY).toLong()
    242. start()
    243. }
    244. left--
    245. right++
    246. } else if (left >= 0) {
    247. val leftChild = getChildAt(left)
    248. val animLeft =
    249. ObjectAnimator.ofFloat(leftChild, "translationX", 0F)
    250. val animLeftScaleX = ObjectAnimator.ofFloat(leftChild, "scaleX", 1F)
    251. val animLeftScaleY = ObjectAnimator.ofFloat(leftChild, "scaleY", 1F)
    252. val animSet = AnimatorSet().apply {
    253. duration = DURATION
    254. interpolator = AccelerateInterpolator()
    255. playTogether(animLeft, animLeftScaleX, animLeftScaleY)
    256. startDelay = (abs(index - left) * DELAY).toLong()
    257. start()
    258. }
    259. left--
    260. } else if (right < childCount) {
    261. val rightChild = getChildAt(right)
    262. val animRight =
    263. ObjectAnimator.ofFloat(rightChild, "translationX", 0F)
    264. val animRightScaleX = ObjectAnimator.ofFloat(rightChild, "scaleX", 1F)
    265. val animRightScaleY = ObjectAnimator.ofFloat(rightChild, "scaleY", 1F)
    266. val animSet = AnimatorSet().apply {
    267. duration = DURATION
    268. interpolator = AccelerateInterpolator()
    269. playTogether(animRight, animRightScaleX, animRightScaleY)
    270. startDelay = (abs(index - left) * DELAY).toLong()
    271. start()
    272. }
    273. right++
    274. } else {
    275. break
    276. }
    277. }
    278. postDelayed({
    279. for (o in 0 until childCount) {
    280. getChildAt(o).isClickable = true
    281. getChildAt(o).elevation = 0F
    282. }
    283. }, totalDelay.toLong())
    284. }
    285. }
    1. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    2. xmlns:app="http://schemas.android.com/apk/res-auto"
    3. xmlns:tools="http://schemas.android.com/tools"
    4. android:layout_width="match_parent"
    5. android:layout_height="wrap_content">
    6. <androidx.cardview.widget.CardView
    7. android:layout_width="match_parent"
    8. android:layout_height="0dp"
    9. android:background="@drawable/bg_card_title"
    10. app:cardCornerRadius="16dp"
    11. app:cardElevation="5dp"
    12. app:cardUseCompatPadding="true"
    13. app:layout_constraintDimensionRatio="1920:1200"
    14. app:layout_constraintLeft_toLeftOf="parent"
    15. app:layout_constraintRight_toRightOf="parent"
    16. app:layout_constraintTop_toTopOf="parent">
    17. <ImageView
    18. android:id="@+id/img_card"
    19. android:layout_width="match_parent"
    20. android:layout_height="match_parent"
    21. android:scaleType="centerCrop"
    22. tools:ignore="ContentDescription"
    23. tools:src="@drawable/scene1" />
    24. <TextView
    25. android:id="@+id/txt_card"
    26. android:layout_width="wrap_content"
    27. android:layout_height="wrap_content"
    28. android:layout_marginStart="8dp"
    29. android:layout_marginTop="8dp"
    30. android:background="@drawable/bg_card_title"
    31. android:paddingHorizontal="8dp"
    32. android:paddingVertical="2dp"
    33. android:textColor="@color/black"
    34. android:textSize="14sp"
    35. tools:text="这是卡片的描述" />
    36. androidx.cardview.widget.CardView>
    37. androidx.constraintlayout.widget.ConstraintLayout>

             卡片点击监听器

    1. package com.openld.seniorui.testcards
    2. /**
    3. * author: lllddd
    4. * created on: 2022/7/30 13:23
    5. * description:卡片点击监听
    6. */
    7. interface OnCardClickListener {
    8. /**
    9. * 卡片点击的监听
    10. *
    11. * @param position 点击的卡片的位置
    12. * @param isFold 当前卡包是否折叠
    13. */
    14. fun onCardClicked(position: Int, isFold: Boolean)
    15. }

             卡片滑动监听器

    1. package com.openld.seniorui.testcards
    2. /**
    3. * author: lllddd
    4. * created on: 2022/7/31 9:38
    5. * description:卡片滚动监听
    6. */
    7. interface OnCardScrollListener {
    8. /**
    9. * 当前滚动到的卡片游标
    10. *
    11. * @param index 卡片游标
    12. */
    13. fun onCardScrolled(index: Int)
    14. }

             卡片Bean约定

    1. package com.openld.seniorui.testcards
    2. data class CardBean(val image: Int, val title: String) {
    3. }

             调用页面相关

    1. package com.openld.seniorui.testcards
    2. import android.annotation.SuppressLint
    3. import android.os.Build
    4. import android.os.Bundle
    5. import android.widget.Toast
    6. import androidx.annotation.RequiresApi
    7. import androidx.appcompat.app.AppCompatActivity
    8. import com.openld.seniorui.R
    9. class TestCardsActivity : AppCompatActivity() {
    10. private lateinit var mScrollView: CardsHorizontalScrollView
    11. private lateinit var mCardsContainerLayout: CardsContainerLayout
    12. private lateinit var mCardList: MutableList
    13. private var mWidth = 0
    14. @SuppressLint("ClickableViewAccessibility", "UseCompatLoadingForDrawables")
    15. @RequiresApi(Build.VERSION_CODES.M)
    16. override fun onCreate(savedInstanceState: Bundle?) {
    17. super.onCreate(savedInstanceState)
    18. setContentView(R.layout.activity_test_cards)
    19. mWidth = resources.displayMetrics.widthPixels
    20. mCardList = ArrayList()
    21. mCardList.add(CardBean(R.drawable.scene1, "阴阳师卡片 0"))
    22. mCardList.add(CardBean(R.drawable.scene2, "阴阳师卡片 1"))
    23. mCardList.add(CardBean(R.drawable.scene3, "阴阳师卡片 2"))
    24. mCardList.add(CardBean(R.drawable.scene4, "阴阳师卡片 3"))
    25. mCardList.add(CardBean(R.drawable.scene5, "阴阳师卡片 4"))
    26. mCardList.add(CardBean(R.drawable.scene6, "阴阳师卡片 5"))
    27. mCardList.add(CardBean(R.drawable.scene7, "阴阳师卡片 6"))
    28. mCardList.add(CardBean(R.drawable.scene8, "阴阳师卡片 7"))
    29. mCardList.add(CardBean(R.drawable.scene9, "阴阳师卡片 8"))
    30. mCardList.add(CardBean(R.drawable.scene10, "阴阳师卡片 9"))
    31. mCardList.add(CardBean(R.drawable.scene11, "阴阳师卡片 10"))
    32. mCardList.add(CardBean(R.drawable.scene12, "阴阳师卡片 11"))
    33. mCardList.add(CardBean(R.drawable.scene13, "阴阳师卡片 12"))
    34. mCardList.add(CardBean(R.drawable.scene14, "阴阳师卡片 13"))
    35. mScrollView = findViewById(R.id.scroll_container)
    36. mScrollView.mOnCardScrollListener = object : OnCardScrollListener {
    37. @SuppressLint("UseCompatLoadingForDrawables")
    38. override fun onCardScrolled(index: Int) {
    39. Toast.makeText(this@TestCardsActivity, "滑到了第${index}个卡片", Toast.LENGTH_SHORT).show()
    40. }
    41. }
    42. mScrollView.post {
    43. mScrollView.setCards(mCardList)
    44. }
    45. mCardsContainerLayout = findViewById(R.id.cards_container_layout)
    46. mCardsContainerLayout.mOnCardClickListener = object : OnCardClickListener {
    47. @SuppressLint("ClickableViewAccessibility")
    48. override fun onCardClicked(position: Int, isFold: Boolean) {
    49. mScrollView.mIsFold = isFold
    50. if (isFold) {
    51. mScrollView.setOnTouchListener { v, event -> true }
    52. } else {
    53. mScrollView.setOnTouchListener { v, event -> false }
    54. }
    55. }
    56. }
    57. }
    58. }

    1. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    2. xmlns:app="http://schemas.android.com/apk/res-auto"
    3. xmlns:tools="http://schemas.android.com/tools"
    4. android:layout_width="match_parent"
    5. android:layout_height="match_parent"
    6. android:background="@color/white"
    7. tools:context=".testcards.TestCardsActivity"
    8. tools:ignore="MissingDefaultResource">
    9. <com.openld.seniorui.testcards.CardsHorizontalScrollView
    10. android:id="@+id/scroll_container"
    11. android:layout_width="0dp"
    12. android:layout_height="0dp"
    13. android:fillViewport="true"
    14. android:orientation="horizontal"
    15. android:scrollbars="none"
    16. app:layout_constraintBottom_toBottomOf="parent"
    17. app:layout_constraintDimensionRatio="33:20"
    18. app:layout_constraintEnd_toEndOf="parent"
    19. app:layout_constraintStart_toStartOf="parent"
    20. app:layout_constraintTop_toTopOf="parent">
    21. <com.openld.seniorui.testcards.CardsContainerLayout
    22. android:id="@+id/cards_container_layout"
    23. android:layout_width="wrap_content"
    24. android:layout_height="match_parent" />
    25. com.openld.seniorui.testcards.CardsHorizontalScrollView>
    26. androidx.constraintlayout.widget.ConstraintLayout>

     完整工程及图片等资源有需要可以去项目中自取

  • 相关阅读:
    软件测试/测试开发丨学会与 AI 对话,高效提升学习效率
    ①Redis String 字符串类型【命令汇总】
    一个简单的音乐网站设计与实现(HTML+CSS)
    [游戏开发][Shader]ShaderToy通用模板转Unity-CG语言
    算法|每日一题|只出现一次的数字|位运算
    图的邻接矩阵表示
    立足小餐饮,“新名酒”江小白能走多远?
    东莞松山湖数据中心|莞服务器托管的优势
    systemd
    2022-09-28 Android APP 用interface 接口的形式对jni进行回调,实例测试
  • 原文地址:https://blog.csdn.net/ldld1717/article/details/126083036