Определение пользовательских атрибутов

472

Мне нужно реализовать свои собственные атрибуты, как в com.android.R.attr

В официальной документации ничего не найдено, поэтому мне нужна информация о том, как определить эти атрибуты и как их использовать из моего кода.

Александр Олейников
источник
20
Эти документы могут быть новее, чем ваш пост, но для того, чтобы сохранить их актуальность, вы можете найти хорошую официальную документацию по атрибутам здесь: developer.android.com/training/custom-views/…
OYRM
Я рекомендую хорошую статью с примером о пользовательских атрибутах: amcmobileware.org/android/blog/2016/09/11/custom-attributes
Аркадиуш Цешлинский
Небольшой рабочий пример может быть полезен: github.com/yujiaao/MergeLayout1
Yu Jiaao

Ответы:

971

На данный момент лучшей документацией является источник. Вы можете посмотреть на это здесь (attrs.xml) .

Вы можете определить атрибуты в верхнем <resources>элементе или внутри <declare-styleable>элемента. Если я собираюсь использовать attr в нескольких местах, я помещаю его в корневой элемент. Обратите внимание, что все атрибуты имеют одно и то же глобальное пространство имен. Это означает, что даже если вы создадите новый атрибут внутри <declare-styleable>элемента, его можно будет использовать вне его, и вы не сможете создать другой атрибут с тем же именем другого типа.

<attr>Элемент имеет два атрибута XML nameи format. nameпозволяет назвать это что - то и это, как вы в конечном итоге со ссылкой на него в коде, например, R.attr.my_attribute. formatАтрибут может иметь разные значения в зависимости от «типа» атрибута вы хотите.

  • ссылка - если он ссылается на другой идентификатор ресурса (например, "@ color / my_color", "@ layout / my_layout")
  • цвет
  • логический
  • измерение
  • поплавок
  • целое число
  • строка
  • доля
  • enum - обычно неявно определяется
  • флаг - обычно неявно определенный

Вы можете установить формат для нескольких типов, используя |, например, format="reference|color".

enum атрибуты могут быть определены следующим образом:

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

flag атрибуты похожи, за исключением того, что значения должны быть определены, чтобы их можно было объединить в биты:

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

Помимо атрибутов есть <declare-styleable>элемент. Это позволяет вам определять атрибуты, которые может использовать пользовательский вид. Вы делаете это, указывая <attr>элемент, если он был определен ранее, вы не указываете format. Если вы хотите повторно использовать атрибут Android, например, Android: gravity, то вы можете сделать это nameследующим образом.

Пример пользовательского представления <declare-styleable>:

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

При определении ваших пользовательских атрибутов в XML в вашем пользовательском представлении вам нужно сделать несколько вещей. Сначала вы должны объявить пространство имен, чтобы найти ваши атрибуты. Вы делаете это на корневом элементе макета. Обычно есть только xmlns:android="http://schemas.android.com/apk/res/android". Вы должны теперь также добавить xmlns:whatever="http://schemas.android.com/apk/res-auto".

Пример:

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

Наконец, для доступа к этому пользовательскому атрибуту вы обычно делаете это в конструкторе своего пользовательского представления следующим образом.

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

Конец. :)

Рич Шулер
источник
14
Вот пример проекта, демонстрирующий пользовательские атрибуты для использования с пользовательскими View: github.com/commonsguy/cw-advandroid/tree/master/Views/…
CommonsWare,
7
Если вы используете пользовательские атрибуты из библиотечного проекта: посмотрите этот вопрос: stackoverflow.com/questions/5819369/… - Кажется, это работает, если вы используете xmlns:my="http://schemas.android.com/apk/lib/my.namespace"- не копировать attrs.xml. Обратите внимание, что путь URI пространства имен должен быть / apk / * lib *, а не / apk / res.
thom_nic
2
@ ThomNichols apk/libуловка не работала для меня на пользовательских атрибутах с форматом ссылки из проекта библиотеки. Что же работа заключалась в использовании apk/res-auto, как это было предложено в stackoverflow.com/a/13420366/22904 чуть ниже , а также в stackoverflow.com/a/10217752
Джулио Piancastelli
1
Цитирование @Qberticus: «Атрибуты флага похожи, за исключением того, что значения должны быть определены, чтобы их можно было объединять в биты». По моему мнению, это как бы подчеркивает основное различие между enumи flag: первое позволяет нам выбирать одно и только одно значение, второе позволяет объединять несколько. Я написал более длинный ответ на подобный вопрос здесь , и теперь, найдя этот вопрос, я решил связать его с этим.
Рад Харинг
5
a.recycle()здесь очень важно освободить память
Таш Пемхива
87

