Android 使用 2 向绑定的微调器设置选择
Android Spinner Setting Selection with 2-Way Binding
当配置了 2 向数据绑定时,我正在努力获得一些与 Android 微调器一起工作的功能。我想通过 android:selectedItemPosition
上的双向数据绑定设置微调器的初始值。微调器条目由 ViewModel 初始化并正确填充,因此数据绑定似乎正常工作。
问题出在 selectedItemPosition
的双向绑定上。该变量由 ViewModel 初始化为 5,但微调器的选定项保持为 0(第一项)。调试时,ObservableInt 的值似乎最初为 5(设置),但在 executeBindings 的第二阶段重置为零。
如有任何帮助,我们将不胜感激。
test_spinner_activity.xml
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewModel"
type="com.aapp.viewmodel.TestSpinnerViewModel"/>
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.AppCompatSpinner
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/sTimeHourSpinner"
android:selectedItemPosition="@={viewModel.startHourIdx}"
android:entries="@{viewModel.startTimeHourSelections}"/>
</LinearLayout>
</layout>
TestSpinnerViewModel.java
public class TestSpinnerViewModel {
public final ObservableArrayList<String> startTimeHourSelections = new ObservableArrayList<>();
public final ObservableInt startHourIdx = new ObservableInt();
public TestSpinnerViewModel(Context context) {
this.mContext = context;
for (int i=0; i < 24; i++) {
int hour = i;
startTimeHourSelections.add(df.format(hour));
}
startHourIdx.set(5);
}
}
TestSpinnerActivity.java
public class TestSpinnerActivity extends AppCompatActivity {
private TestSpinnerActivityBinding binding;
private TestSpinnerViewModel mTestSpinnerViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.bind(findViewById(R.id.test_spinner));
mTestSpinnerViewModel = new TestSpinnerViewModel(this);
binding.setViewModel(mTestSpinnerViewModel);
}
我正在使用 Android Studio 2.2.2 并且启用了数据绑定。
我最近在 GitHub 上创建了一个演示应用程序,以展示如何利用 bindingAdapter 和 InverseBindingAdapter 机制在微调器上实现双向数据绑定。
在这个应用程序中,我没有绑定 "android:selectedItemPosition" 属性,而是绑定了微调器的所选项目本身(利用 ObservableField class),如下面的代码片段所示。因为它是双向绑定,通过在微调器适配器设置期间为绑定的 ObservableField(即选定项)分配初始值,以及微调器的 bindingAdapter 中的特殊处理,可以实现微调器初始选择。
随时查看演示应用程序 here 了解更多详情。
acivity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="bindingPlanet"
type="au.com.chrisli.spinnertwowaydatabindingdemo.BindingPlanet"/>
<variable
name="spinAdapterPlanet"
type="android.widget.ArrayAdapter"/>
</data>
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
...>
<android.support.v7.widget.AppCompatSpinner
android:id="@+id/spin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"
bind:selectedPlanet="@={bindingPlanet.obvSelectedPlanet_}"
app:adapter="@{spinAdapterPlanet}"/>
...(not relevant content omitted for simplicity)
</RelativeLayout>
</layout>
BindingPlanet.java
绑定适配器内的特殊处理
public final ObservableField<Planet> obvSelectedPlanet_ = new ObservableField<>(); //for simplicity, we use a public variable here
private static class SpinPlanetOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
private Planet initialSelectedPlanet_;
private InverseBindingListener inverseBindingListener_;
public SpinPlanetOnItemSelectedListener(Planet initialSelectedPlanet, InverseBindingListener inverseBindingListener) {
initialSelectedPlanet_ = initialSelectedPlanet;
inverseBindingListener_ = inverseBindingListener;
}
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
if (initialSelectedPlanet_ != null) {
//Adapter is not ready yet but there is already a bound data,
//hence we need to set a flag so we can implement a special handling inside the OnItemSelectedListener
//for the initial selected item
Integer positionInAdapter = getPlanetPositionInAdapter((ArrayAdapter<Planet>) adapterView.getAdapter(), initialSelectedPlanet_);
if (positionInAdapter != null) {
adapterView.setSelection(positionInAdapter); //set spinner selection as there is a match
}
initialSelectedPlanet_ = null; //set to null as the initialization is done
} else {
if (inverseBindingListener_ != null) {
inverseBindingListener_.onChange();
}
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {}
}
@BindingAdapter(value = {"bind:selectedPlanet", "bind:selectedPlanetAttrChanged"}, requireAll = false)
public static void bindPlanetSelected(final AppCompatSpinner spinner, Planet planetSetByViewModel,
final InverseBindingListener inverseBindingListener) {
Planet initialSelectedPlanet = null;
if (spinner.getAdapter() == null && planetSetByViewModel != null) {
//Adapter is not ready yet but there is already a bound data,
//hence we need to set a flag in order to implement a special handling inside the OnItemSelectedListener
//for the initial selected item, otherwise the first item will be selected by the framework
initialSelectedPlanet = planetSetByViewModel;
}
spinner.setOnItemSelectedListener(new SpinPlanetOnItemSelectedListener(initialSelectedPlanet, inverseBindingListener));
//only proceed further if the newly selected planet is not equal to the already selected item in the spinner
if (planetSetByViewModel != null && !planetSetByViewModel.equals(spinner.getSelectedItem())) {
//find the item in the adapter
Integer positionInAdapter = getPlanetPositionInAdapter((ArrayAdapter<Planet>) spinner.getAdapter(), planetSetByViewModel);
if (positionInAdapter != null) {
spinner.setSelection(positionInAdapter); //set spinner selection as there is a match
}
}
}
@InverseBindingAdapter(attribute = "bind:selectedPlanet", event = "bind:selectedPlanetAttrChanged")
public static Planet captureSelectedPlanet(AppCompatSpinner spinner) {
return (Planet) spinner.getSelectedItem();
}
感谢您的建议。但我找到了我自己问题的答案。事实证明,android:selectedItemPosition=@={viewModel.startHourIdx}
变量从初始值 5 重置为 0 的原因是因为 selectedItemPosition
和 entries
属性的声明顺序。在我的示例中,它们是按特定顺序声明的,自动生成的绑定代码以相同顺序生成初始化。
因此,即使 selectedItemPosition
设置正确,entries
的初始化也会导致 ArrayAdapter 实例化,从而将 selectedItemPosition
重置为 0。
因此,解决方法是交换布局文件中的两个属性声明。
<data>
<variable name="viewModel"
type="com.aapp.viewmodel.TestSpinnerViewModel"/>
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.AppCompatSpinner
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/sTimeHourSpinner"
android:entries="@{viewModel.startTimeHourSelections}"
android:selectedItemPosition="@={viewModel.startHourIdx}"/>
</LinearLayout>
当配置了 2 向数据绑定时,我正在努力获得一些与 Android 微调器一起工作的功能。我想通过 android:selectedItemPosition
上的双向数据绑定设置微调器的初始值。微调器条目由 ViewModel 初始化并正确填充,因此数据绑定似乎正常工作。
问题出在 selectedItemPosition
的双向绑定上。该变量由 ViewModel 初始化为 5,但微调器的选定项保持为 0(第一项)。调试时,ObservableInt 的值似乎最初为 5(设置),但在 executeBindings 的第二阶段重置为零。
如有任何帮助,我们将不胜感激。
test_spinner_activity.xml
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewModel"
type="com.aapp.viewmodel.TestSpinnerViewModel"/>
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.AppCompatSpinner
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/sTimeHourSpinner"
android:selectedItemPosition="@={viewModel.startHourIdx}"
android:entries="@{viewModel.startTimeHourSelections}"/>
</LinearLayout>
</layout>
TestSpinnerViewModel.java
public class TestSpinnerViewModel {
public final ObservableArrayList<String> startTimeHourSelections = new ObservableArrayList<>();
public final ObservableInt startHourIdx = new ObservableInt();
public TestSpinnerViewModel(Context context) {
this.mContext = context;
for (int i=0; i < 24; i++) {
int hour = i;
startTimeHourSelections.add(df.format(hour));
}
startHourIdx.set(5);
}
}
TestSpinnerActivity.java
public class TestSpinnerActivity extends AppCompatActivity {
private TestSpinnerActivityBinding binding;
private TestSpinnerViewModel mTestSpinnerViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.bind(findViewById(R.id.test_spinner));
mTestSpinnerViewModel = new TestSpinnerViewModel(this);
binding.setViewModel(mTestSpinnerViewModel);
}
我正在使用 Android Studio 2.2.2 并且启用了数据绑定。
我最近在 GitHub 上创建了一个演示应用程序,以展示如何利用 bindingAdapter 和 InverseBindingAdapter 机制在微调器上实现双向数据绑定。
在这个应用程序中,我没有绑定 "android:selectedItemPosition" 属性,而是绑定了微调器的所选项目本身(利用 ObservableField class),如下面的代码片段所示。因为它是双向绑定,通过在微调器适配器设置期间为绑定的 ObservableField(即选定项)分配初始值,以及微调器的 bindingAdapter 中的特殊处理,可以实现微调器初始选择。
随时查看演示应用程序 here 了解更多详情。
acivity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="bindingPlanet"
type="au.com.chrisli.spinnertwowaydatabindingdemo.BindingPlanet"/>
<variable
name="spinAdapterPlanet"
type="android.widget.ArrayAdapter"/>
</data>
<RelativeLayout
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
...>
<android.support.v7.widget.AppCompatSpinner
android:id="@+id/spin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"
bind:selectedPlanet="@={bindingPlanet.obvSelectedPlanet_}"
app:adapter="@{spinAdapterPlanet}"/>
...(not relevant content omitted for simplicity)
</RelativeLayout>
</layout>
BindingPlanet.java
绑定适配器内的特殊处理public final ObservableField<Planet> obvSelectedPlanet_ = new ObservableField<>(); //for simplicity, we use a public variable here
private static class SpinPlanetOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
private Planet initialSelectedPlanet_;
private InverseBindingListener inverseBindingListener_;
public SpinPlanetOnItemSelectedListener(Planet initialSelectedPlanet, InverseBindingListener inverseBindingListener) {
initialSelectedPlanet_ = initialSelectedPlanet;
inverseBindingListener_ = inverseBindingListener;
}
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
if (initialSelectedPlanet_ != null) {
//Adapter is not ready yet but there is already a bound data,
//hence we need to set a flag so we can implement a special handling inside the OnItemSelectedListener
//for the initial selected item
Integer positionInAdapter = getPlanetPositionInAdapter((ArrayAdapter<Planet>) adapterView.getAdapter(), initialSelectedPlanet_);
if (positionInAdapter != null) {
adapterView.setSelection(positionInAdapter); //set spinner selection as there is a match
}
initialSelectedPlanet_ = null; //set to null as the initialization is done
} else {
if (inverseBindingListener_ != null) {
inverseBindingListener_.onChange();
}
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {}
}
@BindingAdapter(value = {"bind:selectedPlanet", "bind:selectedPlanetAttrChanged"}, requireAll = false)
public static void bindPlanetSelected(final AppCompatSpinner spinner, Planet planetSetByViewModel,
final InverseBindingListener inverseBindingListener) {
Planet initialSelectedPlanet = null;
if (spinner.getAdapter() == null && planetSetByViewModel != null) {
//Adapter is not ready yet but there is already a bound data,
//hence we need to set a flag in order to implement a special handling inside the OnItemSelectedListener
//for the initial selected item, otherwise the first item will be selected by the framework
initialSelectedPlanet = planetSetByViewModel;
}
spinner.setOnItemSelectedListener(new SpinPlanetOnItemSelectedListener(initialSelectedPlanet, inverseBindingListener));
//only proceed further if the newly selected planet is not equal to the already selected item in the spinner
if (planetSetByViewModel != null && !planetSetByViewModel.equals(spinner.getSelectedItem())) {
//find the item in the adapter
Integer positionInAdapter = getPlanetPositionInAdapter((ArrayAdapter<Planet>) spinner.getAdapter(), planetSetByViewModel);
if (positionInAdapter != null) {
spinner.setSelection(positionInAdapter); //set spinner selection as there is a match
}
}
}
@InverseBindingAdapter(attribute = "bind:selectedPlanet", event = "bind:selectedPlanetAttrChanged")
public static Planet captureSelectedPlanet(AppCompatSpinner spinner) {
return (Planet) spinner.getSelectedItem();
}
感谢您的建议。但我找到了我自己问题的答案。事实证明,android:selectedItemPosition=@={viewModel.startHourIdx}
变量从初始值 5 重置为 0 的原因是因为 selectedItemPosition
和 entries
属性的声明顺序。在我的示例中,它们是按特定顺序声明的,自动生成的绑定代码以相同顺序生成初始化。
因此,即使 selectedItemPosition
设置正确,entries
的初始化也会导致 ArrayAdapter 实例化,从而将 selectedItemPosition
重置为 0。
因此,解决方法是交换布局文件中的两个属性声明。
<data>
<variable name="viewModel"
type="com.aapp.viewmodel.TestSpinnerViewModel"/>
</data>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.AppCompatSpinner
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/sTimeHourSpinner"
android:entries="@{viewModel.startTimeHourSelections}"
android:selectedItemPosition="@={viewModel.startHourIdx}"/>
</LinearLayout>