Android
Création de vues personnalisées
Recherche…
Création de vues personnalisées
Si vous avez besoin d'une vue totalement personnalisée, vous devrez sous-classer View
(la super-classe de toutes les vues Android) et fournir vos onMeasure(...)
dimensionnement personnalisées ( onMeasure(...)
) et de dessin ( onDraw(...)
):
Créez votre squelette de vue personnalisée: il s'agit essentiellement de la même chose pour chaque vue personnalisée. Ici, nous créons le squelette d'une vue personnalisée qui peut dessiner un smiley, appelé
SmileyView
:public class SmileyView extends View { private Paint mCirclePaint; private Paint mEyeAndMouthPaint; private float mCenterX; private float mCenterY; private float mRadius; private RectF mArcBounds = new RectF(); public SmileyView(Context context) { this(context, null, 0); } public SmileyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaints(); } private void initPaints() {/* ... */} @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {/* ... */} @Override protected void onDraw(Canvas canvas) {/* ... */} }
Initialiser vos peintures: les objets
Paint
sont les pinceaux de votre canevas virtuel définissant la manière dont vos objets géométriques sont rendus (par exemple, style de couleur, de remplissage et de trait, etc.). Nous créons ici deuxPaint
, une peinture jaune remplie pour le cercle et une peinture noire pour les yeux et la bouche:private void initPaints() { mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setColor(Color.YELLOW); mEyeAndMouthPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mEyeAndMouthPaint.setStyle(Paint.Style.STROKE); mEyeAndMouthPaint.setStrokeWidth(16 * getResources().getDisplayMetrics().density); mEyeAndMouthPaint.setStrokeCap(Paint.Cap.ROUND); mEyeAndMouthPaint.setColor(Color.BLACK); }
Implémentez votre propre
onMeasure(...)
: ceci est nécessaire pour que les mises en page parent (par exemple,FrameLayout
) puissent aligner correctement votre vue personnalisée. Il fournit un ensemble demeasureSpecs
que vous pouvez utiliser pour déterminer la hauteur et la largeur de votre vue. Ici, nous créons un carré en nous assurant que la hauteur et la largeur sont les mêmes:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); int size = Math.min(w, h); setMeasuredDimension(size, size); }
Notez que
onMeasure(...)
doit contenir au moins un appel àsetMeasuredDimension(..)
sinon votre vue personnalisée tombera en panne avec uneIllegalStateException
.Implémentez votre propre
onSizeChanged(...)
: cela vous permet de saisir la hauteur et la largeur actuelles de votre vue personnalisée pour ajuster correctement votre code de rendu. Ici, nous calculons simplement notre centre et notre rayon:@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mCenterX = w / 2f; mCenterY = h / 2f; mRadius = Math.min(w, h) / 2f; }
Implémentez votre propre
onDraw(...)
: c'est là que vous implémentez le rendu réel de votre vue. Il fournit un objetCanvas
lequel vous pouvez dessiner (voir la documentation officielleCanvas
pour toutes les méthodes de dessin disponibles).@Override protected void onDraw(Canvas canvas) { // draw face canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint); // draw eyes float eyeRadius = mRadius / 5f; float eyeOffsetX = mRadius / 3f; float eyeOffsetY = mRadius / 3f; canvas.drawCircle(mCenterX - eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); canvas.drawCircle(mCenterX + eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); // draw mouth float mouthInset = mRadius /3f; mArcBounds.set(mouthInset, mouthInset, mRadius * 2 - mouthInset, mRadius * 2 - mouthInset); canvas.drawArc(mArcBounds, 45f, 90f, false, mEyeAndMouthPaint); }
Ajouter votre vue personnalisée à une mise en page: la vue personnalisée peut désormais être incluse dans tous les fichiers de mise en page dont vous disposez. Ici, on l'enveloppe simplement dans un
FrameLayout
:<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.app.SmileyView android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
Notez qu'il est recommandé de créer votre projet une fois le code de la vue terminé. Sans le construire, vous ne pourrez pas voir la vue sur un écran d'aperçu dans Android Studio.
Après avoir tout réuni, vous devriez être accueilli avec l'écran suivant après le lancement de l'activité contenant la mise en page ci-dessus:
Ajout d'attributs à des vues
Les vues personnalisées peuvent également prendre des attributs personnalisés pouvant être utilisés dans les fichiers de ressources de présentation Android. Pour ajouter des attributs à votre vue personnalisée, procédez comme suit:
Définissez le nom et le type de vos attributs: cela se fait à l'intérieur de
res/values/attrs.xml
(créez-le si nécessaire). Le fichier suivant définit un attribut de couleur pour la couleur du visage de notre smiley et un attribut enum pour l'expression du smiley:<resources> <declare-styleable name="SmileyView"> <attr name="smileyColor" format="color" /> <attr name="smileyExpression" format="enum"> <enum name="happy" value="0"/> <enum name="sad" value="1"/> </attr> </declare-styleable> <!-- attributes for other views --> </resources>
Utilisez vos attributs dans votre mise en page: cela peut être fait à l'intérieur de tous les fichiers de mise en page qui utilisent votre vue personnalisée. Le fichier de mise en page suivant crée un écran avec un smiley jaune heureux:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="match_parent" android:layout_width="match_parent"> <com.example.app.SmileyView android:layout_height="56dp" android:layout_width="56dp" app:smileyColor="#ffff00" app:smileyExpression="happy" /> </FrameLayout>
Conseil: Les attributs personnalisés ne fonctionnent pas avec les
tools:
préfixe dans Android Studio 2.1 et versions antérieures (et éventuellement dans les futures versions). Dans cet exemple, en remplaçantapp:smileyColor
par destools:smileyColor
smileyColor
être défini pendant l'exécution ou au moment du design.Lisez vos attributs: cela se fait à l'intérieur du code source de votre vue personnalisée. L'extrait suivant de
SmileyView
montre comment les attributs peuvent être extraits:public class SmileyView extends View { // ... public SmileyView(Context context) { this(context, null); } public SmileyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, 0); mFaceColor = a.getColor(R.styleable.SmileyView_smileyColor, Color.TRANSPARENT); mFaceExpression = a.getInteger(R.styleable.SmileyView_smileyExpression, Expression.HAPPY); // Important: always recycle the TypedArray a.recycle(); // initPaints(); ... } }
(Facultatif) Ajoutez le style par défaut: pour ce faire, ajoutez un style avec les valeurs par défaut et chargez-le dans votre vue personnalisée. Le style smiley par défaut suivant représente un style jaune heureux:
<!-- styles.xml --> <style name="DefaultSmileyStyle"> <item name="smileyColor">#ffff00</item> <item name="smileyExpression">happy</item> </style>
Ce qui est appliqué dans notre
SmileyView
en l’ajoutant comme dernier paramètre de l’appel àobtainStyledAttributes
(voir le code à l’étape 3):TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, R.style.DefaultSmileyViewStyle);
Notez que toute valeur d'attribut définie dans le fichier de mise en forme gonflé (voir le code à l'étape 2) remplacera les valeurs correspondantes du style par défaut.
(Facultatif) Fournissez des styles à l'intérieur des thèmes: cela se fait en ajoutant un nouvel attribut de référence de style qui peut être utilisé dans vos thèmes et en fournissant un style pour cet attribut. Ici, nous
smileyStyle
simplement notre attribut de référencesmileyStyle
:<!-- attrs.xml --> <attr name="smileyStyle" format="reference" />
Nous fournissons ensuite un style dans notre thème d'application (ici, nous réutilisons simplement le style par défaut de l'étape 4):
<!-- themes.xml --> <style name="AppTheme" parent="AppBaseTheme"> <item name="smileyStyle">@style/DefaultSmileyStyle</item> </style>
Création d'une vue composée
Une vue composée est un ViewGroup
personnalisé traité comme une vue unique par le code du programme environnant. Un tel ViewGroup peut être vraiment utile dans une conception de type DDD , car il peut correspondre à un agrégat, dans cet exemple, un contact. Il peut être réutilisé partout où ce contact est affiché.
Cela signifie que le code de contrôleur environnant, une activité, un fragment ou un adaptateur, peut simplement transmettre l'objet de données à la vue sans le séparer en plusieurs widgets d'interface utilisateur.
Cela facilite la réutilisation du code et permet une meilleure conception selon les principes SOLID .
La mise en page XML
C'est généralement là que vous commencez. Vous avez un bit XML existant que vous vous retrouvez en train de réutiliser, peut-être en tant que <include/>
. Extrayez-le dans un fichier XML distinct et enveloppez-le dans un élément <merge>
:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/photo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true" />
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/photo" />
<TextView
android:id="@+id/phone_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_toLeftOf="@id/photo" />
</merge>
Ce fichier XML fonctionne parfaitement dans l'éditeur de disposition d'Android Studio. Vous pouvez le traiter comme n'importe quelle autre mise en page.
Le composé ViewGroup
Une fois que vous avez le fichier XML, créez le groupe de vues personnalisé.
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.ImageView;
import android.widget.TextView;
import myapp.R;
/**
* A compound view to show contacts.
*
* This class can be put into an XML layout or instantiated programmatically, it
* will work correctly either way.
*/
public class ContactView extends RelativeLayout {
// This class extends RelativeLayout because that comes with an automatic
// (MATCH_PARENT, MATCH_PARENT) layout for its child item. You can extend
// the raw android.view.ViewGroup class if you want more control. See the
// note in the layout XML why you wouldn't want to extend a complex view
// such as RelativeLayout.
// 1. Implement superclass constructors.
public ContactView(Context context) {
super(context);
init(context, null);
}
// two extra constructors left out to keep the example shorter
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ContactView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
// 2. Initialize the view by inflating an XML using `this` as parent
private TextView mName;
private TextView mPhoneNumber;
private ImageView mPhoto;
private void init(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.contact_view, this, true);
mName = (TextView) findViewById(R.id.name);
mPhoneNumber = (TextView) findViewById(R.id.phone_number);
mPhoto = (ImageView) findViewById(R.id.photo);
}
// 3. Define a setter that's expressed in your domain model. This is what the example is
// all about. All controller code can just invoke this setter instead of fiddling with
// lots of strings, visibility options, colors, animations, etc. If you don't use a
// custom view, this code will usually end up in a static helper method (bad) or copies
// of this code will be copy-pasted all over the place (worse).
public void setContact(Contact contact) {
mName.setText(contact.getName());
mPhoneNumber.setText(contact.getPhoneNumber());
if (contact.hasPhoto()) {
mPhoto.setVisibility(View.VISIBLE);
mPhoto.setImageBitmap(contact.getPhoto());
} else {
mPhoto.setVisibility(View.GONE);
}
}
}
La méthode init(Context, AttributeSet)
permet de lire tous les attributs XML personnalisés, comme expliqué dans Ajout d'attributs aux vues .
Avec ces pièces en place, vous pouvez l'utiliser dans votre application.
Utilisation en XML
Voici un exemple fragment_contact_info.xml
qui montre comment placer un seul ContactView au-dessus d’une liste de messages:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- The compound view becomes like any other view XML element -->
<myapp.ContactView
android:id="@+id/contact"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/message_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
Utilisation dans le code
Voici un exemple RecyclerView.Adapter
qui affiche une liste de contacts. Cet exemple illustre à quel point le code du contrôleur est plus propre lorsqu'il est totalement exempt de manipulation de View.
package myapp;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
public class ContactsAdapter extends RecyclerView.Adapter<ContactsViewHolder> {
private final Context context;
public ContactsAdapter(final Context context) {
this.context = context;
}
@Override
public ContactsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ContactView v = new ContactView(context); // <--- this
return new ContactsViewHolder(v);
}
@Override
public void onBindViewHolder(ContactsViewHolder holder, int position) {
Contact contact = this.getItem(position);
holder.setContact(contact); // <--- this
}
static class ContactsViewHolder extends RecyclerView.ViewHolder {
public ContactsViewHolder(ContactView itemView) {
super(itemView);
}
public void setContact(Contact contact) {
((ContactView) itemView).setContact(contact); // <--- this
}
}
}
Conseils de performance CustomView
Ne pas allouer de nouveaux objets dans onDraw
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(); //Do not allocate here
}
Au lieu de dessiner des tirables en toile ...
drawable.setBounds(boundsRect);
drawable.draw(canvas);
Utilisez un bitmap pour un dessin plus rapide:
canvas.drawBitmap(bitmap, srcRect, boundsRect, paint);
Ne redessinez pas la vue entière pour ne mettre à jour qu'une petite partie. Au lieu de cela, redessinez la partie spécifique de la vue.
invalidate(boundToBeRefreshed);
Si votre vue effectue une animation continue, par exemple une montre montrant chaque seconde, arrêtez au moins l'animation à onStop()
de l'activité et redémarrez-la sur onStart()
de l'activité.
Ne faites aucun calcul dans la méthode onDraw
d'une vue, vous devriez plutôt terminer de dessiner avant d'appeler invalidate()
. En utilisant cette technique, vous pouvez éviter de laisser tomber des images dans votre vue.
Rotations
Les opérations de base d'une vue sont translater, faire pivoter, etc. Presque tous les développeurs ont rencontré ce problème lorsqu'ils utilisent des bitmap ou des dégradés dans leur vue personnalisée. Si la vue affiche une vue pivotée et que le bitmap doit être pivoté dans cette vue personnalisée, beaucoup d’entre nous penseront que cela va coûter cher. Beaucoup pensent que tourner un bitmap est très coûteux car pour ce faire, vous devez traduire la matrice de pixels du bitmap. Mais la vérité est que ce n'est pas si difficile! Au lieu de faire pivoter le bitmap, faites simplement pivoter la toile elle-même!
// Save the canvas state
int save = canvas.save();
// Rotate the canvas by providing the center point as pivot and angle
canvas.rotate(pivotX, pivotY, angle);
// Draw whatever you want
// Basically whatever you draw here will be drawn as per the angle you rotated the canvas
canvas.drawBitmap(...);
// Now restore your your canvas to its original state
canvas.restore(save);
// Unless canvas is restored to its original state, further draw will also be rotated.
Vue composée pour SVG / VectorDrawable comme drawableRight
Le motif principal pour développer cette vue composée est que, sous 5.0, les périphériques ne supportent pas svg dans drawable dans TextView / EditText. Un autre avantage est que nous pouvons définir la height
et la width
de drawableRight
dans EditText
. Je l'ai séparé de mon projet et créé dans un module séparé.
Nom du module: custom_edit_drawable (nom abrégé pour prefix- c_d_e)
"c_d_e_" préfixe à utiliser pour que les ressources du module d'application ne les remplacent pas par erreur. Exemple: le préfixe "abc" est utilisé par Google dans la bibliothèque de support.
build.gradle
dependencies {
compile 'com.android.support:appcompat-v7:25.3.1'
}
utiliser AppCompat> = 23
Fichier de mise en page: c_e_d_compound_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/edt_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:paddingEnd="40dp"
android:paddingLeft="5dp"
android:paddingRight="40dp"
android:paddingStart="5dp" />
<!--make sure you are not using ImageView instead of this-->
<android.support.v7.widget.AppCompatImageView
android:id="@+id/drawbleRight_search"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="right|center_vertical"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp" />
</FrameLayout>
Attributs personnalisés: attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="EditTextWithDrawable">
<attr name="c_e_d_drawableRightSVG" format="reference" />
<attr name="c_e_d_hint" format="string" />
<attr name="c_e_d_textSize" format="dimension" />
<attr name="c_e_d_textColor" format="color" />
</declare-styleable>
</resources>
Code: EditTextWithDrawable.java
public class EditTextWithDrawable extends FrameLayout {
public AppCompatImageView mDrawableRight;
public EditText mEditText;
public EditTextWithDrawable(Context context) {
super(context);
init(null);
}
public EditTextWithDrawable(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrs) {
if (attrs != null && !isInEditMode()) {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.c_e_d_compound_view, this, true);
mDrawableRight = (AppCompatImageView) ((FrameLayout) getChildAt(0)).getChildAt(1);
mEditText = (EditText) ((FrameLayout) getChildAt(0)).getChildAt(0);
TypedArray attributeArray = getContext().obtainStyledAttributes(
attrs,
R.styleable.EditTextWithDrawable);
int drawableRes =
attributeArray.getResourceId(
R.styleable.EditTextWithDrawable_c_e_d_drawableRightSVG, -1);
if (drawableRes != -1) {
mDrawableRight.setImageResource(drawableRes);
}
mEditText.setHint(attributeArray.getString(
R.styleable.EditTextWithDrawable_c_e_d_hint));
mEditText.setTextColor(attributeArray.getColor(
R.styleable.EditTextWithDrawable_c_e_d_textColor, Color.BLACK));
int textSize = attributeArray.getDimensionPixelSize(R.styleable.EditTextWithDrawable_c_e_d_textSize, 15);
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
android.view.ViewGroup.LayoutParams layoutParams = mDrawableRight.getLayoutParams();
layoutParams.width = (textSize * 3) / 2;
layoutParams.height = (textSize * 3) / 2;
mDrawableRight.setLayoutParams(layoutParams);
attributeArray.recycle();
}
}
}
Exemple: Comment utiliser la vue ci-dessus
Mise en page: activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.customeditdrawable.AppEditTextWithDrawable
android:id="@+id/edt_search_emp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:c_e_d_drawableRightSVG="@drawable/ic_svg_search"
app:c_e_d_hint="@string/hint_search_here"
app:c_e_d_textColor="@color/text_color_dark_on_light_bg"
app:c_e_d_textSize="@dimen/text_size_small" />
</LinearLayout>
Activité: MainActivity.java
public class MainActivity extends AppCompatActivity {
EditTextWithDrawable mEditTextWithDrawable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEditTextWithDrawable= (EditTextWithDrawable) findViewById(R.id.edt_search_emp);
}
}
Répondre aux événements tactiles
De nombreuses vues personnalisées doivent accepter l'interaction de l'utilisateur sous la forme d'événements tactiles. Vous pouvez accéder aux événements tactiles en onTouchEvent
. Vous pouvez filtrer plusieurs actions. Les principaux sont
-
ACTION_DOWN
: Ceci est déclenché une fois lorsque votre doigt touche pour la première fois la vue. -
ACTION_MOVE
: Ceci est appelé chaque fois que votre doigt se déplace un peu sur la vue. Il est appelé à plusieurs reprises. -
ACTION_UP
: Ceci est la dernière action à appeler lorsque vous retirez votre doigt de l'écran.
Vous pouvez ajouter la méthode suivante à votre vue, puis observer la sortie du journal lorsque vous touchez et déplacez votre doigt autour de votre vue.
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomView", "onTouchEvent: ACTION_DOWN: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomView", "onTouchEvent: ACTION_MOVE: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_UP:
Log.i("CustomView", "onTouchEvent: ACTION_UP: x = " + x + ", y = " + y);
break;
}
return true;
}
Lectures complémentaires: