项目需求讨论-APP中提交信息及编辑信息界面及功能

好久好久没写文章了,这次我们来讨论下一些具有填写很多资料的界面,或者详情编辑界面等如何做起来更方便。
(PS:我写的可能不好,希望大家不好喷,哈哈,可以留言)

内容包括:自定义ViewDatabinding及与自定义View的双向绑定图片及文字上传


自定义View

我们来看下一个一般的提交的界面会长什么样(我写的很简单,就写了四项,有时候七八项或者更多很正常):
如图所示:

布局:

  1. 箭头的问题:

    一般来是<左边标题>,然后<中间内容><右边箭头>。前二个没问题,没有箭头显示,我们点击中间的时候,直接跳出来系统键盘,直接输入文字内容。而箭头一般用在什么地方呢,提示我们这一项是用来跳转或者点击弹出选择框等,比如我们的日期点击是这样的:

    所以在不同的item中控制显示是否有箭头
    上面这个布局很简单,我们可以这么写:
<LinearLayout>
    <TextView/>
    <EditText />
    <ImageView/>
</LinearLayout>

然后每一项都这么写一个LinearLayout,然后如果没有<右边的箭头>,我们可以把<ImageView>去掉,但明显很不方便。

  1. 文字颜色的问题:
    我们发现<左边标题>的字体颜色肯定不会发生变化,但是<中间内容>的字体颜色会发生改变,当没内容,显示的是默认的提示语,颜色是一种美工规定好的浅色,然后当我们通过键盘手动输入内容,或者是时间选择框选择了时间,我们的<中间内容>的颜色就会变成美工规定好的深色。给人一眼看过去就知道已经填好了内容。

  2. <中间内容>的点击事件:
    我们知道如果是输入内容的,我们应该是点击后出来系统键盘,然后我们打字输入,但如果是点击出来类似日期选择器,我们就不应该是出来键盘,而是出来选择框,然后选择了之后,<中间内容>变成我们选择好的内容。

如果我们的项目中也有这种类似的item,而且有很多,单纯的LinearLayout分别包裹很多个TextView 和ImageView就很麻烦。主要还是layout的代码会显得很多很多,很臃肿,维护起来也不清晰。


一般我们都会用自定义View来处理,市面上有很多这类类似的第三方:SuperTextView , LSettingView

但是也有问题:

  1. 比如SuperTextView 因为设计的时候是面向很多开发者,所以很多功能都要考虑进去,但是我们实际开发时候并不需要这么强大的功能。我引入这个,实际上有点大材小用,而且扩展不好,毕竟如果有点定制化需求,你要去改别人的代码。
  2. 他们一般都是用于固定内容的显示,而不是中间是可输入的内容。
  3. 很多人就是直接引入,直接使用,而不是去看如何实现的。

所以我也根据我的项目封装了一个,可能扩展性并不符合各个项目,但是功能简单,代码少,只需要符合我的项目即可:

我们既然要中间可以输入,我们中间就统一放入EditText。我们就取名为LEditTextItem:

ledit_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusable="true"
    android:focusableInTouchMode="true"
    android:background="#FFFFFF"
    android:gravity="center_vertical"
    >

    <TextView
        android:id="@+id/tv_lefttext"
        android:layout_width="85dp"
        android:layout_height="match_parent"
        android:layout_marginLeft="6dp"
        android:gravity="center_vertical"
        android:padding="10dp"
        android:textSize="15sp"
        tools:text="标题标题" />

    <EditText
        android:id="@+id/et_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_toLeftOf="@+id/iv_righticon"
        android:layout_toRightOf="@id/tv_lefttext"
        android:background="@null"
        android:focusable="false"
        android:gravity="center_vertical"
        android:padding="10dp"
        android:textColor="#222"
        android:textColorHint="#666"
        android:textSize="15sp"
        tools:text="内容"
        />


    <ImageView
        android:id="@+id/iv_righticon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_gravity="center"
        android:layout_marginRight="6dp"
        android:src="@drawable/righticon" />

    <View
        android:id="@+id/underline"
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:layout_alignParentBottom="true"
        android:background="#99999999" />
</RelativeLayout>

LEditTextItem.java:

public class LEditTextItem extends RelativeLayout{


    private Context mContext;
    private ImageView ivIcon;
    private TextView tvTitle;
    private EditText etContent;
    private LEditOnclickListener listener;
    private boolean isedit;


    public LEditTextItem(Context context) {
        super(context);
        mContext = context;
        initView();
    }

