Definir atributos personalizados

472

Necesito implementar mis propios atributos como en com.android.R.attr

No encontré nada en la documentación oficial, así que necesito información sobre cómo definir estos atributos y cómo usarlos desde mi código.

Alexander Oleynikov
fuente
20
Estos documentos pueden ser más recientes que su publicación, pero para mantenerla actualizada, puede encontrar buena documentación oficial para los atributos aquí: developer.android.com/training/custom-views/…
OYRM 01 de
Recomiendo un buen artículo con un ejemplo sobre los atributos personalizados: amcmobileware.org/android/blog/2016/09/11/custom-attributes
Arkadiusz Cieśliński
un pequeño ejemplo de trabajo puede ser útil: github.com/yujiaao/MergeLayout1
Yu Jiaao

Respuestas:

971

Actualmente la mejor documentación es la fuente. Puedes echarle un vistazo aquí (attrs.xml) .

Puede definir atributos en el <resources>elemento superior o dentro de un <declare-styleable>elemento. Si voy a usar un atributo en más de un lugar, lo pongo en el elemento raíz. Tenga en cuenta que todos los atributos comparten el mismo espacio de nombres global. Eso significa que incluso si crea un nuevo atributo dentro de un<declare-styleable> elemento, puede usarse fuera de él y no puede crear otro atributo con el mismo nombre de un tipo diferente.

Un <attr>elemento tiene dos atributos xml namey format. namele permite llamar a algo y esto es como se llega a referirse a ella en el código, por ejemplo, R.attr.my_attribute. El formatatributo puede tener valores diferentes según el 'tipo' de atributo que desee.

  • referencia: si hace referencia a otro ID de recurso (por ejemplo, "@ color / my_color", "@ layout / my_layout")
  • color
  • booleano
  • dimensión
  • flotador
  • entero
  • cuerda
  • fracción
  • enumeración - normalmente definida implícitamente
  • bandera - normalmente definida implícitamente

Puede establecer el formato de múltiples tipos utilizando |, por ejemplo, format="reference|color".

enum Los atributos se pueden definir de la siguiente manera:

<attr name="my_enum_attr">
  <enum name="value1" value="1" />
  <enum name="value2" value="2" />
</attr>

flag los atributos son similares, excepto que los valores necesitan ser definidos para que puedan ser mordidos juntos:

<attr name="my_flag_attr">
  <flag name="fuzzy" value="0x01" />
  <flag name="cold" value="0x02" />
</attr>

Además de los atributos existe el <declare-styleable>elemento. Esto le permite definir atributos que una vista personalizada puede usar. Para ello, especifica un <attr>elemento, si se definió previamente, no especifique el format. Si desea reutilizar un atributo de Android, por ejemplo, android: gravity, puede hacerlo de la namesiguiente manera.

Un ejemplo de una vista personalizada <declare-styleable>:

<declare-styleable name="MyCustomView">
  <attr name="my_custom_attribute" />
  <attr name="android:gravity" />
</declare-styleable>

Al definir sus atributos personalizados en XML en su vista personalizada, debe hacer algunas cosas. Primero debe declarar un espacio de nombres para encontrar sus atributos. Hace esto en el elemento de diseño raíz. Normalmente solo hay xmlns:android="http://schemas.android.com/apk/res/android". Ahora también debes agregar xmlns:whatever="http://schemas.android.com/apk/res-auto".

Ejemplo:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:whatever="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">

    <org.example.mypackage.MyCustomView
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:gravity="center"
      whatever:my_custom_attribute="Hello, world!" />
</LinearLayout>

Finalmente, para acceder a ese atributo personalizado, normalmente lo hace en el constructor de su vista personalizada de la siguiente manera.

public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);

  TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyle, 0);

  String str = a.getString(R.styleable.MyCustomView_my_custom_attribute);

  //do something with str

  a.recycle();
}

El fin. :)

Rich Schuler
fuente
14
Aquí hay un proyecto de muestra que demuestra atributos personalizados para usar con una costumbre View: github.com/commonsguy/cw-advandroid/tree/master/Views/…
CommonsWare
77
Si está usando atributos personalizados de un proyecto de biblioteca: vea esta pregunta: stackoverflow.com/questions/5819369/… - Parece que funciona si lo usa xmlns:my="http://schemas.android.com/apk/lib/my.namespace", sin copiar attrs.xml. Tenga en cuenta que la ruta URI del espacio de nombres debe ser / apk / * lib * not / apk / res.
thom_nic
2
@ThomNichols el apk/libtruco no funcionó para mí en atributos personalizados con formato de referencia de un proyecto de biblioteca. Lo que funcionó fue usar apk/res-auto, como se sugiere en stackoverflow.com/a/13420366/22904 justo debajo y también en stackoverflow.com/a/10217752
Giulio Piancastelli
1
Citando a @Qberticus: "los atributos de marca son similares, excepto que los valores deben definirse para que puedan combinarse". En mi opinión, esto es una especie de subestimación de la principal diferencia entre enumy flag: el primero nos permite elegir un único valor, el segundo nos permite combinar varios. Escribí una respuesta más larga en una pregunta similar aquí , y ahora que he encontrado esta pregunta, pensé que vincularía a eso.
Rad Haring
55
a.recycle()es muy importante aquí para liberar memoria
Tash Pemhiwa
87

La respuesta de Qberticus es buena, pero falta un detalle útil. Si está implementando estos en una biblioteca, reemplace:

xmlns:whatever="http://schemas.android.com/apk/res/org.example.mypackage"

con:

xmlns:whatever="http://schemas.android.com/apk/res-auto"

De lo contrario, la aplicación que usa la biblioteca tendrá errores de tiempo de ejecución.

Neil Miller
fuente
3
Esto fue agregado recientemente ... Creo que hace unas semanas. Ciertamente se agregó mucho después de que Qberticus escribió su respuesta.
ArtOfWarfare
12
Creo que es más antiguo que eso, pero ciertamente fue agregado mucho después de que Qberticus escribió su respuesta. No criticarlo en absoluto, solo agregar un detalle útil.
Neil Miller
11
He actualizado la respuesta de Qbericus para usar apk / res-auto para evitar confusiones.
Intrincaciones
15

La respuesta anterior cubre todo con gran detalle, aparte de un par de cosas.

Primero, si no hay estilos, la (Context context, AttributeSet attrs)firma del método se usará para instanciar la preferencia. En este caso, solo use context.obtainStyledAttributes(attrs, R.styleable.MyCustomView)para obtener el TypedArray.

En segundo lugar, no cubre cómo tratar con los recursos naturales (cadenas de cantidad). No se pueden tratar con TypedArray. Aquí hay un fragmento de código de mi SeekBarPreference que establece el resumen de la preferencia formateando su valor de acuerdo con el valor de la preferencia. Si el xml para la preferencia establece android: summary en una cadena de texto o una cadena de recursos, el valor de la preferencia está formateado en la cadena (debe tener% d, para recoger el valor). Si android: summary está configurado como un recurso plaural, entonces se usa para formatear el resultado.

// Use your own name space if not using an android resource.
final static private String ANDROID_NS = 
    "http://schemas.android.com/apk/res/android";
private int pluralResource;
private Resources resources;
private String summary;

public SeekBarPreference(Context context, AttributeSet attrs) {
    // ...
    TypedArray attributes = context.obtainStyledAttributes(
        attrs, R.styleable.SeekBarPreference);
    pluralResource =  attrs.getAttributeResourceValue(ANDROID_NS, "summary", 0);
    if (pluralResource !=  0) {
        if (! resources.getResourceTypeName(pluralResource).equals("plurals")) {
            pluralResource = 0;
        }
    }
    if (pluralResource ==  0) {
        summary = attributes.getString(
            R.styleable.SeekBarPreference_android_summary);
    }
    attributes.recycle();
}

@Override
public CharSequence getSummary() {
    int value = getPersistedInt(defaultValue);
    if (pluralResource != 0) {
        return resources.getQuantityString(pluralResource, value, value);
    }
    return (summary == null) ? null : String.format(summary, value);
}

  • Esto solo se da como ejemplo, sin embargo, si desea tener la tentación de establecer el resumen en la pantalla de preferencias, debe llamar notifyChanged()al onDialogClosedmétodo de preferencia .
Steve Waring
fuente
5

El enfoque tradicional está lleno de código repetitivo y manejo de recursos torpe. Es por eso que hice el framework Spyglass . Para demostrar cómo funciona, aquí hay un ejemplo que muestra cómo hacer una vista personalizada que muestre un título de Cadena.

Paso 1: crea una clase de vista personalizada.

public class CustomView extends FrameLayout {
    private TextView titleView;

    public CustomView(Context context) {
        super(context);
        init(null, 0, 0);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs, defStyleAttr, 0);
    }

    @RequiresApi(21)
    public CustomView(
            Context context, 
            AttributeSet attrs,
            int defStyleAttr,
            int defStyleRes) {

        super(context, attrs, defStyleAttr, defStyleRes);
        init(attrs, defStyleAttr, defStyleRes);
    }

    public void setTitle(String title) {
        titleView.setText(title);
    }

    private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        inflate(getContext(), R.layout.custom_view, this);

        titleView = findViewById(R.id.title_view);
    }
}

Paso 2: defina un atributo de cadena en el values/attrs.xmlarchivo de recursos:

<resources>
    <declare-styleable name="CustomView">
        <attr name="title" format="string"/>
    </declare-styleable>
</resources>

Paso 3: aplique la @StringHandleranotación al setTitlemétodo para indicarle al marco de Spyglass que enrute el valor del atributo a este método cuando la vista se infla.

@HandlesString(attributeId = R.styleable.CustomView_title)
public void setTitle(String title) {
    titleView.setText(title);
}

Ahora que su clase tiene una anotación Spyglass, el marco Spyglass lo detectará en tiempo de compilación y generará automáticamente la CustomView_SpyglassCompanionclase.

Paso 4: use la clase generada en el initmétodo de la vista personalizada :

private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    inflate(getContext(), R.layout.custom_view, this);

    titleView = findViewById(R.id.title_view);

    CustomView_SpyglassCompanion
            .builder()
            .withTarget(this)
            .withContext(getContext())
            .withAttributeSet(attrs)
            .withDefaultStyleAttribute(defStyleAttr)
            .withDefaultStyleResource(defStyleRes)
            .build()
            .callTargetMethodsNow();
}

Eso es. Ahora, cuando crea una instancia de la clase desde XML, el compañero Spyglass interpreta los atributos y realiza la llamada al método requerido. Por ejemplo, si inflamos el siguiente diseño setTitle, se llamará con "Hello, World!"el argumento.

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:width="match_parent"
    android:height="match_parent">

    <com.example.CustomView
        android:width="match_parent"
        android:height="match_parent"
        app:title="Hello, World!"/>
</FrameLayout>

El marco no se limita a los recursos de cadena tiene muchas anotaciones diferentes para manejar otros tipos de recursos. También tiene anotaciones para definir valores predeterminados y para pasar valores de marcador de posición si sus métodos tienen múltiples parámetros.

Eche un vistazo al repositorio de Github para obtener más información y ejemplos.

Helios
fuente
Puede lograr lo mismo con Google Data Binding: si no hay un enlace de atributo para un atributo específico, GDB intenta encontrar el método set * y lo utiliza en su lugar. En este caso, tendría que escribir, digamos android:title="@{&quot;Hello, world!&quot;}".
Spook
0

si omite el formatatributo del attrelemento, puede usarlo para hacer referencia a una clase de diseños XML.

  • ejemplo de attrs.xml .
  • Android Studio entiende que se hace referencia a la clase desde XML
    • es decir
      • Refactor > Rename trabajos
      • Find Usages trabajos
      • y así...

no especifique un formatatributo en ... / src / main / res / values ​​/ attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="MyCustomView">
        ....
        <attr name="give_me_a_class"/>
        ....
    </declare-styleable>

</resources>

úselo en algún archivo de diseño ... / src / main / res / layout / activity__main_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<SomeLayout
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- make sure to use $ dollar signs for nested classes -->
    <MyCustomView
        app:give_me_a_class="class.type.name.Outer$Nested/>

    <MyCustomView
        app:give_me_a_class="class.type.name.AnotherClass/>

</SomeLayout>

analizar la clase en su código de inicialización de vista ... / src / main / java /.../ MyCustomView.kt

class MyCustomView(
        context:Context,
        attrs:AttributeSet)
    :View(context,attrs)
{
    // parse XML attributes
    ....
    private val giveMeAClass:SomeCustomInterface
    init
    {
        context.theme.obtainStyledAttributes(attrs,R.styleable.ColorPreference,0,0).apply()
        {
            try
            {
                // very important to use the class loader from the passed-in context
                giveMeAClass = context::class.java.classLoader!!
                        .loadClass(getString(R.styleable.MyCustomView_give_me_a_class))
                        .newInstance() // instantiate using 0-args constructor
                        .let {it as SomeCustomInterface}
            }
            finally
            {
                recycle()
            }
        }
    }
Eric
fuente