Custom View — یک کامپوننت رابط کاربری سفارشی در Android است که با ارثبری از کلاس View یا زیرکلاسهای آن (Button, TextView, ImageView) و بازنویسی متدهای کلیدی ایجاد میشود: onDraw برای ترسیم، onMeasure برای اندازهگیری ابعاد و onTouchEvent برای پردازش لمسها. طبق Google Android Developer Guide (2024)، Custom View زمانی استفاده میشود که کامپوننتهای استاندارد Android SDK رفتار یا ظاهر مورد نیاز را ارائه نمیدهند — مثلاً برای انیمیشنهای سفارشی، اشکال غیراستاندارد، نمودارهای تخصصی و عناصر بازی.
نکات اصلی
Custom View — کلاسی است که از android.view.View (یا یکی از زیرکلاسهای آن) ارثبری میکند و متدهای سیستمی را برای پیادهسازی منطق ترسیم، اندازهگیری و پردازش رویدادهای خود بازنویسی میکند. Custom View مکانیزم بنیادی Android برای ایجاد کامپوننتهای UI منحصر به فرد است.
Android دو رویکرد برای ایجاد Custom View ارائه میدهد: ارثبری از View (ترسیم کاملاً سفارشی از طریق onDraw) و ارثبری از زیرکلاس View موجود (مثلاً بازنویسی Button یا TextView برای افزودن قابلیتها در عین حفظ رفتار پایه).
سازنده پایه Custom View باید Context و AttributeSet را دریافت کند — این به سیستم اجازه میدهد View را از کد XML ایجاد کند. اگر View فقط از کد استفاده شود، سازنده با Context کافی است. سازنده سوم با style برای پشتیبانی از تمهای Android لازم است.
class CustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
strokeWidth = 4f
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f,
height / 2f,
50f, paint)
}
}
حداقل Custom View از سازنده و onDraw تشکیل شده است. با این حال برای کار صحیح در سیستم Layout Android باید onMeasure نیز بازنویسی شود — در غیر این صورت View ممکن است با ابعاد صفر نمایش داده شود یا به پارامترهای layout wrap_content واکنش نادرست نشان دهد.
طبق Google I/O 2023، Viewهای سفارشی در 65% برنامههای top 100 Google Play استفاده میشوند. شایعترین دلایل ایجاد Custom View: انیمیشنهای سفارشی (35%)، نمودارها و دیاگرامهای سفارشی (25%)، عناصر کنترل تخصصی (20%) و کامپوننتهای برندسازی شده (20%).
onMeasure — متدی است که در آن View ابعاد مورد نظر خود را بر اساس MeasureSpec (محدودیتهای والد) به سیستم اعلام میکند. بدون پیادهسازی صحیح onMeasure، View ممکن است با wrap_content ارتفاع صفر داشته باشد یا با match_parent تمام مساحت صفحه را بگیرد.
MeasureSpec از حالت (EXACTLY, AT_MOST, UNSPECIFIED) و مقدار تشکیل شده است. EXACTLY — والد اندازه دقیق تعیین کرده (match_parent یا مقدار ثابت). AT_MOST — والد حداکثر را تعیین کرده (wrap_content). UNSPECIFIED — بدون محدودیت (ScrollView, ListView).
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
val desiredWidth = paddingLeft + paddingRight
+ DEFAULT_WIDTH
val desiredHeight = paddingTop + paddingBottom
+ DEFAULT_HEIGHT
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
val resultWidth = when (modeWidth) {
MeasureSpec.EXACTLY -> measuredWidth
MeasureSpec.AT_MOST ->
desiredWidth.coerceAtMost(measuredWidth)
else -> desiredWidth
}
val resultHeight = resolveSize(desiredHeight,
heightMeasureSpec)
setMeasuredDimension(resultWidth, resultHeight)
}
resolveSize — یک متد کاربردی Android است که پیادهسازی onMeasure را ساده میکند. اندازه مورد نظر و MeasureSpec را دریافت میکند و مقدار صحیح را برمیگرداند: برای EXACTLY — اندازه دقیق، برای AT_MOST — حداقل از مقدار مورد نظر و حداکثر، برای UNSPECIFIED — اندازه مورد نظر.
onLayout برای View (نه ViewGroup) معمولاً بازنویسی نمیشود — توسط والد برای مکانیابی عناصر فرزند فراخوانی میشود. برای ViewGroup onLayout اجباری است — در آن باید layout() برای هر View فرزند فراخوانی شود.
onDraw — قلب Custom View است. این متد هر بار که View دوباره ترسیم میشود توسط Android فراخوانی میشود. یک شی Canvas به آن ارسال میشود که از طریق آن تمام ترسیم انجام میشود: اشکال هندسی، متن، مسیرها، Bitmap و انیمیشنها.
Canvas API متدهایی برای تمام عملیات پایه فراهم میکند: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. برای پیکربندی سبک از Paint استفاده میشود — شیای که رنگ، ضخامت، سبک پر کردن، فونت، سایهها و افکتها را تعیین میکند.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// پسزمینه
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// متن
canvas.drawText("نمایش سفارشی",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// دایره
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// مسیر (منحنی Bezier)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paint — پیکربندیکننده سبک ترسیم است. Anti-aliasing (Paint.ANTI_ALIAS_FLAG) برای لبههای صاف ضروری است. Subpixel text (SUBPIXEL_TEXT_FLAG) کیفیت متن را بهبود میبخشد. Style.FILL, STROKE و FILL_AND_STROKE تعیین میکنند که شکل پر شود، با خط ترسیم شود یا هر دو.
مهم: اشیاء Paint, Path, Rect را در onDraw ایجاد نکنید — این باعث تخصیص حافظه در حلقه ترسیم و فراخوانی garbage collection میشود که FPS را کاهش میدهد. تمام اشیاء ترسیم را در سازنده یا متد init ایجاد کنید. برای انیمیشن از ValueAnimator یا ObjectAnimator با invalidate() برای شروع ترسیم مجدد استفاده کنید.
ویژگیهای سفارشی امکان پیکربندی Custom View را از کد XML درست مانند android:layout_width یا android:background استاندارد فراهم میکنند. ویژگیها در فایل res/values/attrs.xml با ذکر نام، نوع و مقدار پیشفرض اختیاری اعلام میشوند.
انواع ویژگیها: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (ارجاع به منبع). برای هر نوع Android به طور خودکار مقدار را از XML تجزیه کرده و به TypedArray ارسال میکند.
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CustomView">
<attr name="circleColor"
format="color" />
<attr name="circleRadius"
format="dimension" />
<attr name="labelText"
format="string" />
<attr name="showAnimation"
format="boolean" />
</declare-styleable>
</resources>
در سازنده Custom View ویژگیها از طریق context.obtainStyledAttributes خوانده میشوند که TypedArray برمیگرداند. TypedArray متدهای دسترسی تایپشده ارائه میدهد: getColor, getDimension, getString, getBoolean, getInt. پس از خواندن باید برای آزادسازی منابع، recycle() روی TypedArray فراخوانی شود.
init {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.CustomView
)
circleColor = typedArray.getColor(
R.styleable.CustomView_circleColor,
Color.BLUE
)
circleRadius = typedArray.getDimension(
R.styleable.CustomView_circleRadius,
50f
)
labelText = typedArray.getString(
R.styleable.CustomView_labelText
) ?: "Default"
typedArray.recycle()
}
استفاده در XML: فضای نام app را اضافه کنید (xmlns:app="http://schemas.android.com/apk/res-auto") و از ویژگیهای سفارشی به صورت app:circleColor="@color/red" استفاده کنید. Android Studio اعلان در attrs.xml را به صورت خودکار تکمیل و انواع ویژگیها را بررسی میکند.
onTouchEvent — متدی است که با هر لمس View فراخوانی میشود. یک شی MotionEvent با اطلاعات نوع رویداد (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL)، مختصات، فشار و تعداد انگشتان (چندلمسی) دریافت میکند.
برای پردازش حرکات پیچیده (سوایپ، بزرگنمایی، فشار طولانی) از GestureDetector یا ScaleGestureDetector در ترکیب با onTouchEvent استفاده کنید. GestureDetector تشخیص onSingleTapUp, onFling, onLongPress, onDoubleTap و سایر حرکات استاندارد را ساده میکند.
private val gestureDetector = GestureDetector(
context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
handleTap(e.x, e.y)
return true
}
})
override fun onTouchEvent(event: MotionEvent): Boolean {
val handled = gestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_MOVE -> {
currentX = event.x
currentY = event.y
invalidate()
return true
}
}
return handled || super.onTouchEvent(event)
}
Hit-testing — تعیین اینکه آیا لمس به منطقه خاصی از View برخورد کرده است. برای مناطق مستطیلی از Rect.contains(x, y) استفاده کنید. برای دایرهها — بررسی فاصله از مرکز: sqrt(dx^2 + dy^2) < radius. برای اشکال دلخواه — Path.op() یا Region.contains().
چندلمسی از طریق MotionEvent.getPointerCount() و getPointerId(index) پردازش میشود. هر انگشت یک ID منحصر به فرد دریافت میکند که از ACTION_DOWN تا ACTION_POINTER_UP حفظ میشود. ACTION_MOVE میتواند دادههای تمام انگشتان فعال را شامل شود — برای درونیابی حرکت از getHistoricalX/Y استفاده کنید.
Custom View میتواند به گلوگاه عملکرد تبدیل شود اگر onDraw مکرراً فراخوانی شود (انیمیشن، اسکرول) یا شامل عملیات سنگین باشد. Android چندین مکانیزم برای بهینهسازی ترسیم کامپوننتهای سفارشی ارائه میدهد.
View.setLayerType امکان تغییر رندر View به لایه نرمافزاری (LAYER_TYPE_SOFTWARE) یا سختافزاری (LAYER_TYPE_HARDWARE) را فراهم میکند. Software layer برای گرافیک Canvas پیچیده که توسط شتابدهی سختافزاری پشتیبانی نمیشود مفید است — مثلاً drawTextOnPath یا افکتهای Path پیچیده.
invalidate(Rect) — فقط ناحیه مشخص شده View را دوباره ترسیم میکند، نه کل کامپوننت را. این برای Custom Viewهای بزرگ (گرافیک، نقشهها، بومهای نقاشی) حیاتی است، جایی که ترسیم مجدد کامل هر فریم باعث افت FPS میشود. برای همگامسازی با vsync از postInvalidateOnAnimation() استفاده کنید.
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// اشیاء یک بار در سازنده ایجاد میشوند
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rect = Rect()
private val position = PointF()
fun updatePosition(x: Float, y: Float) {
position.set(x, y)
// فقط ناحیه اطراف نقطه را دوباره ترسیم کن
rect.set(
(x - 10).toInt(),
(y - 10).toInt(),
(x + 10).toInt(),
(y + 10).toInt()
)
invalidate(rect)
}
}
شتابدهی سختافزاری به طور پیشفرض در Android 3.0+ (API 11+) فعال است. عملیات Canvas drawCircle, drawRect, drawBitmap به صورت سختافزاری شتاب داده شده و روی GPU اجرا میشوند. با این حال drawTextOnPath, drawVertices, عملیات clipping پیچیده شتاب داده نمیشوند — برای آنها از LAYER_TYPE_SOFTWARE استفاده کنید.
طبق Android Performance Patterns (Google, 2023)، دلایل اصلی لگ Custom View عبارتند از: تخصیص حافظه در onDraw (ایجاد اشیاء در هر فریم)، ترسیم مجدد کل View هنگام تغییر یک عنصر و عدم وجود LayerType.HARDWARE برای محتوای ایستا. رفع این سه مشکل در اکثر سناریوها FPS را از 30 به 60 افزایش میدهد.
| متد | کاربرد | فراوانی فراخوانی |
|---|---|---|
| onDraw | ترسیم محتوای View | در هر invalidate() |
| onMeasure | تعیین ابعاد View | در تغییر layout |
| onLayout | مکانیابی Viewهای فرزند | بعد از onMeasure |
| onTouchEvent | پردازش لمسها | در هر لمس |
| onSizeChanged | واکنش به تغییر اندازه | در اولین layout و تغییر |
سؤالات متداول
Custom View (Canvas API) زمانی توجیه دارد که حداکثر عملکرد ترسیم سفارشی (گرافیک، ویرایشگرهای ویدئو، نقشهها)، یکپارچهسازی با کد View-based موجود یا پشتیبانی از نسخههای Android پایینتر از API 21 نیاز باشد. Jetpack Compose رویکرد مدرن برای اکثر پروژههای جدید است که از UI اعلامی استفاده میکند و برای رابطهای پویا مناسبتر است.
بله، اگر View در کد XML از wrap_content استفاده میکند. بدون onMeasure، wrap_content مانند match_parent کار میکند، زیرا پیادهسازی استاندارد View.onMeasure اندازه پیشفرضی تعیین نمیکند. اگر View همیشه اندازه ثابت یا match_parent دارد، onMeasure قابل بازنویسی نیست.
برای تغییر ویژگیهای View (رنگ، شعاع، موقعیت) از ValueAnimator یا ObjectAnimator استفاده کنید و در callback انیمیشن برای ترسیم مجدد invalidate() را فراخوانی کنید. ValueAnimator روی نخ اصلی کار کرده و با vsync همگام شده است. برای انیمیشنهای پیچیده (فیزیک، ذرات) از Choreographer.FrameCallback یا Android Animation Framework استفاده کنید.
در onTouchEvent از MotionEvent.getPointerCount() برای تعیین تعداد انگشتان و getPointerId(i) برای پیگیری هر انگشت با ID منحصر به فرد استفاده کنید. ACTION_POINTER_DOWN / ACTION_POINTER_UP — رویدادهای اضافه/حذف انگشت. برای بزرگنمایی از ScaleGestureDetector همراه با onTouchEvent استفاده کنید — تشخیص pinch-to-zoom را ساده میکند.
بله، از طریق AndroidView — تابع composable که عنصر View-based را در سلسلهمراتب Compose وارد میکند. AndroidView یک کارخانه برای ایجاد View و یک callback update برای همگامسازی وضعیت دریافت میکند. این امکان استفاده مجدد از Custom Viewهای موجود در پروژههای جدید Compose را بدون بازنویسی کامل فراهم میکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید