soapgu/PlayPen

一周安卓开发问题汇总总结(一)

Opened this issue · 0 comments

  • 前言

最近忙着开发,对博客的输出暂缓,但是开发中碰到的问题,还需要有个地方随时记录,随时总结经验。主要还是UI层面为主为了下次开发更加顺畅。

  • 暗黑模式问题

安卓项目在建立的时候会默认生成一个themes.xml的主题配置
还有一个themes.xml是在value-night文件夹里面,对应的qualifier为night。

这个配置对应我们手机上的深色模式,有的手机的神色模式是随时间的变化配置的。问题我们项目的设计暂时只设计一套UI,所以深色模式下显示不同的UI显得毕竟奇怪。
最一劳永逸的解决方式

  1. 修改themes的parent
    把原来的Theme.MaterialComponents.DayNight 改成. NoActionBar Theme.MaterialComponents.Light.NoActionBar

  2. 删除 value-night下themes.xml
    这里要注意,斩草要除根。除了删除app下的themes.xml,还要检查所有module下的themes,因为安卓资源的打包方式是merge,所以我就出现了只删除app下的night的themes.xml而暗黑模式还存在造成怀疑人生。

  • BottomSheetDialog增加圆形导角问题

学会了BottomSheetDialog的基本用法,但是在xml加了Card的圆角也并不能显示导角,还是四四方方的弹框。
圆角并不能在BottomSheetDialog的内容中设置
需要从AppTheme入手

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@android:color/white"/>
    <corners android:topLeftRadius="16dp"
        android:topRightRadius="16dp"/>
</shape>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">       
    <item name="bottomSheetDialogTheme">@style/AppBottomSheetDialogTheme</item>
</style>

<style name="AppBottomSheetDialogTheme"
    parent="Theme.Design.Light.BottomSheetDialog">
    <item name="bottomSheetStyle">@style/AppModalStyle</item>
</style>

<style name="AppModalStyle"
    parent="Widget.Design.BottomSheet.Modal">
    <item name="android:background">@drawable/rounded_dialog</item>
</style>

篇幅太大,另外展开

解决方案简单粗暴 android:padding="0dp"
但是一下子就是找不到找不到!

  • Spinner的高度设置问题

不知道安卓的UI团队是怎么想的
可以通过android:dropDownWidth来设置宽度,但是没有设置高度的地方,似乎高度就一定和内容项绑定
StackOverflow上的高赞回答是用反射来强行设置,似乎高版本已经不起作用了,暂时就是“无解”

Spinner控件实在是不熟,目前解决方式就是高度自助话处理。直接override

public static class SimpleArrayAdapter<T> extends ArrayAdapter<T>{
        private final List<T> list;
        private final int tvResource;
        private int selectedPosition;
        private final int iconResource;
        private final int icon;

        public SimpleArrayAdapter(Context context, @LayoutRes int resource,@IdRes int tvResource,@IdRes int iconResource,@DrawableRes int icon,
                                  List<T> objects){
            super(context,resource, tvResource ,objects);
            this.list = objects;
            this.tvResource = tvResource;
            this.iconResource = iconResource;
            this.icon = icon;
        }

        public void setSelectedPosition(int selectedPosition) {
            this.selectedPosition = selectedPosition;
        }

        @Override
        public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
            View view = super.getDropDownView(position, convertView, parent);
            if( tvResource > 0 ) {
                TextView textView = view.findViewById(tvResource);
                textView.setTextColor( selectedPosition == position ? Color.BLUE : Color.BLACK );
            }
            return view;
        }

        @NonNull
        @Override
        public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
            View view = super.getView(position, convertView, parent);
            if( iconResource > 0 && icon > 0 ){
                ImageView imageView = view.findViewById(iconResource);
                imageView.setImageResource(icon);
            }
            return view;
        }
    }

通过getDropDownView的重载来设置文字的背景色

 @SuppressLint("ResourceType")
    @BindingAdapter(value = {"itemsSource","onItemSelected","selectedIndex","resource","tvResource","dropdownResource","iconResource","icon"},requireAll = false)
    public static <T> void setSpinnerItems(Spinner spinner , List<T> itemsSource, final OnMyItemSelectedListener listener , int index, @LayoutRes int resource, @IdRes int tvResource, @LayoutRes int dropdownResource , @IdRes int iconResource, @DrawableRes int icon){
        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 {
            int viewResource = resource > 0 ? resource : android.R.layout.simple_spinner_item;
            adapter = new SimpleArrayAdapter<>(spinner.getContext(), viewResource, tvResource, iconResource , icon , itemsSource);
            adapter.setDropDownViewResource( dropdownResource > 0 ? dropdownResource : android.R.layout.simple_spinner_dropdown_item);
            if( listener != null ) {
                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);
                        adapter.setSelectedPosition( position );
                        listener.onItemSelected(position);
                    }

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

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

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

    }

