Definiowanie niestandardowych atrybutów

472

Muszę zaimplementować własne atrybuty jak w com.android.R.attr

Nie znalazłem nic w oficjalnej dokumentacji, więc potrzebuję informacji o tym, jak zdefiniować te attry i jak ich używać z mojego kodu.

Alexander Oleynikov
źródło
20
Dokumenty te mogą być nowsze niż Twój post, ale w celu zachowania aktualności możesz znaleźć dobrą, oficjalną dokumentację dla atrybutów tutaj: developer.android.com/training/custom-views/…
OYRM
Polecam fajny artykuł z przykładem niestandardowych atrybutów: amcmobileware.org/android/blog/2016/09/11/custom-attributes
Arkadiusz Cieśliński
pomocny może być mały przykład działania: github.com/yujiaao/MergeLayout1
Yu

Odpowiedzi:

971

Obecnie najlepszą dokumentacją jest źródło. Możesz na to spojrzeć tutaj (attrs.xml) .

Możesz zdefiniować atrybuty w górnym <resources>elemencie lub wewnątrz <declare-styleable>elementu. Jeśli zamierzam użyć attr w więcej niż jednym miejscu, umieszczam go w elemencie głównym. Uwaga: wszystkie atrybuty mają tę samą globalną przestrzeń nazw. Oznacza to, że nawet jeśli utworzysz nowy atrybut w <declare-styleable>elemencie, można go użyć poza nim i nie możesz utworzyć innego atrybutu o tej samej nazwie innego typu.

<attr>Element ma dwa atrybuty xml namei format. namepozwala ci to nazwać czymś i tak w ten sposób ostatecznie odwołujesz się do niego w kodzie, np R.attr.my_attribute. formatAtrybut może mieć różne wartości w zależności od typu „” atrybutu chcesz.

  • referencja - jeśli odwołuje się do innego identyfikatora zasobu (np. „@ color / my_color”, „@ layout / my_layout”)
  • kolor
  • boolean
  • wymiar
  • pływak
  • liczba całkowita
  • strunowy
  • frakcja
  • enum - zwykle domyślnie zdefiniowane
  • flaga - zwykle domyślnie zdefiniowana

Możesz ustawić format na wiele typów, używając |np format="reference|color".

enum atrybuty można zdefiniować w następujący sposób:

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

flag atrybuty są podobne, z tym wyjątkiem, że wartości muszą zostać zdefiniowane, aby można je było bitować razem:

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

Oprócz atrybutów istnieje <declare-styleable>element. Pozwala to zdefiniować atrybuty, których może używać widok niestandardowy. Robisz to poprzez określenie <attr>elementu, jeśli został wcześniej zdefiniowany, nie określasz format. Jeśli chcesz ponownie użyć Androida ATTR, na przykład Android: grawitacja, możesz to zrobić w namenastępujący sposób.

Przykład niestandardowego widoku <declare-styleable>:

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

Podczas definiowania niestandardowych atrybutów w formacie XML w widoku niestandardowym należy wykonać kilka czynności. Najpierw musisz zadeklarować przestrzeń nazw, aby znaleźć swoje atrybuty. Robisz to na elemencie układu głównego. Zwykle jest tylko xmlns:android="http://schemas.android.com/apk/res/android". Musisz teraz również dodaćxmlns:whatever="http://schemas.android.com/apk/res-auto" .

Przykład:

<?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>

Wreszcie, aby uzyskać dostęp do tego niestandardowego atrybutu, zwykle robi się to w konstruktorze niestandardowego widoku w następujący sposób.

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();
}

Koniec. :)

Rich Schuler
źródło
14
Oto przykładowy projekt demonstrujący niestandardowe atrybuty do użycia z niestandardowym View: github.com/commonsguy/cw-advandroid/tree/master/Views/…
CommonsWare
7
Jeśli używasz niestandardowych atrybutów z projektu bibliotecznego: zobacz to pytanie: stackoverflow.com/questions/5819369/… - Wydaje się, że działa, jeśli używasz xmlns:my="http://schemas.android.com/apk/lib/my.namespace"- bez kopiowania attrs.xml. Uwaga: ścieżka identyfikatora URI przestrzeni nazw musi mieć postać / apk / * lib * not / apk / res.
thom_nic
2
@ThomNichols apk/libsztuczka nie działała dla mnie na niestandardowych atrybutach z formatem referencyjnym z projektu bibliotecznego. Co zrobił praca była do użytku apk/res-auto, jak zasugerowano w stackoverflow.com/a/13420366/22904 tuż poniżej, a także w stackoverflow.com/a/10217752
Giulio Piancastelli
1
Cytując @Qberticus: „atrybuty flagi są podobne, z tym wyjątkiem, że wartości muszą zostać zdefiniowane, aby można je było bitować razem”. Moim zdaniem jest to rodzaj zaniżenia głównej różnicy między enumi flag: pierwsza pozwala nam wybrać jedną i tylko jedną wartość, druga pozwala połączyć kilka. Napisałem dłuższą odpowiedź w podobnym pytaniu tutaj i po znalezieniu tego pytania pomyślałem, że będę z nim link.
Rad Haring
5
a.recycle()jest bardzo ważne, aby zwolnić pamięć
Tash Pemhiwa
87

Odpowiedź Qberticusa jest dobra, ale brakuje jednego przydatnego szczegółu. Jeśli implementujesz je w bibliotece, zamień:

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

z:

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

W przeciwnym razie aplikacja korzystająca z biblioteki będzie miała błędy w czasie wykonywania.

Neil Miller
źródło
3
Zostało to niedawno dodane ... Myślę, że kilka tygodni temu. Z pewnością został dodany długo po tym, jak Qberticus napisał swoją odpowiedź.
ArtOfWarfare
12
Myślę, że jest starszy, ale z pewnością został dodany długo po tym, jak Qberticus napisał swoją odpowiedź. Wcale go nie winił, po prostu dodając użyteczny szczegół.
Neil Miller
11
Zaktualizowałem odpowiedź Qbericusa, aby użyć apk / res-auto, aby uniknąć zamieszania.
Zawiłości
15

Powyższa odpowiedź obejmuje wszystko bardzo szczegółowo, z wyjątkiem kilku rzeczy.

Po pierwsze, jeśli nie ma stylów, (Context context, AttributeSet attrs)do utworzenia preferencji zostanie użyta sygnatura metody. W takim przypadku wystarczy użyć, context.obtainStyledAttributes(attrs, R.styleable.MyCustomView)aby uzyskać TypedArray.

Po drugie, nie obejmuje to, jak radzić sobie z zasobami kamienia nazębnego (ciągi ilości). Nie można sobie z tym poradzić przy użyciu TypedArray. Oto fragment kodu z mojego SeekBarPreference, który ustawia podsumowanie preferencji formatując jego wartość zgodnie z wartością preferencji. Jeśli xml dla preferencji ustawia android: podsumowanie na ciąg tekstowy lub ciąg znaków resouce, wartość preferencji jest formatowana do ciągu (powinien zawierać% d, aby podnieść wartość). Jeśli Android: podsumowanie jest ustawione na zasób plaurals, to jest on używany do formatowania wyniku.

// 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);
}

  • Jest to tylko przykład, ale jeśli chcesz pokusić się o ustawienie podsumowania na ekranie preferencji, musisz zadzwonić notifyChanged()onDialogClosed metodę preferencji .
Steve Waring
źródło
5

Tradycyjne podejście jest pełne kodu podstawowego i niezręcznej obsługi zasobów. Dlatego stworzyłem szkielet Spyglass . Aby zademonstrować, jak to działa, oto przykład pokazujący, jak utworzyć niestandardowy widok, który wyświetla tytuł String.

Krok 1: Utwórz niestandardową klasę widoku.

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);
    }
}

Krok 2: Zdefiniuj atrybut ciągu w values/attrs.xmlpliku zasobów:

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

Krok 3: Zastosuj @StringHandleradnotację do setTitlemetody, aby poinformować środowisko Spyglass, aby kierowała wartość atrybutu do tej metody, gdy widok jest zawyżony.

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

Teraz, gdy Twoja klasa ma adnotację Spyglass, struktura Spyglass wykryje ją w czasie kompilacji i automatycznie wygeneruje CustomView_SpyglassCompanion klasę.

Krok 4: Użyj wygenerowanej klasy w initmetodzie widoku niestandardowego :

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();
}

Otóż ​​to. Teraz, gdy tworzysz instancję klasy z XML, towarzysz Spyglass interpretuje atrybuty i wykonuje wymagane wywołanie metody. Na przykład, jeśli napompujemy następujący układ, setTitlezostaniemy wywołani z "Hello, World!"argumentem.

<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>

Struktura nie ogranicza się do zasobów łańcuchowych i zawiera wiele różnych adnotacji do obsługi innych typów zasobów. Zawiera także adnotacje do definiowania wartości domyślnych i przekazywania wartości zastępczych, jeśli metody mają wiele parametrów.

Zobacz repozytorium Github, aby uzyskać więcej informacji i przykładów.

Helios
źródło
To samo możesz osiągnąć dzięki usłudze Google Data Binding - jeśli nie ma powiązania atrybutu dla konkretnego atrybutu, GDB próbuje znaleźć metodę set * i używa jej zamiast tego. W takim przypadku musiałbyś napisać, powiedzmy android:title="@{&quot;Hello, world!&quot;}".
Spook
0

jeśli pominiesz formatatrybut w attrelemencie, możesz go użyć do odwołania się do klasy z układów XML.

  • przykład z attrs.xml .
  • Android Studio rozumie, że do klasy odwołuje się XML
    • to znaczy
      • Refactor > Rename Pracuje
      • Find Usages Pracuje
      • i tak dalej...

nie określaj formatatrybutu w ... / 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>

użyj go w pliku układu ... / 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>

przeanalizuj klasę w kodzie inicjalizacji widoku ... / 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
źródło