    public LEditTextItem(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initView();

        float defaultTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, mContext.getResources().getDisplayMetrics());
        TypedArray array = mContext.obtainStyledAttributes(attrs, R.styleable.LEditTextItem);
        String title = array.getString(R.styleable.LEditTextItem_title);
        float titleSize = array.getDimension(R.styleable.LEditTextItem_title_size, defaultTextSize);
        int titleColor = array.getColor(R.styleable.LEditTextItem_title_color, Color.parseColor("#666666"));
        String content = array.getString(R.styleable.LEditTextItem_content);
        float contentSize = array.getDimension(R.styleable.LEditTextItem_content_size, defaultTextSize);
        int contentColor = array.getColor(R.styleable.LEditTextItem_content_color, Color.parseColor("#222222"));
        String hintContent = array.getString(R.styleable.LEditTextItem_hintcontent);
        int hintColor = array.getColor(R.styleable.LEditTextItem_hintcontent_color, Color.parseColor("#999999"));
        boolean showIcon = array.getBoolean(R.styleable.LEditTextItem_showicon, true);
        boolean isEditable = array.getBoolean(R.styleable.LEditTextItem_isedit, false);

        ivIcon.setVisibility(showIcon ? View.VISIBLE : View.GONE);

        tvTitle.setText(title);
        tvTitle.setTextColor(titleColor);
        tvTitle.getPaint().setTextSize(titleSize);

        etContent.setText(content);
        etContent.getPaint().setTextSize(contentSize);
        etContent.setTextColor(contentColor);
        etContent.setHint(hintContent);
        etContent.setHintTextColor(hintColor);

        etContent.setFocusableInTouchMode(isEditable);
        etContent.setLongClickable(false);

        etContent.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (listener != null) {
                    listener.onClick();
                }
            }
        });

    
    }

    public void initView() {
        View view = LayoutInflater.from(mContext).inflate(R.layout.ledit_layout, this);
        ivIcon = (ImageView) view.findViewById(R.id.iv_righticon);
        tvTitle = (TextView) view.findViewById(R.id.tv_lefttext);
        etContent = (EditText) view.findViewById(R.id.et_content);
    }

    public void setIconVisible(int visible) {
        ivIcon.setVisibility(visible);
    }

    public void setContentStr(String content) {
        etContent.setText(content);
    }


    public String getContentStr() {
        return etContent.getText().toString().trim();
    }


    public EditText getContentEt() {
        return etContent;
    }


    public void setIsedit(boolean isedit) {
        this.isedit = isedit;
        etContent.setFocusableInTouchMode(isedit);
    }

    public interface LEditOnclickListener {
        void onClick();
    }

    public void setLEditOnClickListener(LEditOnclickListener listener) {
        this.listener = listener;
        etContent.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (LEditTextItem.this.listener != null) {
                    LEditTextItem.this.listener.onClick();
                }
            }
        });
    }
}

其实就是最简单的根据传入不同的属性值,来控制我们的这个item是否显示<右边箭头>,<中间内容>是直接输入还是点击事件,<左边标题>和<中间内容>的字体颜色、字体大小等。

这里我们注意到了。我们的自定义View直接继承了RelativeLayout,然后往这个里面添加了我们的layout,然后我们的layout里面也是用了Relativelayout,所以我们可以看到结构:


其实我们可以直接继承RelativeLayout后可以添加一个个添加TextView ,ImageView等,这样可以少掉一层。我这二层这么干就是为了省事。哈哈。

所以再回头看看我们的上面实现的布局只需要:

<LEditTextItem
        android:layout_width="match_parent"
        android:layout_height="@dimen/item_height"
        app:hintcontent="请选择XXX"
        app:isedit="true"
        app:showicon="false"
        app:title="XX标题" />
        
<LEditTextItem
        android:layout_width="match_parent"
        android:layout_height="@dimen/item_height"
        app:hintcontent="请选择日期"
        app:isedit="false"
        app:title="日期" />        

我们用app:isedit来控制这个中间的EditText是直接输入文字,还是执行点击事件:

xxxx.setLEditOnClickListener(new LEditTextItem.LEditOnclickListener() {
    @Override
    public void onClick() {
        //弹出日期选择框     
    }
});

PS:我用的选择框库:Bigkoo/Android-PickerView


Databinding

我们知道一般我们会有这二种情况(我以一个用户基本信息界面为例):

  1. 第一次进入,该界面的内容都是空的,然后我们该键盘输入的输入,该选择框选择的就选择,然后有相应的内容,然后再上传提交
  2. 后来进入,先加载以前已经上传过的基本信息,然后再针对性的去更改其中某一项,然后保存上传,去更新。

其实二种情况可以合为一种:进来的时候加载该用户的基本信息,然后更改后保存。(第一次添加可以理解为进来加载的用户基本信息为空内容即可)

然后你们可能就要大段大段的这样处理:

EditText et = (EditText)findViewById(R.id.xx);
//获取用户基本信息
PersonInfo bean = .......
et.setText(bean.xxxx);

//按了保存按钮后
String newStr = et.getText().toString();
//上传新的newStr内容

如果有十项,你就要写10个findViewById(),10个setText(),10个getText();

这时候当然需要我们的神器:Databinding
基本知识我就不介绍了,网上很多写的很好的文章:
DataBinding使用教程(一):配置与基本使用
DataBinding使用教程(二):xml标签详解
DataBinding使用教程(三):各个注解详解
DataBinding使用教程(四):BaseObservable与双向绑定

我们如果使用了双向绑定,那我们当通过结果获取到了PersonInfo的bean对象后,不需要专门的每个去setText,我们只需要在我们的自定义控件处添加app:content即可,当然这时候因为是注入值,而且因为是自定义属性的缘故,我们要在代码中写上:

<layout>
<data>
    <variable
        name="bean"
        type="com.xxxx.PersonInfo" />

    <variable
        name="isedit"
        type="Boolean" />
</data>

<com.leon.lib.settingview.LEditTextItem
            android:id="@+id/le_cost_edit"
            android:layout_width="match_parent"
            android:layout_height="@dimen/item_height"
            app:content="@={bean.name}"
            app:hintcontent="请输入YYY"
            app:isedit="@{isedit}"
            app:showicon="false"
            app:title="YY标题" />

</layout>

所以那我们就不需要去setText,也不需要重新去getText。
当内容发生变化后,我们代码中的bean对象的这个属性的值也会相应的发生变化。我们最后只要提交的时候直接用我们代码中的PersonInfo的bean对象即可,不需要重新去getText内容。

普通的EditText当然很方便,只需要:
android:text = "@={bean.xxx}"
但是我们这里是自定义View,所以我们要实现双向绑定,我们就要自己写方法来实现:

具体可以再参考别人的优秀文章:DataBinding双向绑定

LEditTextItem.java:

public class LEditTextItem extends RelativeLayout implements TextWatcher {


    private Context mContext;
    private ImageView ivIcon;
    private TextView tvTitle;
    private EditText etContent;
    private LEditOnclickListener listener;
    private boolean isedit;


    public LEditTextItem(Context context) {
        super(context);
        mContext = context;
        initView();
    }

    public LEditTextItem(Context context, AttributeSet attrs) {
        .....
        .....
        .....
        
        etContent.addTextChangedListener(this);
    }

    public void initView() {
        .....
        ....
        .....
    }

    public void setIconVisible(int visible) {
        ivIcon.setVisibility(visible);
    }

    public void setContentStr(String content) {
        etContent.setText(content);
    }


    public String getContentStr() {
        return etContent.getText().toString().trim();
    }


    public EditText getContentEt() {
        return etContent;
    }


    public void setIsedit(boolean isedit) {
        this.isedit = isedit;
        etContent.setFocusableInTouchMode(isedit);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    @Override
    public void afterTextChanged(Editable s) {
        if (onTextChangeListener != null) {
            onTextChangeListener.onchange();
        }
    }

    public interface LEditOnclickListener {
        void onClick();
    }

    public void setLEditOnClickListener(LEditOnclickListener listener) {
        this.listener = listener;
        etContent.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (LEditTextItem.this.listener != null) {
                    LEditTextItem.this.listener.onClick();
                }
            }
        });
    }

    @BindingAdapter(value = "content")
    public static void setContent(LEditTextItem view, String content) {
        String oldContent = view.getContentStr();

        if (TextUtils.equals(content, oldContent)) {
            return;
        }
        view.getContentEt().setText(content);

    }

    @InverseBindingAdapter(attribute = "content", event = "contentAttrChanged")
    public static String getContent(LEditTextItem view) {
        return view.getContentEt().getText().toString().trim();
    }

    @BindingAdapter(value = "contentAttrChanged")
    public static void setChangeListener(LEditTextItem view, InverseBindingListener listener) {
        if (listener != null) {
            view.setOnTextChangeListener(listener::onChange);
        }
    }

    OnTextChangeListener onTextChangeListener;

    public void setOnTextChangeListener(OnTextChangeListener listener) {
        onTextChangeListener = listener;
    }

    interface OnTextChangeListener {
        void onchange();
    }
}