Ответ Qberticus хорош, но одна полезная деталь отсутствует. Если вы реализуете их в библиотеке, замените:

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

с:

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

В противном случае приложение, которое использует библиотеку, будет иметь ошибки во время выполнения.

Нил Миллер
источник
3
Это было добавлено только недавно ... Я думаю, что в течение нескольких недель назад. Конечно, это было добавлено задолго до того, как Квертикус написал свой ответ.
ArtOfWarfare
12
Я думаю, что он старше этого, но он, безусловно, был добавлен еще долго после того, как Qberticus написал свой ответ Не винить его вообще, просто добавив полезную деталь.
Нил Миллер
11
Я обновил ответ Qbericus, чтобы использовать apk / res-auto, чтобы избежать путаницы.
Затруднения
15

Ответ выше охватывает все очень подробно, за исключением нескольких вещей.

Во-первых, если нет стилей, то (Context context, AttributeSet attrs)сигнатура метода будет использоваться для создания экземпляра предпочтения. В этом случае просто используйте, context.obtainStyledAttributes(attrs, R.styleable.MyCustomView)чтобы получить TypedArray.

Во-вторых, он не охватывает, как обращаться с ресурсами plaurals (количество строк). Это не может быть решено с помощью TypedArray. Вот фрагмент кода из моего SeekBarPreference, который устанавливает сводку предпочтения, форматируя его значение в соответствии со значением предпочтения. Если xml для предпочтения устанавливает android: summary в текстовую строку или в строку resouce, значение предпочтения форматируется в строку (в ней должно быть% d, чтобы получить значение). Если для android: summary задан ресурс plaurals, то он используется для форматирования результата.

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

  • Это просто приведено в качестве примера, однако, если вы хотите задать сводку на экране предпочтений, вам нужно вызвать notifyChanged()метод предпочтения onDialogClosed.
Стив Уоринг
источник
5

Традиционный подход полон стандартного кода и неуклюжей обработки ресурсов. Вот почему я сделал каркас Spyglass . Чтобы продемонстрировать, как это работает, вот пример, показывающий, как создать собственное представление, отображающее заголовок String.

Шаг 1: Создайте пользовательский класс представления.

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

Шаг 2: Определите строковый атрибут в values/attrs.xmlфайле ресурсов:

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

Шаг 3: Примените @StringHandlerаннотацию к setTitleметоду, чтобы сообщить платформе Spyglass о необходимости перенаправить значение атрибута этому методу при завышении представления.

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

Теперь, когда у вашего класса есть аннотация Spyglass, платформа Spyglass обнаружит его во время компиляции и автоматически сгенерирует CustomView_SpyglassCompanionкласс.

Шаг 4. Используйте сгенерированный класс в initметоде пользовательского представления :

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

Вот и все. Теперь, когда вы создаете экземпляр класса из XML, собеседник Spyglass интерпретирует атрибуты и выполняет требуемый вызов метода. Например, если мы надуваем следующий макет, то в качестве аргумента setTitleбудем вызывать "Hello, World!".

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

Фреймворк не ограничивается строковыми ресурсами, имеет множество различных аннотаций для обработки других типов ресурсов. Он также имеет аннотации для определения значений по умолчанию и для передачи значений заполнителей, если ваши методы имеют несколько параметров.

Взгляните на репозиторий Github для получения дополнительной информации и примеров.

Helios
источник
Вы можете добиться того же с помощью привязки данных Google - если для определенного атрибута нет привязки атрибута, GDB пытается найти метод set * и использует его вместо этого. В этом случае вам придется написать, скажем android:title="@{&quot;Hello, world!&quot;}".
Spook
0

если вы опустите formatатрибут в attrэлементе, вы можете использовать его для ссылки на класс из макетов XML.

  • пример из attrs.xml .
  • Android Studio понимает, что на класс ссылаются из XML
    • т.е.
      • Refactor > Rename работает
      • Find Usages работает
      • и так далее...

не указывайте formatатрибут в ... / 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>

использовать его в каком-то файле макета ... / 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>

разобрать класс в коде инициализации вашего представления ... / 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()
            }
        }
    }
Эрик
источник