安卓使用技巧

2022/2/9

# 安卓手机安装谷歌服务

# 手动模式

首先必须了解 Google 套件正常运行是由三个基础构建而成:服务框架,账户管理程序,Play 服务。

一般大家用的都是商店为基础,那就再加个 商店 组成四件套。

安装四个谷歌软件,软件安装的顺序很关键,(顺序出错要么闪退要么卡死)。

软件的顺序错了的话,基本上是怎么折腾也不会有结果的。

  1. 安装 Google Service Framework (谷歌服务框架 GSF)。
  2. 安装 Google Account Manager (谷歌账户管理程序,貌似已经不需要了?2022-05-20)。
  3. 安装 Google Play Service (Google Play 服务又称 GMS)。
  4. 安装 商店 Google Play Store。

可以从此处www.apkmirror.com (opens new window)下载所需的 APK 及对应版本。

# 自动模式

使用谷歌安装器

  1. 点击下载Go 安装器 (opens new window)

# 运行安卓项目

Android Studio>sidebar>project 切换找不到 app moudle 和 project moudle

    1. app moudle

为什么找不到呢,估计是 IDE 抽风 先记录下:

  1. 剪切一下 settings.gradle 下的 include ‘:app’,
  2. 然后 Sync Project with gradle files,
  3. 再然后,粘贴回去 include ‘:app’,
  4. 再 build 一下就可以了.
    1. project moudle
  1. 去项目根目录找到.idea 文件夹
  2. 找到.idea 后删除
  3. 重启 AS 就可以了
    1. Android Studio > sidebar 控制显示隐藏:通过 view -> tool windows -> 控制

# 红米手机

  1. 进入工程模式:*#*#6484#*#*,用于调试屏幕等。
  2. Google“安全码”在哪里获取?首先在手机中打开科学上网软件,运行 Google Play,依次点击右上角头像-“管理您的 Google 帐号”-“安全性”,点击“安全码(获取一次性验证码以验证您的身份)”。

# debug

  1. 查看 APP 的 SHA1、SHA256 和 MD5 值: keytool -list -v -keystore C:/Users/uia68502/.android/debug.keystore -alias androiddebugkey password: android

  2. APP name 以app/build.gradle 中的applicationId为准,AndroidManifest.xml为辅。

  3. Error running second Activity: The activity must be exported or contain an intent-filter编译能成功,但是在虚拟机或真机上面调试时,弹出这个错误,后来查了一下,要在 AndroidManifest.xml 中,把每个窗口都加上一句:android:exported="true"

比如:

<activity android:name=".MainActivity"
    android:exported="true">
</activity>
<activity android:name=".TextView_Paomadeng"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
1
2
3
4
5
6
7
8
9
10
11

这样就可以调试成功了。

  1. MacOSX 修改 Android Studio 使用本地 gradle, gradle-wrapper.properties: distributionUrl=file:/Users/eric/.gradle/wrapper/dists/gradle-5.6.4-all/ankdp27end7byghfw1q2sw75f/gradle-5.6.4-all.zip

# ijkplayer 编译 so 库

ijkplayer 编译 so 库 (opens new window)

# adb

如何通过 adb 把文件从手机上拉下来?

  1. 如果有多个模拟器或者手机连接着电脑,先通过adb devices -l查看一下设备号,然后通过adb -s 设备号 pull /sdcard/AIUI/cfg/aiui.cfg aiui.cfg,即adb -s deviceId pull remote/path local/path

  2. D:\>adb pull /sdcard/AIUI/cfg/aiui.cfg aiui.cfg

  3. 使用 adb pull 命令并不能直接使用通配符(如 **)来批量复制文件。adb pull 命令只支持指定单个文件或整个目录。

  4. 复制整个文件夹到指定目录下:adb -s 4a790be6 pull /storage/emulated/0/DCIM/Camera/ /Users/eric/phone

  5. 如果您只想复制 MP4 文件,可以使用 adb shellfind 命令结合 pull 来实现:

    • 使用 find 命令查找 MP4 文件:adb -s 4a790be6 shell find /storage/emulated/0/DCIM/Camera/ -name "*.mp4"
    • 使用 pull 命令逐个复制文件:adb -s 4a790be6 shell find /storage/emulated/0/DCIM/Camera/ -name "*.mp4" > mp4_files.txt,由于 adb pull 不支持直接批量操作,您可以将所有 MP4 文件路径输出到一个文本文件,然后使用脚本逐个复制。可以使用该命令创建一个文件并将路径保存
    • 在 Mac 上读取该文件并逐个复制:while read line; do adb -s 4a790be6 pull "$line" /Users/eric/phone; done < mp4_files.txt