这样我们只需要:

//服务器获取信息
PersonInfo bean = xxxxxxx;
binding.setBean(bean);

//提交服务器
post(bean)

图片及文字上传:

我们一般这种界面不仅会上传文字,可能还要上传图片,比如一个个人信息页,说不定还要上传一些照片和图片信息。

现在比较流行跟微信朋友圈的图片差不多。我也是偷懒用的第三方的库:

Android 图片选择、预览、九宫格图片控件、拖拽排序九宫格图片控件

PS:关于这个库,有二个地方要注意:

  1. 我们默认的是新增用户信息,所以显示的是本地的图片,但有时候我们是要编辑用户信息,那进来的时候就要先用这个库加载服务器上的图片,这个第三方也是可以的。然后我们可以再去删除老的图片,然后新增新的图片,再上传,具体方法我就不说了,有需要的可以询问我。
  2. 默认加载的图片是本地图片没问题,但是如果是加载网络图片,或者是服务器上指定的图片,如果是要有权限才能查看的图片,就要修改这个库中加载图片的源码,因为我配合的是Glide,所以我在这里要对这个Glide附上Cookie,才能正确显示网络图片,不然加载不出来。可以参考Glide加载需要权限验证的图片Url,如果也要用到这个库的,可以询问我怎么处理。

最后我们就到了上传照片的一步了。其实上传照片不难:
我们通过上面的第三方控件,可以获取到所有图片的路径,然后我们先把图片压缩都压缩,再去上传到服务器,不然现在的图片都太大了,会很慢及浪费客户时间:

//图片压缩并上传
ArrayList<File> outFilesList = new ArrayList<>(files.length);
final File[] outFiles = new File[files.length];
File[] finalFiles = files;
Observable.fromArray(files)
        .doOnSubscribe(disposable -> showDialog(AnnotationConfig.DialogType.LOAD_STATUS, "压缩图片中.."))
        .subscribeOn(AndroidSchedulers.mainThread())
        .subscribeOn(Schedulers.io())
        .observeOn(Schedulers.io())
        .subscribe(new Observer<File>() {
            @Override
            public void onSubscribe(Disposable d) {

            }

            @Override
            public void onNext(File file) {
                outFilesList.add(BitmapUtil.compressBitmap(aty, file));
            }

            @Override
            public void onError(Throwable e) {
                runOnUiThread(() -> {
                    closeDialog();
                    showDialog(ERROR_STATUS, "上传图片压缩失败,请重试");
                });
            }

            @Override
            public void onComplete() {
                runOnUiThread(() -> closeDialog());
                for (int i = 0; i < finalFiles.length; i++) {
                    outFiles[i] = outFilesList.get(i);
                }
                runOnUiThread(() -> {
                
                    //我们在这里再去进行相关的上传操作,infoMap是相关的文字内容信息
                    presenter.saveCostInfo(infoMap, outFiles);
                });

        }
});

压缩好的图片进行上传(同时还有文字内容信息):


List<MultipartBody.Part> photos = new ArrayList<>();
    if (files != null && files.length > 0) {
        for (int i = 0; i < files.length; i++) {
            RequestBody requestFile = RxPartMapUtils.toRequestBodyOfImage(files[i]);
            MultipartBody.Part MultipartFile =
                    MultipartBody.Part.createFormData("picture" + i, files[i].getName(), requestFile);
            photos.add(MultipartFile);
    }
}

api.saveCostDetailInfo(bean, photos).compose(Transformer.switchSchedulers());

API.java:

@Multipart
@POST(UrlConfig.COST_INFO_SAVE_URL)
Observable<HttpResult<Boolean>> saveCostDetailInfo(@PartMap() Map<String, RequestBody> info, @Part() List<MultipartBody.Part> parts);

就这样,文字内容和图片我们都上传成功了。我可能写的很简略,有问题我可以后面修改仔细点。哈哈。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,050评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,090评论 4 62
  • 上一章 订票前后 全章目录 电影已剧终,场内突然光亮起来。 人们摘下3D眼镜,纷纷向场外散去。依依仍然安静地坐在自...
    小豆利子阅读 257评论 0 5
  • 20170521 我很丰盛,因为 1.尽管今天早上闹钟响时,我仍然很困很累,不想睁眼,还是坚持起床去上课 2.尽管...
    喵喵A阅读 145评论 2 2
  • “字节”是byte,“位”是bit 1个字节为8位 在java中默认(默认表示不分编码方式): char 类型 2...
    大梦想家程序员阅读 657评论 0 1