Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android的下拉框——Spinner #119

Open
3 tasks done
soapgu opened this issue Mar 8, 2022 · 0 comments
Open
3 tasks done

Android的下拉框——Spinner #119

soapgu opened this issue Mar 8, 2022 · 0 comments
Labels
安卓 安卓

Comments

@soapgu
Copy link
Owner

soapgu commented Mar 8, 2022

  • 引言

几乎没有platform都有自己的下拉框,安卓翻一下官网教程,看上去应该用的是Spinner

  • 标准用法

  1. layout声明
<Spinner
            android:id="@+id/spinnerTest"
            android:layout_marginTop="300dp"
            android:layout_width="150dp"
            android:layout_height="80dp" />
  1. 定义SpinnerAdapter

SpinnerAdapter是接口,我们使用的他的一个具体实现类ArrayAdapter

         private static final String[] buildings ={"宝石大楼","25号楼"};

         Spinner spinner = view.findViewById(R.id.spinnerTest);
        ArrayAdapter<String> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_spinner_item, buildings);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinner.setAdapter(adapter);

其中比较陌生的是两个Resource的定义

  • 构造参数里面android.R.layout.simple_spinner_item
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@android:id/text1"
    style="?android:attr/spinnerItemStyle"
    android:singleLine="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:ellipsize="marquee"
    android:textAlignment="inherit"/>

通过ide找过去发现就一个文本显示,表示展开的单项的UI

  • setDropDownViewResource的参数android.R.layout.simple_spinner_dropdown_item
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    style="?android:attr/spinnerDropDownItemStyle"
    android:singleLine="true"
    android:layout_width="match_parent"
    android:layout_height="?android:attr/dropdownListPreferredItemHeight"
    android:ellipsize="marquee"/>

这个应该是未展开时候的UI主体

  1. OnItemSelectedListener监听
         spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                String building = (String) parent.getItemAtPosition(position);
                MessageHelper.ShowToast( view.getContext(), String.format("%s building selected",building) );
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });

可以监听选中获取未选的情况

  • 好像少了点什么不开心

就是现在的代码我只能写在View层,并不能和我的业务数据好好的结合
我要解决以下问题

  • 从ViewModel层绑定数据源

  • 选中项需要能持有

  • 监听选中事件到ViewModel的Method

  • 将MVVM进行到底!

public class SpinnerBindingAdapters {
    @BindingAdapter(value = {"itemsSource","onItemSelected","selectedIndex"},requireAll = false)
    public static <T> void setSpinnerItems(Spinner spinner , List<T> itemsSource,final OnMyItemSelectedListener listener , int index){
        SimpleArrayAdapter<T> adapter;
        @SuppressWarnings("unchecked")
        SimpleArrayAdapter<T> oldAdapter = (SimpleArrayAdapter<T>)spinner.getAdapter();
        boolean createNew = oldAdapter != null && oldAdapter.list == itemsSource;
        if( createNew ){
            adapter = oldAdapter;
            adapter.notifyDataSetChanged();
        }
        else {
            adapter = new SimpleArrayAdapter<>(spinner.getContext(), android.R.layout.simple_spinner_item, itemsSource);
            adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
            spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
                private Optional<Integer> lastPosition= Optional.empty();
                @Override
                public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                    //String itemValue = (String) parent.getItemAtPosition(position);
                    if( lastPosition.isPresent() && lastPosition.get() == position ){
                        return;
                    }
                    lastPosition = Optional.of(position);
                    listener.onItemSelected(position);
                }

                @Override
                public void onNothingSelected(AdapterView<?> parent) {

                }
            });
        }
        spinner.setAdapter(adapter);

        if( spinner.getSelectedItemPosition() != index ) {
            spinner.setSelection(index);
        }

    }

    public static class SimpleArrayAdapter<T> extends ArrayAdapter<T>{
        private final List<T> list;
        public SimpleArrayAdapter(Context context, @LayoutRes int resource,
                                  List<T> objects){
            super(context,resource,objects);
            this.list = objects;
        }
    }
}

略过中间过程了(很多坑),直接上“成果”了

还是靠我们神奇的BindingAdapter来实现
@BindingAdapter标记的函数解析

  • itemsSource:用来绑定来自ViewModel的数据集

  • onItemSelected:用来绑定下拉框切换的事件绑定

  • selectedIndex:用来绑定已选中的下标值,能让整个页面“记住”

  • 为什么要扩展ArrayAdapter写个自定义类?
    是为了能持有绑定的list对象,如果对象的引用一致则不需要初始化Adapter

  • 为啥有个Optional lastPosition?
    因为有个小bug,设置selection后会触发Listener两次,一时解决不了,只能加一个判断过滤一下

<FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".fragments.SpaceFragment">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="32sp"
            android:text="@string/space" />

        <Spinner
            android:id="@+id/spinnerTest"
            app:itemsSource="@{dataContext.buildings}"
            app:selectedIndex="@{dataContext.buildingIndex}"
            app:onItemSelected="@{(item)->dataContext.onSelected(item)}"
            android:layout_marginTop="300dp"
            android:layout_width="150dp"
            android:layout_height="80dp" />
    </FrameLayout>

View部分的定义,不解释了,很清楚了

@HiltViewModel
public class SpaceViewModel extends ObservableViewModel {
    private final List<String> buildings = Arrays.asList( "宝石大楼","25号楼" );
    private int buildingIndex;

    @Inject
    public SpaceViewModel( @NonNull Application application ) {
        super(application);
    }

    @Bindable
    public List<String> getBuildings() {
        return buildings;
    }

    @Bindable
    public int getBuildingIndex() {
        return buildingIndex;
    }

    public void onSelected(int position ){
        Logger.i("SpaceViewModel get select:%s",position);
        if( buildingIndex != position ){
            buildingIndex = position;
        }

    }
}

ViewModel部分源码

  • 总结

这次基本完成了Spinner的简版 MVVM实现。
当然未来还可以拓展

  • 复杂数据类型(非String)
  • 自定义item的layout
    基本上是RecyclerView兄弟,Adapter用法有神似的地方,比RecyclerView用起来简单。

还有一些教训

  • BindingAdapter requireAll = false一定要加,否则死得很难看
  • 绑定的listener要用接口来抽象,不能用实体类
  • {"A", "B", "C"},不是{"A,B,C"},一定要看清楚

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
安卓 安卓
Projects
None yet
Development

No branches or pull requests

1 participant