Custom View در Android — ترسیم و پردازش رویدادها

نویسنده: IT Sectr منتشر شده: 2026-07-20 زمان مطالعه: 9 دقیقه

Custom View — یک کامپوننت رابط کاربری سفارشی در Android است که با ارث‌بری از کلاس View یا زیرکلاس‌های آن (Button, TextView, ImageView) و بازنویسی متدهای کلیدی ایجاد می‌شود: onDraw برای ترسیم، onMeasure برای اندازه‌گیری ابعاد و onTouchEvent برای پردازش لمس‌ها. طبق Google Android Developer Guide (2024)، Custom View زمانی استفاده می‌شود که کامپوننت‌های استاندارد Android SDK رفتار یا ظاهر مورد نیاز را ارائه نمی‌دهند — مثلاً برای انیمیشن‌های سفارشی، اشکال غیراستاندارد، نمودارهای تخصصی و عناصر بازی.

نکات اصلی

  • Custom View — با ارث‌بری از View یا زیرکلاس‌های آن و بازنویسی onDraw, onMeasure, onTouchEvent ایجاد می‌شود.
  • ترسیم از طریق Canvas API انجام می‌شود — اشکال هندسی، متن، مسیرها، Bitmap و انیمیشن.
  • onMeasure مسئول ابعاد صحیح View با در نظر گرفتن پارامترهای layout و padding است.
  • ویژگی‌های XML سفارشی در res/values/attrs.xml اعلام شده و در سازنده View خوانده می‌شوند.
  • پردازش لمس‌ها onTouchEvent امکان افزودن پشتیبانی از حرکات، کشیدن و رها کردن و چندلمسی را فراهم می‌کند.

Custom View در Android چیست؟

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 لازم است.

kotlin
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 و onLayout: مدیریت ابعاد

onMeasure — متدی است که در آن View ابعاد مورد نظر خود را بر اساس MeasureSpec (محدودیت‌های والد) به سیستم اعلام می‌کند. بدون پیاده‌سازی صحیح onMeasure، View ممکن است با wrap_content ارتفاع صفر داشته باشد یا با match_parent تمام مساحت صفحه را بگیرد.

MeasureSpec از حالت (EXACTLY, AT_MOST, UNSPECIFIED) و مقدار تشکیل شده است. EXACTLY — والد اندازه دقیق تعیین کرده (match_parent یا مقدار ثابت). AT_MOST — والد حداکثر را تعیین کرده (wrap_content). UNSPECIFIED — بدون محدودیت (ScrollView, ListView).

kotlin
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 و Canvas API: ترسیم

onDraw — قلب Custom View است. این متد هر بار که View دوباره ترسیم می‌شود توسط Android فراخوانی می‌شود. یک شی Canvas به آن ارسال می‌شود که از طریق آن تمام ترسیم انجام می‌شود: اشکال هندسی، متن، مسیرها، Bitmap و انیمیشن‌ها.

Canvas API متدهایی برای تمام عملیات پایه فراهم می‌کند: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. برای پیکربندی سبک از Paint استفاده می‌شود — شی‌ای که رنگ، ضخامت، سبک پر کردن، فونت، سایه‌ها و افکت‌ها را تعیین می‌کند.

kotlin
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() برای شروع ترسیم مجدد استفاده کنید.

ویژگی‌های XML سفارشی در attrs.xml

ویژگی‌های سفارشی امکان پیکربندی 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 ارسال می‌کند.

xml
<!-- 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 فراخوانی شود.

kotlin
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

onTouchEvent — متدی است که با هر لمس View فراخوانی می‌شود. یک شی MotionEvent با اطلاعات نوع رویداد (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL)، مختصات، فشار و تعداد انگشتان (چندلمسی) دریافت می‌کند.

برای پردازش حرکات پیچیده (سوایپ، بزرگنمایی، فشار طولانی) از GestureDetector یا ScaleGestureDetector در ترکیب با onTouchEvent استفاده کنید. GestureDetector تشخیص onSingleTapUp, onFling, onLongPress, onDoubleTap و سایر حرکات استاندارد را ساده می‌کند.

kotlin
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

Custom View می‌تواند به گلوگاه عملکرد تبدیل شود اگر onDraw مکرراً فراخوانی شود (انیمیشن، اسکرول) یا شامل عملیات سنگین باشد. Android چندین مکانیزم برای بهینه‌سازی ترسیم کامپوننت‌های سفارشی ارائه می‌دهد.

Layer type برای ترسیم پیچیده

View.setLayerType امکان تغییر رندر View به لایه نرم‌افزاری (LAYER_TYPE_SOFTWARE) یا سخت‌افزاری (LAYER_TYPE_HARDWARE) را فراهم می‌کند. Software layer برای گرافیک Canvas پیچیده که توسط شتاب‌دهی سخت‌افزاری پشتیبانی نمی‌شود مفید است — مثلاً drawTextOnPath یا افکت‌های Path پیچیده.

