Привязка адаптеров

Адаптеры привязки отвечают за выполнение соответствующих вызовов платформы для установки значений. Одним из примеров является установка значения свойства, например вызов метода setText() . Другой пример — установка прослушивателя событий, например вызов метода setOnClickListener() .

Библиотека привязки данных позволяет указать метод, вызываемый для установки значения, предоставить собственную логику привязки и указать тип возвращаемого объекта с помощью адаптеров.

Установить значения атрибутов

При каждом изменении связанного значения созданный класс привязки должен вызывать метод установки представления с выражением привязки. Вы можете позволить библиотеке привязки данных автоматически определять метод или явно объявить метод или предоставить собственную логику для выбора метода.

Автоматический выбор метода

Для атрибута с именем example библиотека автоматически находит метод setExample(arg) , который принимает в качестве аргумента совместимые типы. Пространство имен атрибута не учитывается. При поиске метода используются только имя и тип атрибута.

Например, учитывая выражение android:text="@{user.name}" библиотека ищет метод setText(arg) , который принимает тип, возвращаемый user.getName() . Если тип возвращаемого значения user.getName()String , библиотека ищет метод setText() , который принимает аргумент String . Если выражение возвращает int , библиотека ищет метод setText() , который принимает аргумент int . Выражение должно возвращать правильный тип. При необходимости вы можете привести возвращаемое значение.

Привязка данных работает, даже если атрибута с данным именем не существует. Вы можете создать атрибуты для любого установщика, используя привязку данных. Например, класс поддержки DrawerLayout не имеет атрибутов, но имеет множество установщиков. Следующий макет автоматически использует методы setScrimColor(int) и addDrawerListener(DrawerListener) в качестве установщика атрибутов app:scrimColor и app:drawerListener соответственно:

<androidx.drawerlayout.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}">

Укажите имя пользовательского метода

У некоторых атрибутов есть установщики, имена которых не совпадают. В таких ситуациях атрибут можно связать с установщиком с помощью аннотации BindingMethods . Аннотация используется с классом и может содержать несколько аннотаций BindingMethod , по одной для каждого переименованного метода. Методы привязки — это аннотации, которые вы можете добавить в любой класс вашего приложения.

В следующем примере атрибут android:tint связан с методом setImageTintList(ColorStateList) , а не с методом setTint() :

Котлин

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])

Ява

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

Обычно вам не нужно переименовывать установщики в классах платформы Android. Атрибуты уже реализованы с использованием соглашения об именах для автоматического поиска соответствующих методов.

Обеспечьте собственную логику

Для некоторых атрибутов требуется специальная логика привязки. Например, для атрибута android:paddingLeft не существует связанного установщика. Вместо этого предоставляется метод setPadding(left, top, right, bottom) . Метод адаптера статической привязки с аннотацией BindingAdapter позволяет настроить способ вызова установщика атрибута.

Атрибуты классов платформы Android уже имеют аннотации BindingAdapter . В следующем примере показан адаптер привязки для атрибута paddingLeft :

Котлин

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

Ява

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

Типы параметров важны. Первый параметр определяет тип представления, связанного с атрибутом. Второй параметр определяет тип, принятый в выражении привязки для данного атрибута.

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

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

Котлин

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

Ява

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}

Вы можете использовать адаптер в своем макете, как показано в следующем примере. Обратите внимание, что @drawable/venueError относится к ресурсу в вашем приложении. Окружение ресурса @{} делает его допустимым выражением привязки.

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

Адаптер вызывается, если imageUrl и error используются для объекта ImageView , imageUrl — это строка, а error — это Drawable . Если вы хотите, чтобы адаптер вызывался при установке любого из атрибутов, установите для дополнительного флага requireAll адаптера значение false , как показано в следующем примере:

Котлин

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

Ява

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

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

Котлин

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

Ява

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
  if (oldPadding != newPadding) {
      view.setPadding(newPadding,
                      view.getPaddingTop(),
                      view.getPaddingRight(),
                      view.getPaddingBottom());
   }
}

Обработчики событий можно использовать только с интерфейсами или абстрактными классами с одним абстрактным методом, как показано в следующем примере:

Котлин

@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

Ява

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}

Используйте этот обработчик событий в своем макете следующим образом:

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

Если у прослушивателя есть несколько методов, его необходимо разделить на несколько прослушивателей. Например, View.OnAttachStateChangeListener имеет два метода: onViewAttachedToWindow(View) и onViewDetachedFromWindow(View) . Библиотека предоставляет два интерфейса для различения атрибутов и их обработчиков:

Котлин

// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
    fun onViewDetachedFromWindow(v: View)
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
    fun onViewAttachedToWindow(v: View)
}

Ява

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

Поскольку изменение одного прослушивателя может повлиять на другой, вам нужен адаптер, который работает с любым атрибутом или с обоими. Вы можете установить для requireAll значение false в аннотации, чтобы указать, что не каждому атрибуту должно быть назначено выражение привязки, как показано в следующем примере:

Котлин

@BindingAdapter(
        "android:onViewDetachedFromWindow",
        "android:onViewAttachedToWindow",
        requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
        val newListener: View.OnAttachStateChangeListener?
        newListener = if (detach == null && attach == null) {
            null
        } else {
            object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View) {
                    attach.onViewAttachedToWindow(v)
                }

                override fun onViewDetachedFromWindow(v: View) {
                    detach.onViewDetachedFromWindow(v)
                }
            }
        }

        val oldListener: View.OnAttachStateChangeListener? =
                ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener)
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener)
        }
    }
}

Ява

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

Приведенный выше пример немного сложен, поскольку класс View использует методы addOnAttachStateChangeListener() и removeOnAttachStateChangeListener() вместо метода установки для OnAttachStateChangeListener . Класс android.databinding.adapters.ListenerUtil помогает отслеживать эти прослушиватели, чтобы их можно было удалить в адаптере привязки.

Преобразования объектов

Автоматическое преобразование объектов

Когда Object возвращается из выражения привязки, библиотека выбирает метод, используемый для установки значения свойства. Object преобразуется к типу параметра выбранного метода. Такое поведение удобно в приложениях, использующих класс ObservableMap для хранения данных, как показано в следующем примере:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

Объект userMap в выражении возвращает значение, которое автоматически преобразуется к типу параметра, найденному в методе setText(CharSequence) используемом для установки значения атрибута android:text . Если тип параметра неоднозначен, приведите тип возвращаемого значения в выражении.

Пользовательские преобразования

В некоторых ситуациях требуется пользовательское преобразование между конкретными типами. Например, атрибут представления android:background ожидает Drawable , но указанное значение color является целым числом. В следующем примере показан атрибут, который ожидает Drawable , но вместо него предоставляется целое число:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Всякий раз, когда ожидается Drawable и возвращается целое число, преобразуйте int в ColorDrawable . Чтобы выполнить преобразование, используйте статический метод с аннотацией BindingConversion , как показано ниже:

Котлин

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

Ява

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

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

// The @drawable and @color represent different value types in the same
// expression, which causes a build error.
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Дополнительные ресурсы

Дополнительные сведения о привязке данных см. в следующих ресурсах.

Образцы

Кодлабы

Сообщения в блоге

{% дословно %} {% дословно %} {% дословно %} {% дословно %}