这是我Spinner 的绑定适配器,配合一起使用的,由于当时觉得不是太复杂,走的思路不是MVVM。现在属性多了有点惨不忍睹。日后一定是要重构的,只是完成现阶段需求。

  • Material Design控件应用简介

安卓的界面基础是从官网进去 界面和导航
不过还不能完全符合需求
material控件库基本上算是谷歌的官方库了。
只要依赖声明一下就行很方便
implementation 'com.google.android.material:material:1.4.0'
具体用法参考
material控件库
图片
Android是官方支持,而且api文档用例齐全,是安卓开发的常用网站

  • 适配多种手机分辨率的弹性布局方案

  • 从专用设备到通用移动设备转型的阵痛
    以前开发的专用屏,现在开发安卓手机端通用app。第一个变化是没法root了。当然没有啥“变态”功能还能忍。
    最大的变化是不能把界面“做死”
    不同的手机分辨率是由细微差别的。以前做专用设备,统一的屏幕统一的分辨率。控件布局啥的“焊死”得了,一点问题都没有。现在做手机的应用是老办法就不行了。控件重叠,间距难看等问题都来了。

  • 思路转变
    控件不能写死dp,而是适配的方式,比如宽度“顶住”左右控件但是不设置大小,只固定边距,宽度直接和屏幕大小适配
    图片
    如图constraint-layout支持控件平均分配以及按权重分配
    图片
    如图6个输入框是按屏幕宽度平均“瓜分”的,先把中间的分割线以及固定边距留下了剩下“平分”

  • “随心”的控制文本框的焦点

如果这些验证码都要一个一个自己切实在太麻烦了,要怎么让他自然切换那?
好在TextInputEditText有afterTextChanged事件,通过binding库的配合实现

<com.google.android.material.textfield.TextInputEditText
               android:id="@+id/code_text_input_1"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:afterTextChanged="@{()->dataContext.inputCode( dataContext.code1 , codeTextInput2 )}"
               android:inputType="number"
               android:maxLength="1"
               android:padding="0dp"
               android:text="@={dataContext.code1}"
               android:textSize="18sp" />
       </com.google.android.material.textfield.TextInputLayout>
public void inputCode( String current , View next  ){
        //MessageHelper.ShowToast( this.getApplication(), "text changed:" + current );
        if(! StringUtils.isTrimEmpty( current )  ){
            next.requestFocus();
        }
    }

这样就能实现输入焦点自动切换了。当然还有一些细节并没有做到完美,比如最后一个输入框应该输入后自动隐藏输入法等。慢慢完善吧。

  • Navigation中模态Fragment和普通Fragment的通信问题

  • Navigation的单向性的问题
    图片
    如图fragment->dialog是OK的,dialog->fragment导航图可以画出来,运行给你分分钟报错!!!
    图片
    冷静一下也合理,fragment和dialog是不平等的,其实我们平时写dialog也是fragment调用的,就是dialog是fragment的下级,下级是无法转为“上级”的。
    那fragment怎么拿到dialog的结果
    1 viewModel共享
    这个前面博客有介绍,但是代价有点大。viewModel的生命周期延长了。一级页面还能接受(本来就是堆栈缓存的),二三级页面就有点不可接受了

2 FragmentResultListener
可以实现

private void closeSelf( String url ){
        this.dismiss();
        Bundle result = new Bundle();
        result.putString("url", url);
        getParentFragmentManager().setFragmentResult("requestKanbanUrl", result);
    }

这是dialog的实现
getParentFragmentManager().setFragmentResultListener("requestKanbanUrl", this, (requestKey, bundle) -> {
String url = bundle.getString("url");
Logger.i( "receive url: %s",url );
NavDirections action = KanbanFragmentDirections.actionKanbanFragmentToMobileWebFragment(url);
this.disposables.add(Single.timer( 100, TimeUnit.MILLISECONDS )
.observeOn(AndroidSchedulers.mainThread())
.subscribe( t-> NavHostFragment.findNavController(this).navigate(action) ) );
});
这是fragment的FragmentResultListener,
通过delay来进行跳转
因为FragmentResult获取在前,dialog关闭在后,跳转必须要在dialog关闭以后,所以不得不加了一把延时,确实有一点点奇葩。所以以后这种情况要考虑dialog是否要踢出导航了

  • gradle加速问题解决

当“翻墙”也不能挽救gradle下载的时候,就只能靠镜像了

repositories {
       maven { url 'https://maven.aliyun.com/repository/central' }
       maven { url 'https://maven.aliyun.com/repository/google' }
       google()
       mavenCentral()
   }

在project级别上的build.gradle加上两行maven。
速度飞快,很好

相关参考站点

先罗列再填坑