invalidate فقط ناحیه مورد نیاز

invalidate(Rect) — فقط ناحیه مشخص شده View را دوباره ترسیم می‌کند، نه کل کامپوننت را. این برای Custom Viewهای بزرگ (گرافیک، نقشه‌ها، بوم‌های نقاشی) حیاتی است، جایی که ترسیم مجدد کامل هر فریم باعث افت FPS می‌شود. برای همگام‌سازی با vsync از postInvalidateOnAnimation() استفاده کنید.

kotlin
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 و تغییر

سؤالات متداول

چه زمانی به جای Compose از Custom View استفاده کنیم؟

Custom View (Canvas API) زمانی توجیه دارد که حداکثر عملکرد ترسیم سفارشی (گرافیک، ویرایشگرهای ویدئو، نقشه‌ها)، یکپارچه‌سازی با کد View-based موجود یا پشتیبانی از نسخه‌های Android پایین‌تر از API 21 نیاز باشد. Jetpack Compose رویکرد مدرن برای اکثر پروژه‌های جدید است که از UI اعلامی استفاده می‌کند و برای رابط‌های پویا مناسب‌تر است.

آیا بازنویسی onMeasure ضروری است؟

بله، اگر View در کد XML از wrap_content استفاده می‌کند. بدون onMeasure، wrap_content مانند match_parent کار می‌کند، زیرا پیاده‌سازی استاندارد View.onMeasure اندازه پیش‌فرضی تعیین نمی‌کند. اگر View همیشه اندازه ثابت یا match_parent دارد، onMeasure قابل بازنویسی نیست.

چگونه Custom View را انیمیت کنیم؟

برای تغییر ویژگی‌های View (رنگ، شعاع، موقعیت) از ValueAnimator یا ObjectAnimator استفاده کنید و در callback انیمیشن برای ترسیم مجدد invalidate() را فراخوانی کنید. ValueAnimator روی نخ اصلی کار کرده و با vsync همگام شده است. برای انیمیشن‌های پیچیده (فیزیک، ذرات) از Choreographer.FrameCallback یا Android Animation Framework استفاده کنید.

چگونه چندلمسی را در Custom View پردازش کنیم؟

در onTouchEvent از MotionEvent.getPointerCount() برای تعیین تعداد انگشتان و getPointerId(i) برای پیگیری هر انگشت با ID منحصر به فرد استفاده کنید. ACTION_POINTER_DOWN / ACTION_POINTER_UP — رویدادهای اضافه/حذف انگشت. برای بزرگنمایی از ScaleGestureDetector همراه با onTouchEvent استفاده کنید — تشخیص pinch-to-zoom را ساده می‌کند.

آیا می‌توان از Custom View در Jetpack Compose استفاده کرد؟

بله، از طریق AndroidView — تابع composable که عنصر View-based را در سلسله‌مراتب Compose وارد می‌کند. AndroidView یک کارخانه برای ایجاد View و یک callback update برای همگام‌سازی وضعیت دریافت می‌کند. این امکان استفاده مجدد از Custom Viewهای موجود در پروژه‌های جدید Compose را بدون بازنویسی کامل فراهم می‌کند.

خلاصه

  • Custom View — کامپوننت سفارشی Android که با ارث‌بری از View و بازنویسی onDraw, onMeasure و onTouchEvent برای ترسیم و پردازش سفارشی ایجاد می‌شود.
  • onDraw و Canvas API — اساس ترسیم: اشکال هندسی، متن، Bitmap، مسیرها — با پیکربندی از طریق Paint برای رنگ، سبک و فونت.
  • onMeasure برای کار صحیح wrap_content مهم است؛ از MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) و resolveSize برای محاسبه ابعاد استفاده می‌کند.
  • ویژگی‌های سفارشی از طریق attrs.xml و TypedArray امکان پیکربندی Custom View را از کد XML با پشتیبانی autocomplete در Android Studio فراهم می‌کند.
  • onTouchEvent با GestureDetector پردازش لمس‌ها، حرکات و چندلمسی را فراهم می‌کند و hit-testing ناحیه تعامل را تعیین می‌کند.
  • عملکرد: اشیاء را یک بار در سازنده ایجاد کنید، از invalidate(Rect) برای ترسیم مجدد جزئی و LayerType برای افکت‌های پیچیده استفاده کنید.
  • Custom View ادغام می‌شود با Jetpack Compose از طریق AndroidView که امکان استفاده مجدد از کامپوننت‌های آماده در پروژه‌های جدید Compose را فراهم می‌کند.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید