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. Ο τρίτος κατασκευαστής με στυλ χρειάζεται για υποστήριξη θεμάτων 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. Αυτή η μέθοδος καλείται από το Android σε κάθε επανασχεδίαση της View. Δέχεται ένα αντικείμενο 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 — αυτό οδηγεί σε δεσμεύσεις μνήμης στον βρόχο σχεδίασης και ενεργοποιεί τη συλλογή σκουπιδιών, μειώνοντας τα 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. Χρησιμοποιήστε postInvalidateOnAnimation() για συγχρονισμό με vsync.
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, σύνθετες λειτουργίες αποκοπής δεν επιταχύνονται — γι' αυτές χρησιμοποιήστε 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 χρησιμοποιεί wrap_content σε XML κώδικα. Χωρίς onMeasure, το wrap_content θα λειτουργεί ως match_parent, επειδή η τυπική υλοποίηση View.onMeasure δεν ορίζει προεπιλεγμένο μέγεθος. Αν η View έχει πάντα σταθερό μέγεθος ή match_parent, το onMeasure μπορεί να παραλειφθεί.
Χρησιμοποιήστε ValueAnimator ή ObjectAnimator για αλλαγή ιδιοτήτων View (χρώμα, ακτίνα, θέση) και καλέστε 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 και μια επιστροφή κλήσης update για συγχρονισμό κατάστασης. Αυτό επιτρέπει την επαναχρησιμοποίηση υπαρχόντων Custom View σε νέα έργα Compose χωρίς πλήρη επαναγραφή.
Περίληψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης