واجهات مخصّصة
ارسموا مكوّنات واجهة مستخدم مخصّصة من الصفر. وسّعوا View، وطبّقوا onMeasure وonDraw باستخدام Canvas وPaint، وعرّفوا سمات XML مخصّصة، وابنوا واجهات مركّبة.
واجهات مخصّصة درس مجاني في Android Academy على CoddyKit. هذا هو الدرس 3 من أصل 5. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Android Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Android Academy 5 دروس في المجموع.
لماذا العروض المخصصة؟
أحيانًا لا تنفذ عروض Android المدمجة ما تحتاج إليه تمامًا. تتيح لك العروض المخصصة ما يلي:
- رسم أشكال أو رسوم بيانية أو حلقات تقدم أو مخططات غير موجودة في SDK
- إنشاء مكونات واجهة مستخدم تحمل هوية علامتك التجارية وتُستخدم باتساق في أنحاء التطبيق
- دمج عدة عروض في مكوّن واحد قابل لإعادة الاستخدام (Compound View)
- تحقيق أداء أفضل عبر إزالة التسلسلات الهرمية المتداخلة للتخطيطات
توسيع View
أنشئ عرضًا مخصصًا عبر توسيع View (أو فئة فرعية مثل ImageView). تجاوز الباني المطلوب:
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 100f)
invalidate() // trigger redraw
}
// onMeasure and onDraw go here
}onMeasure()
تُخبر onMeasure() العنصر الأب بالحجم الذي يرغب هذا العرض في أن يكون عليه. استدعِ setMeasuredDimension() دائمًا في النهاية:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 200 // dp → pixels
val px = (desiredSize * resources.displayMetrics.density).toInt()
val width = resolveSize(px, widthMeasureSpec)
val height = resolveSize(px, heightMeasureSpec)
// Square: same width and height
val size = minOf(width, height)
setMeasuredDimension(size, size)
}Paint — أداة الرسم
يحتفظ كائن Paint بمعلومات النمط اللازمة للرسم، مثل اللون وعرض الخط ومنع التعرج. أنشئه مرة واحدة — ولا تنشئه أبدًا داخل onDraw():
class CircleProgressView ... : View(...) {
// Create Paint in init, not in onDraw!
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.STROKE
strokeWidth = 20f
strokeCap = Paint.Cap.ROUND
}
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#6750A4")
style = Paint.Style.STROKE
strokeWidth = 20f
strokeCap = Paint.Cap.ROUND
}
}onDraw() — رسم العرض
يُستدعى onDraw(canvas) في كل مرة يحتاج فيها العرض إلى إعادة الرسم. استخدم Canvas لرسم الأشكال والنصوص والصور النقطية:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = (minOf(width, height) / 2f) - 30f
// Draw background circle
canvas.drawCircle(cx, cy, radius, backgroundPaint)
// Draw progress arc
val oval = RectF(cx - radius, cy - radius, cx + radius, cy + radius)
val sweepAngle = 360f * (progress / 100f)
canvas.drawArc(oval, -90f, sweepAngle, false, progressPaint)
}invalidate() في مقابل requestLayout()
طريقتان لتفعيل تحديث:
invalidate()— تعيد رسم العرض (وتستدعيonDraw). استخدمها عندما يتغير المحتوى بينما يبقى الحجم نفسه.requestLayout()— تعيد قياس العرض ورسمه (وتستدعيonMeasureثمonDraw). استخدمها عندما يتغير حجم العرض.
السمات المخصصة (attrs.xml)
صرّح بسمات XML مخصصة حتى يمكن تهيئة العرض من ملفات التخطيط:
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CircleProgressView">
<attr name="progressColor" format="color" />
<attr name="trackColor" format="color" />
<attr name="strokeWidth" format="dimension" />
<attr name="progress" format="float" />
</declare-styleable>
</resources>قراءة السمات المخصصة
اقرأ attrs في الباني باستخدام obtainStyledAttributes:
init {
context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView).use { ta ->
progressPaint.color = ta.getColor(
R.styleable.CircleProgressView_progressColor,
Color.parseColor("#6750A4")
)
backgroundPaint.color = ta.getColor(
R.styleable.CircleProgressView_trackColor,
Color.LTGRAY
)
val sw = ta.getDimension(R.styleable.CircleProgressView_strokeWidth, 20f)
progressPaint.strokeWidth = sw
backgroundPaint.strokeWidth = sw
progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f)
}
}استخدام العرض المخصص في XML
استخدم العرض المخصص في XML الخاص بالتخطيط مع المسار الكامل للفئة والسمات المخصصة:
<!-- In any layout XML -->
<com.example.myapp.CircleProgressView
android:id="@+id/progressView"
android:layout_width="120dp"
android:layout_height="120dp"
app:progress="65"
app:progressColor="@color/primary"
app:trackColor="@color/surface_variant"
app:strokeWidth="12dp" />
<!-- Control in code: -->
binding.progressView.progress = 75fالعروض المركبة
يجمع العرض المركب عروضًا موجودة في مكوّن واحد قابل لإعادة الاستخدام. وسّع فئة تخطيط مثل LinearLayout:
class SearchBarView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)
init {
orientation = HORIZONTAL
binding.ivSearch.setOnClickListener {
onSearchClick?.invoke(binding.etSearch.text.toString())
}
}
var onSearchClick: ((String) -> Unit)? = null
fun setHint(hint: String) { binding.etSearch.hint = hint }
}رسم النص
ارسم النص على Canvas باستخدام drawText(). وحاذِه في المنتصف بدقة باستخدام Paint.getFontMetrics():
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
textSize = 48f
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
// Vertically center the text:
val fm = textPaint.fontMetrics
val textY = cy - (fm.ascent + fm.descent) / 2f
canvas.drawText("${progress.toInt()}%", cx, textY, textPaint)
}تحقق سريع
ما الطريقة التي ينبغي استدعاؤها لإعادة رسم العرض المخصص عندما تتغير البيانات بينما يبقى الحجم نفسه؟
مراجعة: العروض المخصصة
يمكنك الآن رسم أي شيء على الشاشة:
- وسّع
Viewباستخدام@JvmOverloads constructor onMeasure()— أبلغ عن الحجم المطلوب باستخدامsetMeasuredDimension()Paint— يحدد النمط؛ أنشئه مرة واحدة، وليس داخلonDraw()onDraw(canvas)— ارسم الدوائر والأقواس والنصوص والصور النقطية- استخدم
invalidate()لإعادة الرسم وrequestLayout()لإعادة القياس - السمات المخصصة عبر
attrs.xmlلتهيئة XML - العرض المركب — وسّع تخطيطًا لدمج عروض موجودة
التالي: أضف الحيوية إلى واجهتك باستخدام الرسوم المتحركة والانتقالات.
الأسئلة الشائعة
هل درس «واجهات مخصّصة» مجاني؟
نعم — نص درس «واجهات مخصّصة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Android Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Android Academy 5 دروس في المجموع.
ماذا ستتعلم في «واجهات مخصّصة»؟
ارسموا مكوّنات واجهة مستخدم مخصّصة من الصفر. وسّعوا View، وطبّقوا onMeasure وonDraw باستخدام Canvas وPaint، وعرّفوا سمات XML مخصّصة، وابنوا واجهات مركّبة. تتمرن على Android Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Android Academy؟
لا تُشترط خبرة سابقة. Android Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 5.
كم من الوقت يستغرق درس «واجهات مخصّصة»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Android Academy هذا؟
نعم. كل درس في Android Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- أساسيات Material Design
- Themes وStyles
- واجهات مخصّصة
- الرسوم المتحركة والانتقالات
- التنقّل السفلي وعلامات التبويب