Bỏ qua để đến nội dung

Android UI - Views

Phần này tổng hợp các câu hỏi về Android UI Views - hệ thống View truyền thống của Android.


Trả lời:

Aspect View ViewGroup
Definition Single UI element Container for Views
Can contain children
Examples TextView, Button, ImageView LinearLayout, ConstraintLayout
Hierarchy Leaf node Branch node
// View - single element
val textView = TextView(context).apply {
text = "Hello, World!"
textSize = 16f
}
// ViewGroup - container
val linearLayout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
addView(TextView(context).apply { text = "Child 1" })
addView(TextView(context).apply { text = "Child 2" })
}

💡 Mẹo: View.java có hơn 34,000 dòng code! Tránh tạo View không cần thiết để optimize performance.

📚 Tìm hiểu thêm: AndroidView trong Compose


Q: View Lifecycle gồm những giai đoạn nào?

Phần tiêu đề “Q: View Lifecycle gồm những giai đoạn nào?”

Trả lời:

flowchart TD
A["onFinishInflate()"] --> B["onAttachedToWindow()"]
B --> C["onMeasure()"]
C --> D["onLayout()"]
D --> E["onDraw()"]
E --> F["User Interaction"]
F --> G["onDetachedFromWindow()"]
style E fill:#c8e6c9
style F fill:#fff3e0
Phase Method Purpose
Creation onFinishInflate() After XML inflation
Attachment onAttachedToWindow() View added to window
Measurement onMeasure() Determine size
Layout onLayout() Position children
Drawing onDraw() Render on canvas
Interaction onTouchEvent() Handle user input
Detachment onDetachedFromWindow() Cleanup
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// Start animations, register listeners
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Calculate desired size
val width = calculateWidth(widthMeasureSpec)
val height = calculateHeight(heightMeasureSpec)
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas) {
// Draw content
canvas.drawRect(...)
canvas.drawText(...)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// Cleanup resources
}
}

Trả lời:

RecyclerView tái sử dụng ViewHolders để hiển thị large datasets hiệu quả.

flowchart LR
A["Adapter"] --> B["ViewHolder Pool"]
B --> C["LayoutManager"]
C --> D["RecyclerView"]
D -.->|"recycle"| B

Components:

Component Responsibility
Adapter Create ViewHolders, bind data
ViewHolder Hold view references
LayoutManager Position items
ItemDecoration Dividers, spacing
ItemAnimator Add/remove animations
class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
private var items = listOf<User>()
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val binding = ItemUserBinding.bind(view)
fun bind(user: User) {
binding.nameText.text = user.name
binding.avatar.load(user.avatarUrl)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = ItemUserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(view.root)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
fun submitList(newItems: List<User>) {
items = newItems
notifyDataSetChanged() // Use DiffUtil for better performance
}
}

Trả lời:

DiffUtil tính toán minimal updates cần thiết để biến đổi list cũ thành list mới.

// ❌ Without DiffUtil - redraws everything
adapter.submitList(newList)
adapter.notifyDataSetChanged()
// ✅ With DiffUtil - only updates changed items
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
class UserAdapter : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// ...
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
// Usage
adapter.submitList(newList) // DiffUtil handles updates automatically

Benefits:

  • ✅ Smooth animations
  • ✅ Better performance
  • ✅ Preserves scroll position

Trả lời:

// 1. Use setHasFixedSize for fixed-size items
recyclerView.setHasFixedSize(true)
// 2. Set item view cache size
recyclerView.setItemViewCacheSize(20)
// 3. Use RecycledViewPool for multiple RecyclerViews
val sharedPool = RecyclerView.RecycledViewPool()
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)
// 4. Avoid nested RecyclerViews (use ConcatAdapter)
val concatAdapter = ConcatAdapter(headerAdapter, contentAdapter, footerAdapter)
recyclerView.adapter = concatAdapter
// 5. Use stable IDs
class MyAdapter : RecyclerView.Adapter<ViewHolder>() {
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long = items[position].id
}
// 6. Prefetch for nested scrolling
(recyclerView.layoutManager as LinearLayoutManager)
.initialPrefetchItemCount = 4

Trả lời:

3 cách tạo Custom View:

class UserInfoView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private val binding: ViewUserInfoBinding
init {
orientation = HORIZONTAL
binding = ViewUserInfoBinding.inflate(
LayoutInflater.from(context), this
)
// Read custom attributes
context.obtainStyledAttributes(attrs, R.styleable.UserInfoView).use {
binding.nameText.text = it.getString(R.styleable.UserInfoView_userName)
}
}
fun setUser(user: User) {
binding.nameText.text = user.name
binding.avatar.load(user.avatarUrl)
}
}
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f)
invalidate() // Request redraw
}
override fun onDraw(canvas: Canvas) {
val centerX = width / 2f
val centerY = height / 2f
val radius = minOf(centerX, centerY) - paint.strokeWidth
// Draw background circle
paint.color = Color.LTGRAY
canvas.drawCircle(centerX, centerY, radius, paint)
// Draw progress arc
paint.color = Color.BLUE
canvas.drawArc(
centerX - radius, centerY - radius,
centerX + radius, centerY + radius,
-90f, 360f * progress, false, paint
)
}
}
class SquareImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Make it square
val size = minOf(measuredWidth, measuredHeight)
setMeasuredDimension(size, size)
}
}

Trả lời:

Touch events được dispatch từ parent → children (tunnel down), xử lý từ children → parent (bubble up).

flowchart TD
A["dispatchTouchEvent()"] --> B["onInterceptTouchEvent()<br/>(ViewGroup only)"]
B -->|"not intercepted"| C["Child dispatchTouchEvent()"]
B -->|"intercepted"| D["onTouchEvent()"]
C --> E["Child onTouchEvent()"]
E -->|"not handled"| D
style B fill:#fff3e0
style D fill:#c8e6c9
Method Purpose Return
dispatchTouchEvent() Dispatch to children true if handled
onInterceptTouchEvent() Parent intercepts true to steal
onTouchEvent() Handle touch true if consumed
class CustomViewGroup : FrameLayout {
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
// Return true to steal touch from children
return when (ev.action) {
MotionEvent.ACTION_MOVE -> {
val dx = abs(ev.x - startX)
dx > touchSlop // Intercept if horizontal swipe
}
else -> false
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
return true
}
MotionEvent.ACTION_MOVE -> {
// Handle drag
return true
}
MotionEvent.ACTION_UP -> {
// Handle release
return true
}
}
return super.onTouchEvent(event)
}
}

Trả lời:

Layout Nesting Performance Flexibility
ConstraintLayout Flat ✅ Best ✅ Highest
LinearLayout Can nest ⚠️ OK Medium
RelativeLayout 2-pass ⚠️ Slower Medium
FrameLayout Flat ✅ Fast Low
<!-- ConstraintLayout - flat hierarchy -->
<androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/name"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="@id/avatar"/>
<TextView
android:id="@+id/email"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name"/>
</androidx.constraintlayout.widget.ConstraintLayout>

ConstraintLayout features:

  • Chains, barriers, guidelines
  • Percentage sizing
  • Circular positioning
  • MotionLayout animations

Trả lời:

MeasureSpec là 32-bit int chứa mode (2 bits) và size (30 bits).

Mode Description Example
EXACTLY Exact size required 100dp, match_parent
AT_MOST Max size limit wrap_content with limit
UNSPECIFIED No constraints ScrollView children
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val desiredWidth = 200.dpToPx()
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth
}
setMeasuredDimension(width, calculatedHeight)
}

Trả lời:

Type Use Case API
Property Animation Any property ObjectAnimator, ValueAnimator
View Animation Simple transforms XML, Animation class
Transition Layout changes TransitionManager
MotionLayout Complex gestures MotionScene
// ObjectAnimator
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).apply {
duration = 300
interpolator = AccelerateDecelerateInterpolator()
start()
}
// ViewPropertyAnimator (simpler)
view.animate()
.translationX(100f)
.alpha(0.5f)
.setDuration(300)
.setInterpolator(AccelerateDecelerateInterpolator())
.start()
// Transition
TransitionManager.beginDelayedTransition(container, Fade())
view.visibility = View.GONE // Animates automatically
// MotionLayout
motionLayout.transitionToEnd()
motionLayout.setTransitionDuration(500)

Practice Why
Keep hierarchy flat Faster measure/layout
Use ConstraintLayout Single-pass layout
Recycle ViewHolders Memory efficiency
Use DiffUtil Minimal updates
Avoid requestLayout() in loops Expensive
Use invalidate() for redraw Not full relayout
Clean up in onDetachedFromWindow() Prevent leaks
// ❌ Bad - deep hierarchy
LinearLayout > LinearLayout > LinearLayout > TextView
// ✅ Good - flat with ConstraintLayout
ConstraintLayout > TextView, TextView, TextView

📚 Tìm hiểu thêm: AndroidView trong Compose