# Mac 连接小米手机真机调试

  1. 小米手机开启开发者选项:
    • 打开设置:在您的小米手机上,找到并打开“设置”应用。
    • 找到“我的设备”点击进入,然后滚动到“全部参数与信息”选项并点击进入。
    • 连续点击“OS 版本号”,直到看到提示“您已处于开发者模式,无需进行此操作”。
    • 返回设置:返回到设置主菜单,滚动到最下面,点击“更多设置”,在最下面您会看到“开发者选项”。
  2. 开启 USB 调试:
    • 进入开发者选项
    • 开启 USB 调试:在开发者选项中,找到“USB 调试”并开启它。系统可能会弹出警告,点击“确定”以确认。
    • 如果想用无线调试也可以打开,并自行设置。
  3. 连接手机和电脑:
    • 使用 USB 数据线:使用 USB 数据线将小米手机连接到 Mac 电脑。
    • 选择 USB 连接模式:在手机上,您可能会看到一个提示,询问您选择 USB 连接模式。选择“传输文件(MTP)”或“USB 调试”模式。
  4. 安装 ADB 工具:使用 Homebrew 安装 ADB
  5. 验证连接:打开终端运行 ADB 命令 -- adb devices -l,查看输出的内容里有无设备号,如果有,则表示连接成功。
  6. 在 Android Studio 中运行项目时,选择连接设备即可。

# 安卓开发笔记

# 常用图标绘制库

  1. MPAndroidChart

# ViewBinding

使用到了 Kotlin 委托,ActivityViewBindings就是一个 Kotlin 委托类,当获取 binding 的时候,去触发fun getValue(thisRef: A, property: KProperty<*>): T,而vbFactory: (View) -> T是我们从MainActivity传入的,实际就是在调用ActivityMainBinding::bind

看似炫酷的被施了魔法的private val binding :ActivityMainBinding by viewbind()实现ViewBinding,其实就是用到了Kotlin委托,并在委托类的get()方法中,通过不反射 / 反射 的方式 调用ActivityMainBinding.java的 inflatebind方法,然后,可以将setContentView()也在此处进行调用,这样,就不用再去写ViewBinding常规的那些代码了。

# 原生做法

android {
    buildFeatures {
        viewBinding true
    }
}
1
2
3
4
5
// 重点:lateinit延迟初始化/懒加载。ActivityMainBinding即xml的名称倒过来写再加个Binging后缀
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
  // 重点:inflate
	binding = ActivityMainBinding.inflate(layoutInflater)
  // 重点:binding.root
	setContentView(binding.root)

  // 之后即可直接使用组件id来操作组件
}
1
2
3
4
5
6
7
8
9
10
11
12

对于通过 include 方式引入的子布局 xml 中的组件,还要再单独 binding

// 先定义
private var _binding: FragmentWeatherBinding? = null
private val binding get() = _binding!!
private lateinit var rainChancePieChart: ColorfulRingProgressView

// 比如:需重写onCreateView方法,然后在父binding中通过findViewById找到子binding,之后才能使用子binding
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    _binding = FragmentWeatherBinding.inflate(inflater, container, false)

    val weatherInfoChartBinding = LayoutWeatherInfoChartBinding.bind(binding.root.findViewById(R.id.layout_weather_info_chart))
    rainChancePieChart = weatherInfoChartBinding.rainChancePieChart

    return binding.root
}

// 组件销毁时需要手动清空_binding
override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 第三方库

  1. com.github.kirich1409:viewbindingpropertydelegate-full:采用不反射的方式,性能上会比较好
// 重点:Fragment这里要传入布局ID
class ProfileFragment : Fragment(R.layout.profile) {

    // reflection API and ViewBinding.bind are used under the hood
    private val viewBinding: ProfileBinding by viewBinding()

    // reflection API and ViewBinding.inflate are used under the hood
    private val viewBinding: ProfileBinding by viewBinding(createMethod = CreateMethod.INFLATE)

    // no reflection API is used under the hood,在viewBinding()需要传参
    private val viewBinding by viewBinding(ProfileBinding::bind)
}
class ProfileActivity : AppCompatActivity(R.layout.profile) {

    // reflection API is used under the hood
    private val viewBinding: ProfileBinding by viewBinding(R.id.container)

    // no reflection API is used under the hood
    private val viewBinding by viewBinding(ProfileBinding::bind, R.id.container)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. com.hi-dhl:binding
// ViewBinding
val binding: ActivityViewBindBinding by viewbind()

// DataBinding
val binding: ActivityDataBindBinding by databind(R.layout.activity_data_bind)
// or
val binding: ActivityDataBindBinding by databind()

init {
    with(binding) {
        result.setText("Use DataBinding and ViewBinding in Custom ViewGroup")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. com.github.DylanCaiCoding.ViewBindingKTX
class MainActivity : AppCompatActivity() {

  private val binding: ActivityMainBinding by binding()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.tvHelloWorld.text = "Hello Android!"
  }
}
class HomeFragment : Fragment(R.layout.fragment_home) {

  private val binding: FragmentHomeBinding by binding()
  private val childBinding: LayoutChildBinding by binding(Method.INFLATE)

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.container.addView(childBinding.root)
  }
}
class FooAdapter : BaseQuickAdapter<Foo, BaseViewHolder>(R.layout.item_foo) {

  override fun convert(holder: BaseViewHolder, item: Foo) {
    holder.getBinding(ItemFooBinding::bind).apply {
      tvFoo.text = item.value
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Retrofit 网络请求

结合 Gson 对 JSON 数据进行解析

  • com.squareup.retrofit2:retrofit
  • com.squareup.retrofit2:converter-gson
private val retrofit = Retrofit.Builder()
    .baseUrl("https://mock.apipost.net/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

private val weatherApiService: WeatherApiService =
    retrofit.create(WeatherApiService::class.java)

// 在onViewCreated中调用fetchWeatherData()即可

private fun fetchWeatherData() {
    Log.d("WeatherFragment", "fetchWeatherData called")
    weatherApiService.getWeatherData().enqueue(object : Callback<WeatherResponse> {
        override fun onResponse(
            call: Call<WeatherResponse>,
            response: Response<WeatherResponse>
        ) {

            Log.d("fetchWeatherData", response.body().toString())
            if (response.isSuccessful && response.body() != null) {
                val weatherData = response.body()!!
                val rainChance = weatherData.result.additional_info.rain_chance.value.toFloat()

                with(weatherInfoChartBinding) {
                    // update ColorfulRingProgressView 填充数据
                    rainChancePieChart.percent = rainChance
                }
            } else {
                Log.e("fetchWeatherData", "response is not successful!")
            }
        }

        override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
            Log.e("fetchWeatherData", "Request failed: ${t.message}")
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// WeatherApiService.kt  写个interface就行
interface WeatherApiService {
    @GET("mock/3093cexxxx2404c/weather/mock?apipost_id=3093xxx4e")
    fun getWeatherData(): Call<WeatherResponse>
}
1
2
3
4
5
// WeatherResponse
data class WeatherResponse(
    val status: Int,
    val result: WeatherResult,
    val message: String
)
// ...
1
2
3
4
5
6
7

# 自定义圆环"Chart"

<com.example.weather.fragment.weather.ColorfulRingProgressView
    android:id="@+id/rainChancePieChart"
    android:layout_width="70dp"
    android:layout_height="70dp"
    android:layout_gravity="center"
    app:bgColor="@color/ring_bg_color"
    app:fgColorEnd="@color/ring_color"
    app:fgColorStart="@color/ring_color"
    app:percent="15"
    app:startAngle="45"
    app:strokeWidth="4dp" />
1
2
3
4
5
6
7
8
9
10
11
package com.example.weather.fragment.weather

import android.animation.ObjectAnimator
import android.animation.TimeInterpolator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import com.example.weather.R

class ColorfulRingProgressView(private val mContext: Context, attrs: AttributeSet?) : View(
    mContext, attrs
) {
    private var mPercent = 75f
    private var mStrokeWidth = 0f
    private var mBgColor = -0x1e1e1f
    private var mStartAngle = 0f
    private var mFgColorStart = -0x1c00
    private var mFgColorEnd = -0xb800

    private var mShader: LinearGradient? = null
    private var mOval: RectF? = null
    private var mBgPaint: Paint? = null // 背景画笔
    private var mFgPaint: Paint? = null // 前景画笔

    private var animator: ObjectAnimator? = null

    init {
        val a =
            mContext.theme.obtainStyledAttributes(attrs, R.styleable.ColorfulRingProgressView, 0, 0)

        try {
            mBgColor = a.getColor(R.styleable.ColorfulRingProgressView_bgColor, -0x1e1e1f)
            mFgColorEnd = a.getColor(R.styleable.ColorfulRingProgressView_fgColorEnd, -0xb800)
            mFgColorStart = a.getColor(R.styleable.ColorfulRingProgressView_fgColorStart, -0x1c00)
            mPercent = a.getFloat(R.styleable.ColorfulRingProgressView_percent, 0f)
            mStartAngle = a.getFloat(R.styleable.ColorfulRingProgressView_startAngle, 0f) + 270
            mStrokeWidth = a.getDimensionPixelSize(
                R.styleable.ColorfulRingProgressView_strokeWidth, dp2px(21f)
            ).toFloat()
        } finally {
            a.recycle()
        }

        init()
    }

    private fun init() {
        // 此处为了展示一粗一细两个圆环的效果
        mBgPaint = Paint() // 初始化背景画笔
        mBgPaint!!.isAntiAlias = true
        mBgPaint!!.style = Paint.Style.STROKE
        mBgPaint!!.strokeWidth = mStrokeWidth // 背景圆环的宽度
        mBgPaint!!.color = mBgColor

        mFgPaint = Paint() // 初始化前景画笔
        mFgPaint!!.isAntiAlias = true
        mFgPaint!!.style = Paint.Style.STROKE
        mFgPaint!!.strokeWidth = mStrokeWidth + dp2px(4f) // 前景圆环的宽度,比背景宽4dp
        mFgPaint!!.strokeCap = Paint.Cap.SQUARE
    }

    private fun dp2px(dp: Float): Int {
        return (mContext.resources.displayMetrics.density * dp + 0.5f).toInt()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.drawArc(mOval!!, 0f, 360f, false, mBgPaint!!)
        // 绘制前景圆环
        mFgPaint!!.setShader(mShader) // 设置前景画笔的着色器
        canvas.drawArc(mOval!!, mStartAngle, mPercent * 3.6f, false, mFgPaint!!) // 绘制前景圆环
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)

        updateOval()

        mShader = LinearGradient(
            mOval!!.left,
            mOval!!.top,
            mOval!!.left,
            mOval!!.bottom,
            mFgColorStart,
            mFgColorEnd,
            Shader.TileMode.MIRROR
        )
    }

    var percent: Float
        get() = mPercent
        set(mPercent) {
            this.mPercent = mPercent
            refreshTheLayout()
        }

    var strokeWidth: Float
        get() = mStrokeWidth
        set(mStrokeWidth) {
            this.mStrokeWidth = mStrokeWidth
            mBgPaint!!.strokeWidth = mStrokeWidth // 设置背景圆环的宽度
            mFgPaint!!.strokeWidth = mStrokeWidth + dp2px(4f) // 设置前景圆环的宽度,比背景宽4dp
            updateOval()
            refreshTheLayout()
        }

    private fun updateOval() {
        val xp = paddingLeft + paddingRight
        val yp = paddingBottom + paddingTop
        mOval = RectF(
            paddingLeft + mStrokeWidth,
            paddingTop + mStrokeWidth,
            paddingLeft + (width - xp) - mStrokeWidth,
            paddingTop + (height - yp) - mStrokeWidth
        )
    }

    fun setStrokeWidthDp(dp: Float) {
        this.mStrokeWidth = dp2px(dp).toFloat()
        mBgPaint!!.strokeWidth = mStrokeWidth // 设置背景圆环的宽度
        mFgPaint!!.strokeWidth = mStrokeWidth + dp2px(4f) // 设置前景圆环的宽度,比背景宽4dp
        updateOval()
        refreshTheLayout()
    }

    fun refreshTheLayout() {
        invalidate()
        requestLayout()
    }

    var fgColorStart: Int
        get() = mFgColorStart
        set(mFgColorStart) {
            this.mFgColorStart = mFgColorStart
            mShader = LinearGradient(
                mOval!!.left,
                mOval!!.top,
                mOval!!.left,
                mOval!!.bottom,
                mFgColorStart,
                mFgColorEnd,
                Shader.TileMode.MIRROR
            )
            refreshTheLayout()
        }

    var fgColorEnd: Int
        get() = mFgColorEnd
        set(mFgColorEnd) {
            this.mFgColorEnd = mFgColorEnd
            mShader = LinearGradient(
                mOval!!.left,
                mOval!!.top,
                mOval!!.left,
                mOval!!.bottom,
                mFgColorStart,
                mFgColorEnd,
                Shader.TileMode.MIRROR
            )
            refreshTheLayout()
        }


    var startAngle: Float
        get() = mStartAngle
        set(mStartAngle) {
            this.mStartAngle = mStartAngle + 270
            refreshTheLayout()
        }

    @JvmOverloads
    fun animateIndeterminate(
        durationOneCircle: Int = 800,
        interpolator: TimeInterpolator? = AccelerateDecelerateInterpolator()
    ) {
        animator = ObjectAnimator.ofFloat(this, "startAngle", startAngle, startAngle + 360)
        interpolator?.let { animator?.interpolator = it }
        animator?.duration = durationOneCircle.toLong()
        animator?.repeatCount = ValueAnimator.INFINITE
        animator?.repeatMode = ValueAnimator.RESTART
        animator?.start()
    }

    fun stopAnimateIndeterminate() {
        if (animator != null) {
            animator!!.cancel()
            animator = null
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198

# ORM 框架

ORM(Object-Relational Mapping,对象关系映射)是一种软件技术,用于在关系型数据库和面向对象编程语言之间建立映射关系。它的目标是将对象模型和关系数据库之间的数据转换和操作自动化。

ORM 框架的作用是简化开发人员处理数据库的过程。它将数据库表和记录映射到编程语言中的对象和属性上,提供了一种更直观、面向对象的方式来操作和访问数据。通过使用 ORM,开发人员可以使用面向对象的编程语言来进行数据库操作,而无需编写原始的 SQL 查询。

使用 ORM 框架可以简化数据库操作的编写和维护工作,提高开发效率和代码可读性。

  • 几种常用的 ORM 框架

    • Room:可以视作官方推荐的方案代表,也是 google 推出的方案,用的软件多,支持数据驱动,google 官方支持,支持 sql 语句,基于 sqlite
    • GreenDao:第三方封装的基于 sqlite 的 orm 框架,大量的使用者,较为中庸,没有特别明显的短板与长处,缺少对于数据变化的动态监听,不支持数据驱动式 coding,缺少一些新特性。部分功能不支持 kotlin:在使用 kotlin 文件编写 entity 时无法构建成功,必须使用 Java 代码写 entity。
    • Realm:可以支持跨平台,基于 MongoDB 非关系型数据库
    • ObjectBox:操作速度更快,greenDao 同家出品的数据库框架,支持 liveData,也支持 Flow、协程,支持懒加载,基于 nosql,非关系型数据库
    • SQLite:跨平台的原生数据库能力
  • 读写速度 ● Realm,相比于其他插入大批量新数据慢一点,小批量插入差异不明显,大批量差异和 sqlite 类的慢 10%-20%左右;数据更新速度比 sqlite、greendao、room 等快一倍左右;删除和读取数据的操作速度快到无法比较,可能存在测试用例/缓存等问题导致数据不可信; ● ObjectBox,除了删除操作外,其他的耗时都优于基于 sqlite 的 orm 框架,读取操作慢于 realm; ● Room,在删除操作上优于其他 sqlite 框架,其他操作和 sqlite 的速度持平/稍慢于 sqlite; ● GreenDao,读取操作上优于其他 sqlite 框架(30%-60%),其他操作持平; ● Sqlite,更新操作稍快(10%-30%),其他持平

# 常用控件

# TextView

<TextView
    android:id="@+id/tvHelloWorld"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    android:textSize="24sp"
    android:textColor="@color/black"
    android:layout_margin="16dp"
    android:gravity="center"
    android:background="@color/white"
    android:padding="8dp"
    android:textIsSelectable="true" // 复制其中的内容
    android:descendantFocusability="blocksDescendants"  // 拦截事件的消费,使得textView无法消费触摸的事件
/>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 输入框与输入法遮挡:在AndroidManifest.xml对应的 Activity 里添加 android:windowSoftInputMode="adjustPan"或是android:windowSoftInputMode="adjustResize"属性
  • adjustPan:整个界面向上平移,使输入框露出,它不会改变界面的布局;界面整体可用高度还是屏幕高度
  • adjustResize:需要界面的高度是可变的,或者说 Activity 主窗口的尺寸是可以调整的,如果不能调整,则不会起作用。

# EditText

EditText 输入时被输入法挤压滚动到顶部:

// AndManifest.xml
android:windowSoftInputMode="adjustResize|stateHidden"
// xml layout viewGroup
android:fitsSystemWindows="true"
1
2
3
4

# RecyclerView

一直展示滚动条

android:scrollbarAlwaysDrawVerticalTrack="true"
android:scrollbars="vertical"
android:overScrollMode="always"
android:fadeScrollbars="false"
android:scrollbarFadeDuration="0"
1
2
3
4
5

# 布局

# ConstrainLayout 约束布局

通过辅助线来控制具体的位置,当然也可以设置边距啥的。

上次更新: 9/11/2024