android之仿豆瓣写日志

先来看看某帮的效果图:所说的也是类似的效果图
这里写图片描述

这里写图片描述

图1是正常编辑文本以及插入图片时的状态图,图2是长按拖动图片位置的状态

难点剖析

  • 控件拖动:主要用gitHub上的开源控件DragListView 控件地址 这里不再做讲解
  • RecycleView中光标是如何定位在指定的控件
  • 如何解决部分机型识别不了键盘中的删除键、回车键
  • 如何将图片插入相应的位置
  • 如何实现并发上传图片

逻辑讲解

正常输入文本:

  1. 当按下回车键时,获取光标的位置;
    (1)光标在文本头部,则在文本之上添加一个EditText
    (2)光标在文本尾部,则在文本之后添加一个EditText
    (3)光标在文本之间,则将文本分成两段,并将后一段的文本内容赋值给新建的EditText
  2. 当按下删除键时,获取光标位置
    (1)光标在文本头部,则判断是前一个preItem是存在;如果此preItem不是图片,则将删除光标所在的 item,并且将内容追加在preItem中;如果preItem是图片,则弹出是否要删除图片
    (2)光标不再头部,则不做处理
  3. 当点击添加图片:
    <1>、定位光标(已有焦点): 则选完图之后,将图片插入到光标所在位置,规则同回车键;图片和图片之间要添加一个EditText,如果最后的一个插入的位置下一个nextItem正好EditText,则不需要增加EditText
    <2>、上一次已经定位光标并当前失去焦点(没有光标):则将定位到相应的位置,让图片插入到此item之后
    <3>、未曾获取过焦点或光标,比如进入页面就选图,则将图片插入到第一个item之后
  4. 当点击输入表情:
    <1>、未曾获取过光标:则将焦点默认选择在第一个item
  5. 点击空白处要获取焦点,弹出键盘,总之要判断当前点击的是空白处还是EditText;

<1>RecyclerView中光标是如何定位在指定的控件
不管是ListView或RecyclerView都有复用机制,导致定位光标可能会出现错乱的现象,再加上RecyclerView的NotifyXXX方法很是丰富,不会刷新所有的item,导致holder一直持有的旧的position,如果如果通过position作为item的唯一标识,那么在插入文本时会找不到要定位的EditText;所以一定要给item一个唯一标识,不管是刷新还是不刷新次id必须是唯一的且能映射到item;

先看代码如下:
public class EditViewHolder extends DragItemAdapter.ViewHolder<TopicPublishModelNew.TypeContent> {
    PubTopicEditText etEditorTopic;
    PublishTopicAdapter mItemAdapter;

    //由于使用holderView缓存pos和view,当删除item时,不可见的view,不会执行onBindViewHolder,
    //所以无法删除或换行(pos = 5 删除一项后,还是5,导致pos>数据源的size,会数组越界)
    // 于是用getItemId(每个item都是唯一值)查找现在item的pos。。。。
//        int itemPos = -1;//不可使用,不实时调用onBindViewHolder
    public EditViewHolder(final View itemView, PublishTopicAdapter itemAdapter) {
        super(itemView, itemAdapter.getGrabHandleId(), itemAdapter.getDragOnLongPress());
        this.mItemAdapter = itemAdapter;
        etEditorTopic = (PubTopicEditText) itemView.findViewById(R.id.et_editor_topic);
        setEditTextListener();
    }

    private void setEditTextListener() {
        etEditorTopic.setTextWatchListener(new PubTopicEditText.TextWatchListener() {
            @Override
            public void clickEnterKey(View v, CharSequence splitEnterBefore, CharSequence splitEnterAfter) {
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);
                if (isLegalPosition(itemPos)) {
                    mItemAdapter.getItem(itemPos).info.content = splitEnterBefore;
                    etEditorTopic.setText(splitEnterBefore);
                    long uniqueId = mItemAdapter.getItemUniqueId();
                    mItemAdapter.getCursorItem().setEtFocusId(uniqueId);//让焦点换行
                    mItemAdapter.getCursorItem().setInsertImgParams(0, splitEnterAfter);//回车后,光标在首位
                    mItemAdapter.getCursorItem().setCursorEditeText(null);
                    mItemAdapter.getCursorItem().setCurCursorIndex(0);//换行后,光标应该显示的位置
                    mItemAdapter.addItem(itemPos + 1, 
                    TopicPublishModelNew.TypeContent.createTextInstance(uniqueId, splitEnterAfter));
                    mItemAdapter.notifyItemRangeChanged(itemPos, 2);
                    mItemAdapter.setPressEnterKey(true);
                    etEditorTopic.clearFocus();//先清除焦点,否则会偶现获取两次焦点,直接定位到第3行的情况
                    final int scrollPos = itemPos + 1;
                    PublishTopicAdapter.ScrollPosListener scrollPosListener = mItemAdapter.getScrollPosListener();
                    if (null != scrollPosListener) {//换行后,滚动到RecycleView到相应的位置
                        scrollPosListener.onScrollToPos(scrollPos);
                    }
                }
            }
            //将string改成CharSequence之后,content和这个是一个对象,字符长度是一样的
            @Override
            public void clickDelKey(View v, CharSequence text, int delCount) {
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);
                if (isLegalPosition(itemPos)) {
                    TopicPublishModelNew.PublishContent item = mItemAdapter.getItem(itemPos).info;
                    mItemAdapter.delCharsCount(delCount);
                    item.content = text;
                }
            }

            @Override
            public void delEnterChar(View v, CharSequence text) {
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);//
                //首行不需要删除或者文本行数至少有一行
                if (itemPos > 0 &&
                 itemPos < mItemAdapter.getItemCount() && 
                 TopicPublishModelNew.TypeContent.edtiTextCount > 1) {
                    int prevItem = itemPos - 1;
                    TopicPublishModelNew.TypeContent mTypeItem = mItemAdapter.getItem(prevItem);
                    if (TopicPublishModelNew.TYPE_TEXT.equals(mTypeItem.type)) {//删除文本
                        int size = mTypeItem.info.content.length();
                        CharSequence srcText = mItemAdapter.getItem(prevItem).info.content;
                        mItemAdapter.getItem(prevItem).info.content =
                                new SpannableStringBuilder(srcText).append(text);
                        mItemAdapter.getCursorItem().setCurCursorIndex(size);//删除换行后,光标应该显示的位置
                        mItemAdapter.getCursorItem().setEtFocusId(mTypeItem.position);
                        mItemAdapter.setPressDelEnterKey(true);
                        mItemAdapter.notifyItemChanged(prevItem);
                        mItemAdapter.removeItem(itemPos);
                        TopicPublishModelNew.TypeContent.edtiTextCount--;
                    } else {//删除图片
                        mItemAdapter.showDeleteImgDialog(prevItem);
                    }
                }
            }

            @Override
            public void onTextChanged(CharSequence text, int start, int before, int addCount) {
            //将string改成CharSequence之后,content和这个是一个对象,字符长度是一样的
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);//
                if (isLegalPosition(itemPos)) {
                    TopicPublishModelNew.TypeContent item = mItemAdapter.getItem(itemPos);
                    TopicPublishModelNew.PublishContent info = item.info;
                    if (null != info) {
                        if (text == info.content) {//是同一个对象,不需要重新赋值
                            //添加字符的个数,也可能是删除负数,选中点击删除键就是负值
                            mItemAdapter.addCharsCount(addCount);

                        } else if (null == info.content || 
                        !text.toString().equals(info.content.toString())) {
                            //不是同一个对象,需要重新赋值,且要计算增加个数
                            //添加字符的个数,也可能是删除负数,选中点击删除键就是负值
                            mItemAdapter.addCharsCount(addCount);
                            //防止:列表来回滑动,会重新赋值不再是同一个对象,不需要统计字数
                            //将string改成CharSequence之后,content和这个是一个对象,字符长度是一样的,这个赋值可以不用
                            item.info.content = text;
                        }
                    }
                }
            }

        });
        etEditorTopic.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {//某个et获取焦点
                    //正常手指触摸获取焦点
                    if (!mItemAdapter.isPressEnterKey() && 
                    !mItemAdapter.isPressDelEnterKey()) {
                        mItemAdapter.getCursorItem().setEtFocusId(mItemId);
                        mItemAdapter.getCursorItem()
                        .setInsertImgParams(etEditorTopic.getSelectionStart()
                        ,etEditorTopic.getText().toString());
                        mItemAdapter.getCursorItem().setCursorEditeText(etEditorTopic);
                    }
                    mItemAdapter.setPressEnterKey(false);
                    mItemAdapter.setPressDelEnterKey(false);
                } else {//上个et失去焦点:顺序,上个et失去焦点,当前et获取焦点
                }
            }
        });
    }

    private boolean isLegalPosition(int itemPos) {
        return itemPos > -1 && itemPos < mItemAdapter.getItemCount();
    }

    @Override
    public void updateView(TopicPublishModelNew.TypeContent model, final int position) {
        TopicPublishModelNew.PublishContent content = model.info;
        if (null != content) {
            CharSequence text = content.content;
            //内容且图片为空才显示提示语
            if (0 == position && 
            mItemAdapter.getContentCount() == 0 &&  
            0 == mItemAdapter.getSelectedImgCount()) {
                    etEditorTopic.setHint("请输入正文");
            } else {
                etEditorTopic.setHint("");
            }
            etEditorTopic.setText(text);
            itemView.setTag(model);
            etEditorTopic.post(new Runnable() {
                @Override
                public void run() {
                    setEtLines(etEditorTopic);
                    //这个etTopic获取焦点
                    if (position == mItemAdapter.getCursorItem().getEtFocudPostion()) {
                        etEditorTopic.requestFocus();
                        mItemAdapter.getCursorItem().setCursorEditeText(etEditorTopic);
                        int curCursorIndex = mItemAdapter.getCursorItem().getCurCursorIndex();
                        if (-1 != curCursorIndex && curCursorIndex <= etEditorTopic.length()) {
                            etEditorTopic.setSelection(curCursorIndex);
                            mItemAdapter.getCursorItem().setCurCursorIndex(-1);
                        } else {
                            etEditorTopic.setSelection(etEditorTopic.length());
                        }
                    }
                }
            });

            //监听表情需要传入EditText
            mItemAdapter.setEmojiEditText(etEditorTopic);
        }
    }


    private void setEtLines(PubTopicEditText etEditorTopic) {
        if (mItemAdapter.isStartDraged()) {//将其缩放,拖动的时候
            if (etEditorTopic.getLineCount() > 3) {
                etEditorTopic.setMaxLines(3);
            }
            etEditorTopic.setPadding(20, 20, 20, 10);
            etEditorTopic.setBackgroundResource(R.drawable.publish_topic_item_bg_shape);
        } else {
            etEditorTopic.setPadding(4, 0, 0, 0);
            etEditorTopic.setMaxLines(Integer.MAX_VALUE);
            etEditorTopic.setBackgroundResource(0);
        }
    }


}

从上可知,主要代码就是setEditTextListener()这个方法里边的监听器,而最主要的逻辑是etEditorTopic.setTextWatchListener(new PubTopicEditText.TextWatchListener() {})
这个监听器的四个方法的实现;这个是是在他的父类里边实现的;这里我们先来说说简单的逻辑,之后再看看父类的主要实现;
先来看看当执行onBindViewHolder方法时,会调用updateView更新数据

public void updateView(TopicPublishModelNew.TypeContent model, final int position) {
        TopicPublishModelNew.PublishContent content = model.info;
        if (null != content) {
            CharSequence text = content.content;
            //内容且图片为空才显示提示语
            if (0 == position && mItemAdapter.getContentCount() == 0 && 0 == mItemAdapter.getSelectedImgCount()) {
                    etEditorTopic.setHint("请输入正文");
            } else {
                etEditorTopic.setHint("");
            }
            etEditorTopic.setText(text);
            itemView.setTag(model);
            etEditorTopic.post(new Runnable() {
                @Override
                public void run() {
                    setEtLines(etEditorTopic);
                    //这个etTopic获取焦点
                    if (position == mItemAdapter.getCursorItem().getEtFocudPostion()) {
                        etEditorTopic.requestFocus();
                        mItemAdapter.getCursorItem().setCursorEditeText(etEditorTopic);
                        int curCursorIndex = mItemAdapter.getCursorItem().getCurCursorIndex();
                        if (-1 != curCursorIndex && curCursorIndex <= etEditorTopic.length()) {
                            etEditorTopic.setSelection(curCursorIndex);
                            mItemAdapter.getCursorItem().setCurCursorIndex(-1);
                        } else {
                            etEditorTopic.setSelection(etEditorTopic.length());
                        }
                    }
                }
            });

            //监听表情需要传入EditText
            mItemAdapter.setEmojiEditText(etEditorTopic);
        }
    }

这里主要是更新数据:当插入文本、输入内容、插入图片等等时,更新内容
mItemAdapter.getCursorItem()这个主要是管理当前光标相关的;比如光标所在的位置、光标和item绑定的唯一id、通过id映射到相应的item的position等;
为什么使用post?post里边执行的又是什么逻辑?
其实:
1. setEtLines(etEditorTopic);要获取当前的EditText的行号,以及长按状态下要有固定的可拖动的item高和背景,而高是通过行数控制的,拖动情况下最大行数是3
2. 获取当前要获得焦点的EditText,通过
mItemAdapter.getCursorItem().getEtFocudPostion()获取要聚焦的EditText的当前position

再来看看获取焦点的监听器

etEditorTopic.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {//某个et获取焦点
                    //正常手指触摸获取焦点
                    if (!mItemAdapter.isPressEnterKey() && 
                            !mItemAdapter.isPressDelEnterKey()) {
                        mItemAdapter.getCursorItem().setEtFocusId(mItemId);
                        mItemAdapter.getCursorItem()
                                .setInsertImgParams(
                                        etEditorTopic.getSelectionStart(),
                                        etEditorTopic.getText().toString());
                        mItemAdapter.getCursorItem().setCursorEditeText(etEditorTopic);
                    }
                    mItemAdapter.setPressEnterKey(false);
                    mItemAdapter.setPressDelEnterKey(false);
                } else {//上个et失去焦点:顺序,上个et失去焦点,当前et获取焦点
                }
            }
        });

当获取或失去焦点时就会执行这个方法:而获取焦点有正常点击和updateView()中etEditorTopic.requestFocus();
此时要更新焦点所在的item的FoucsId也就是唯一id(mItemId);以及光标的位置和此Item的EditText实例,因为光标位置所在的item来回点击更换光标的位置不会实时更新,需要通过实例获取光标位置才是准确的;

再来看看那主要的四个方法:接口说明如下:

public interface TextWatchListener {
        /**
         * 点击系统回车键
         *
         * @param v
         * @param splitEnterAfter  按回车键后,字符分成两段,获取后一段字符串
         * @param splitEnterBefore 按回车键后,字符分成两段,获取前一段字符串
         */
        void clickEnterKey(View v, CharSequence splitEnterBefore, CharSequence splitEnterAfter);

        /***
         * 点击系统的删除键
         * @param v EditText
         * @param text 删除后剩余的字符
         *@param delCount 删除字数
         */
        void clickDelKey(View v, CharSequence text, int delCount);

        /**
         * 删除换行符 光标在头部然后点击删除键
         *
         * @param v
         * @param text 删除换行后,剩余的字符串
         */
        void delEnterChar(View v, CharSequence text);

        /****
         *
         * @param s
         * @param start
         * @param before
         * @param addCount 字符串增加个数
         */
        void onTextChanged(CharSequence s, int start, int before, int addCount);
    }

总结来说:四个方法一次只会回调一个
1. clickEnterKey():是指当点击系统回车键 时回调
2. clickDelKey(): 是指当点击系统的删除键时回调
3. delEnterChar():是指当删除换行符时回调:删除换行符也就是光标在头部然后点击删除键
4. onTextChanged():内容发生变化,当以上3个方法都不执行时就会回调这个方法

这四个方法一个个分析:

public void clickEnterKey(View v, CharSequence splitEnterBefore, CharSequence splitEnterAfter) {
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);
                if (isLegalPosition(itemPos)) {
                    mItemAdapter.getItem(itemPos).info.content = splitEnterBefore;
                    etEditorTopic.setText(splitEnterBefore);
                    long uniqueId = mItemAdapter.getItemUniqueId();
                    mItemAdapter.getCursorItem().setEtFocusId(uniqueId);//让焦点换行
                    mItemAdapter.getCursorItem().setInsertImgParams(0, splitEnterAfter);//回车后,光标在首位
                    mItemAdapter.getCursorItem().setCursorEditeText(null);
                    mItemAdapter.getCursorItem().setCurCursorIndex(0);//换行后,光标应该显示的位置
                    mItemAdapter.addItem(itemPos + 1, TopicPublishModelNew.TypeContent.createTextInstance(uniqueId, splitEnterAfter));
                    mItemAdapter.notifyItemRangeChanged(itemPos, 2);
                    mItemAdapter.setPressEnterKey(true);
                    etEditorTopic.clearFocus();//先清除焦点,否则会偶现获取两次焦点,直接定位到第3行的情况
                    final int scrollPos = itemPos + 1;
                    PublishTopicAdapter.ScrollPosListener scrollPosListener = mItemAdapter.getScrollPosListener();
                    if (null != scrollPosListener) {//换行后,滚动到RecycleView到相应的位置
                        scrollPosListener.onScrollToPos(scrollPos);
                    }
                }
            }

当点击回车键后,就是换行,则创建一个新的EditText的控件,将splitEnterAfter(光标之后的文本内容)赋值,将splitEnterBefore赋值给原来的控件;将焦点放到新创建的EditText,所以要更新mItemAdapter.getCursorItem()的一系列信息,如:
long uniqueId = mItemAdapter.getItemUniqueId();//这就是标识item的唯一id
mItemAdapter.getCursorItem().setEtFocusId(uniqueId);//让焦点换行
然后notifyxxxx()执行updateView,此时就会根据mItemId(也就是uniqueId )获取焦点的position,直接
etEditorTopic.requestFocus();流程同之前讲述的。
然后定位RecycleView到相应位置,显示光标;

//将string改成CharSequence之后,content和这个是一个对象,字符长度是一样的
            @Override
            public void clickDelKey(View v, CharSequence text, int delCount) {
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);
                if (isLegalPosition(itemPos)) {
                    TopicPublishModelNew.PublishContent item = mItemAdapter.getItem(itemPos).info;
                    mItemAdapter.delCharsCount(delCount);
                    item.content = text;
                }
            }

当点击删除键之后:主要是用于统计删除的字符个数以及重新赋值给数据源;
getPositionForItemId此方法是根据唯一值id映射item的position

public void delEnterChar(View v, CharSequence text) {
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);//
                if (itemPos > 0 && itemPos < mItemAdapter.getItemCount() && TopicPublishModelNew.TypeContent.edtiTextCount > 1) {//首行不需要删除或者文本行数至少有一行
                    int prevItem = itemPos - 1;
                    TopicPublishModelNew.TypeContent mTypeItem = mItemAdapter.getItem(prevItem);
                    if (TopicPublishModelNew.TYPE_TEXT.equals(mTypeItem.type)) {//删除文本
                        int size = mTypeItem.info.content.length();
                        CharSequence srcText = mItemAdapter.getItem(prevItem).info.content;
                        mItemAdapter.getItem(prevItem).info.content =
                                new SpannableStringBuilder(srcText).append(text);
                        mItemAdapter.getCursorItem().setCurCursorIndex(size);//删除换行后,光标应该显示的位置
                        mItemAdapter.getCursorItem().setEtFocusId(mTypeItem.position);
                        mItemAdapter.setPressDelEnterKey(true);
                        mItemAdapter.notifyItemChanged(prevItem);
                        mItemAdapter.removeItem(itemPos);
                        TopicPublishModelNew.TypeContent.edtiTextCount--;
                    } else {//删除图片
                        mItemAdapter.showDeleteImgDialog(prevItem);
                    }
                }
            }

当点击删除回车符,即:光标在首位,又点击了删除键,则表示想删除此item,将内容和上一行合并;
拿到当前内容,判断preItem是否是图片,若是图片,则弹出是否删除图片;若不是图片,则将preItem和当前item内容合并,删除当前EditText,更新CursorItem,统计EditText个数;最后notifyXXX();更新获取焦点

 public void onTextChanged(CharSequence text, int start, int before, int addCount) {//将string改成CharSequence之后,content和这个是一个对象,字符长度是一样的
                int itemPos = mItemAdapter.getPositionForItemId(mItemId);//
                if (isLegalPosition(itemPos)) {
                    TopicPublishModelNew.TypeContent item = mItemAdapter.getItem(itemPos);
                    TopicPublishModelNew.PublishContent info = item.info;
                    if (null != info) {
                        if (text == info.content) {//是同一个对象,不需要重新赋值
                            //添加字符的个数,也可能是删除负数,选中点击删除键就是负值
                            mItemAdapter.addCharsCount(addCount);

                        } else if (null == info.content || 
                                !text.toString().equals(info.content.toString())) {
                            //不是同一个对象,需要重新赋值,且要计算增加个数
                            //添加字符的个数,也可能是删除负数,选中点击删除键就是负值
                            mItemAdapter.addCharsCount(addCount);
                            //防止:列表来回滑动,会重新赋值不再是同一个对象,不需要统计字数
                            //将string改成CharSequence之后,content和这个是一个对象,字符长度是一样的,这个赋值可以不用
                            item.info.content = text;
                        }
                    }
                }
            }

用于统计内容输入的字符数、数据源更新内容;
这里就分析完了这个方法!!!!
这里注意的是:
由于数据源用的不是String而是CharSequence原因是表情每次都要去解析,很耗性能,而且很多时候解析不出来;关于表情的问题,这里不做讲解 ,之后在讨论,或者有需要的留言讨论;

接下来看看父类是如何实现这四个方法的:立刻上代码

public class PubTopicEditText extends EmojiEditText {
    private static final String TAG = "===111 PubTopicEditText";
    /***是否点击删除键*/
    private boolean isClickDelKey = false;
    /***是不是按了回车键*/
    private boolean isClickEnterKey = false;

    public PubTopicEditText(Context context) {
        this(context, null);
    }

    public PubTopicEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }


    public PubTopicEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initListener();
    }

    /***是否有键盘兼容性问题:华为plk-tl01或原生机型监听不到回车键,只能在TextWatch做处理*/
    private boolean hasKeyboardCompatibility = true;
    private TextWatcher textWatcher = null;

    private void initListener() {
        if (null == textWatcher) {
            Log.d(TAG, "==textWatcher===");
            textWatcher = new TextWatcher() {
                private int beforeLength;
                int selectionStart;

                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
//                Log.d(TAG,"beforeTextChanged s = " + s.toString() + " start = " + start + " count = " + count + " after = " + after);
                    selectionStart = getSelectionStart();
                    int selectionEnd = getSelectionEnd();
                    Log.d(TAG, "beforeTextChanged selectionStart = " + selectionStart + " selectionEnd= " + selectionEnd);
                    Log.d(TAG, s.length() + " beforeTextChanged s  = " + s.toString());
                    beforeLength = s.length();
                }

                @Override
                public void afterTextChanged(Editable s) {
//                Log.d(TAG, "afterTextChanged s = " + s.toString());
                }

                /**
                 * @param s      输入后的所有文本
                 * @param start  光标的位置
                 * @param before
                 * @param count  一次输入字数的个数
                 */
                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (null != textWatchListener) {
                        if (hasKeyboardCompatibility) {//华为有些机型或者原生机型,监听不到键盘的回车键
                            int addLength = s.length() - beforeLength;
                            if (addLength == 1 && s.length() >= selectionStart + addLength) {//换行符算一个字符
                                CharSequence addStr = s.subSequence(selectionStart, selectionStart + addLength);
                                if ("\n".equals(addStr.toString())) {//换行符。。算一个字符
                                    clickEnterKey(selectionStart);
                                    return;
                                }
                            }
                        }
                        if (isClickDelKey) {//删除键
                            isClickDelKey = false;
                            textWatchListener.clickDelKey(PubTopicEditText.this, s, beforeLength - s.length());
                        } else if (!isClickEnterKey) {//不是回车键 增加字符
                            textWatchListener.onTextChanged(s, start, before, s.length() - beforeLength);
                        }
                        isClickEnterKey = false;
                    }
                }
            };
            this.addTextChangedListener(textWatcher);
        }

        this.setOnKeyListener(new View.OnKeyListener() {//华为手机与原生手机等等监听不到键盘的回车键、删除键,只能通过\n来判断兼容
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                hasKeyboardCompatibility = false;
                if (event.getAction() == KeyEvent.ACTION_DOWN) {//要加action_dow否则会执行两次
                    if (keyCode == KeyEvent.KEYCODE_DEL) {
                        if (delKeyFlag == 2) {
                            return false;
                        }
                        delKeyFlag = 1;
                        delEnterChar();
//                        return true;//返回true,则不会删除EditText的文本内容
                    } else if (keyCode == KeyEvent.KEYCODE_ENTER) {//点击换行符
                        clickEnterKey(getSelectionStart());
                        return true;//返回true,则换行符不会输入到EditText,也就不会换行
                    }
                }
                return false;
            }
        });
    }

    /***
     * 点击回车键:换行,则新增一个控件
     * @param selectionStart
     */
    private void clickEnterKey(int selectionStart) {
        if (null != textWatchListener) {
//            Log.d(TAG,"clickEnterKey");
            CharSequence text = getText();
            CharSequence textBefore = "";
            CharSequence textAfter = "";
            if (!TextUtils.isEmpty(text)) {
                if (-1 != selectionStart) {
                    textBefore = text.subSequence(0, selectionStart);
                    textAfter = text.subSequence(selectionStart, text.length());
                    if (!TextUtils.isEmpty(textAfter) && textAfter.toString().startsWith("\n")) {
                        //要去掉换行符,,否则会多次循环执行onTextChange,导致android Cannot call this method while RecyclerView is computing a layout or scrolling
                        textAfter = textAfter.subSequence(1, textAfter.length());//过滤换行符
                        if (textAfter.toString().contains("\n")) {//一般不会包含,因为已经过滤掉了,这里防止有导致异常is computing a layout or scrolling
                            textAfter = textAfter.toString().replaceAll("\n", "");//使用这个可能会导致表情解析不出来,所以这里只是作为防止异常产生
                        }
                    }
                }
            }
            isClickEnterKey = true;
            textWatchListener.clickEnterKey(PubTopicEditText.this, textBefore, textAfter);
        }
    }

    /***光标在控件首位,再次点击,则认为是删除整行:即删除换行符,合并成一行*/
    private void delEnterChar() {
        if (null != textWatchListener) {
            Log.d(TAG,"delEnterChar");
            isClickDelKey = true;
            if (isDelEnterLine()) {//删除换行符
                isClickDelKey = false;
                textWatchListener.delEnterChar(PubTopicEditText.this, getText());
            }
        }
    }

    /***是否删除换行符:默认第一次新增时是true*/
    private boolean isDelEnterLine() {
        return 0 == getSelectionStart();
    }

    /**
     * setOnkeyListener监听不到删除键,EditableInputConnection用这玩意监听,
     * 这个标志防止delEvent触发两次。
     * 0:未初始化;1:使用onKey方法触发;2:使用onDelEvdent方法触发
     */
    private int delKeyFlag;

    /*****
     * 为了兼容华为某些机型监听不到删除键
     * @param outAttrs
     * @return
     */
    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        super.onCreateInputConnection(outAttrs);
        EditableInputConnection editableInputConnection = new EditableInputConnection(this);
        outAttrs.initialSelStart = getSelectionStart();
        outAttrs.initialSelEnd = getSelectionEnd();
        outAttrs.initialCapsMode = editableInputConnection.getCursorCapsMode(getInputType());

        editableInputConnection.setDelEventListener(new EditableInputConnection.OnDelEventListener() {
            @Override
            public boolean onDelEvent() {//华为有些机型监听不到删除键、兼容性处理
                if (delKeyFlag == 1) {
                    return false;
                }
                delKeyFlag = 2;
                delEnterChar();
                return false;
            }
        });
        delKeyFlag = 0;
        return editableInputConnection;
    }


    private TextWatchListener textWatchListener ;

    public void setTextWatchListener(TextWatchListener textWatchListener) {
        this.textWatchListener = textWatchListener;
    }

这里直接看监听器:一切逻辑处理都在监听器
先看看常规的监听键盘设置:

this.setOnKeyListener(new View.OnKeyListener() {//华为手机与原生手机等等监听不到键盘的回车键、删除键,只能通过\n来判断兼容
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                hasKeyboardCompatibility = false;
                if (event.getAction() == KeyEvent.ACTION_DOWN) {//要加action_dow否则会执行两次
                    if (keyCode == KeyEvent.KEYCODE_DEL) {
                        if (delKeyFlag == 2) {
                            return false;
                        }
                        delKeyFlag = 1;
                        delEnterChar();
//                        return true;//返回true,则不会删除EditText的文本内容
                    } else if (keyCode == KeyEvent.KEYCODE_ENTER) {//点击换行符
                        clickEnterKey(getSelectionStart());
                        return true;//返回true,则换行符不会输入到EditText,也就不会换行
                    }
                }
                return false;
            }
        });

这里就是平时常用的监听键盘的删除键、回车键等;
先不分析delKeyFlag;等会在看看它的作用
当我们按下删除键、回车键时会分别调用相应的方法delEnterChar();和clickEnterKey()

/***光标在控件首位,再次点击,则认为是删除整行:即删除换行符,合并成一行*/
    private void delEnterChar() {
        if (null != textWatchListener) {
            Log.d(TAG,"delEnterChar");
            isClickDelKey = true;
            if (isDelEnterLine()) {//删除换行符
                isClickDelKey = false;
                textWatchListener.delEnterChar(PubTopicEditText.this, getText());
            }
        }
    }

这里会判断是否是删除的换行符也就是是不是合并preItem内容,删除当前EditText;
isClickDelKey :当执行合并内容之后会更新onTextChange,这个标志用于控制回调delEnterChar还是clickDelKey()

/***
     * 点击回车键:换行,则新增一个控件
     * @param selectionStart
     */
    private void clickEnterKey(int selectionStart) {
        if (null != textWatchListener) {
//            Log.d(TAG,"clickEnterKey");
            CharSequence text = getText();
            CharSequence textBefore = "";
            CharSequence textAfter = "";
            if (!TextUtils.isEmpty(text)) {
                if (-1 != selectionStart) {
                    textBefore = text.subSequence(0, selectionStart);
                    textAfter = text.subSequence(selectionStart, text.length());
                    ----------todo.....
                    }
                }
            }
            isClickEnterKey = true;
            textWatchListener.clickEnterKey(PubTopicEditText.this, textBefore, textAfter);
        }
    }

这里先不光柱todo,因为那里是兼容机型的处理
正常逻辑是:当点击回车键时,单纯的将文本内容分成两半,onKey()return true;消费了事件;不会将换行符“\n”输入到EditText中,若是return false;则换行符会输入进去;

再来看看onTextChange:

public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (null != textWatchListener) {

                            ----------todo...//华为有些机型或者原生机型,监听不到键盘的回车键

                        if (isClickDelKey) {//删除键
                            isClickDelKey = false;
                            textWatchListener.clickDelKey(PubTopicEditText.this, s, beforeLength - s.length());
                        } else if (!isClickEnterKey) {//不是回车键 增加字符
                            textWatchListener.onTextChanged(s, start, before, s.length() - beforeLength);
                        }
                        isClickEnterKey = false;
                    }
                }

同样也先不关心todo..用于兼容机型的代码;
当isClickDelKey=true:证明点击了删除键,但是又不是删除整个item,则回调删除内容的方法
当isClickEnterKey=false:证明不是点击回车键,则直接回调改变了内容

所以整个逻辑就是这么简单。
最后总结就是:光标要点位在某个控件上,需要给这个item指定一个唯一id,然后通过唯一id映射到item的当前position;最后定位到相应位置,即让EditText在屏幕内,就会显示焦点

<2>如何解决部分机型识别不了键盘中的删除键、回车键

之前已经提到todo…里边都是做兼容性处理;那么到底哪些机型这么变态,改的无法监听到删除键、回车键。遇到这种问题惊不惊喜,刺不刺激,变不变态。。。。。
答案是:目前知道的是plk-tl01h华为的或者原生机型,监听不到键盘的回车键、删除键;
一番百度后:这博客吸引了我的注意力
惊喜过后留下一堆忧桑。。。。看了所有的api,只有监听到删除键,并不能监听到回车键,不过一份惊喜一份忧吧;现在能解决删除键,那么回车键怎么搞。。。。一番百度之后,无果,头痛。。。。结了杯水、楼下转转,吃个下午茶,和同事叨叨叨。。。。灵光一闪,是不是EditText也能接受换行符“\n”,断点调试,果不其然。。。。deal。。。完美,提早下班,早日找到女朋友都不是梦了。。。。。
博客写的太累,bb几句。。。。望见谅!!!!
看代码:

public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (null != textWatchListener) {
                       //华为有些机型或者原生机型,监听不到键盘的回车键
                        if (hasKeyboardCompatibility) {
                            int addLength = s.length() - beforeLength;
                            if (addLength == 1 &&
                                    s.length() >= selectionStart + addLength) {//换行符算一个字符
                                CharSequence addStr =
                                        s.subSequence(selectionStart, selectionStart + addLength);
                                if ("\n".equals(addStr.toString())) {//换行符。。算一个字符
                                    clickEnterKey(selectionStart);
                                    return;
                                }
                            }
                        }
                        }
                        .....
                }

hasKeyboardCompatibility为了兼容,当这个为false时,表示onKey好使,则不执行兼容性为题,如果onKey不执行,为默认的true,表示不好使,则执行里边逻辑:
通过键盘输入的换行符“\n”是长度为1的字符,所以要判断输入的内容是不是长度为1的,如果是,则判断是不是\n;如果是:则调用clickEnterKey,在来看看这个方法

private void clickEnterKey(int selectionStart) {
                ......
            if (!TextUtils.isEmpty(text)) {
                if (-1 != selectionStart) {
                   .......
                    if (!TextUtils.isEmpty(textAfter) && textAfter.toString().startsWith("\n")) {
                        //要去掉换行符,,否则会多次循环执行onTextChange,导致android Cannot call this method while RecyclerView is computing a layout or scrolling
                        textAfter = textAfter.subSequence(1, textAfter.length());//过滤换行符
                        if (textAfter.toString().contains("\n")) {//一般不会包含,因为已经过滤掉了,这里防止有导致异常is computing a layout or scrolling
                            textAfter = textAfter.toString().replaceAll("\n", "");//使用这个可能会导致表情解析不出来,所以这里只是作为防止异常产生
                        }
                    }
                }
            }
           .......
        }
    }

这里主要是过滤换行符,否则由于会更新onTextChange,就可能执行多次这个方法,就会导致一个异常产生:android Cannot call this method while RecyclerView is computing a layout or scrolling
这是由于RecycleView正在刷新当中,又更新Item,所有会有这个异常产生;。。。这是个大坑。。。
分析完回车键,那么接着看看删除键的兼容:
删除键的兼容的思想主要是来自前边提到的博客,所以要好好看博客;
主要代码:

@Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        super.onCreateInputConnection(outAttrs);
        EditableInputConnection editableInputConnection = new EditableInputConnection(this);
        outAttrs.initialSelStart = getSelectionStart();
        outAttrs.initialSelEnd = getSelectionEnd();
        outAttrs.initialCapsMode = editableInputConnection.getCursorCapsMode(getInputType());

        editableInputConnection.setDelEventListener(new EditableInputConnection.OnDelEventListener() {
            @Override
            public boolean onDelEvent() {//华为有些机型监听不到删除键、兼容性处理
                if (delKeyFlag == 1) {
                    return false;
                }
                delKeyFlag = 2;
                delEnterChar();
                return false;
            }
        });
        delKeyFlag = 0;
        return editableInputConnection;
    }

重写onCreateInputConnection,为什么,直接看给的链接的博客吧,里边讲的还是比较清楚的,这里简单说一下,就是当唤起键盘是,view会跟键盘建立链接,而链接的桥梁就是通过此方法返回的InputConnection;所以在这里作文章,而EditableInputConnection这玩意我们是拿不到的,所以你可以网上下一个这个类,或者到你自己安装的sdk路径下copy一份(如:\sdk\sources\android-20\com\android\internal\widget),然后反射相应的方法就可以了;
看代码应该猜出来了delKeyFlag 的作用,就是为了防止有些手机既可以执行onkey又可以执行delSourceString()….以至于执行多次delEnterChar();
ok。。。分析完成。。。

<3>如何将图片插入相应的位置
通过分析<1>,应该知道插入图片到相应的位置,应该如何实现了。。。没错,都是通过获取光标所在的位置,直接根据数据源插入图片与EditText的;代码如下;

public void addAllImage(List<TopicPublishModelNew.TypeContent> itemList) {
        if (ToolOthers.isListEmpty(itemList)) {
            return;
        }
        selectedImgCount += itemList.size();
        int cursorIndex = cursorItem.getStartCursorIndex();
        Logcat.dLog("Cindex = " + cursorIndex);
        if (cursorItem.isCursorLast()) {//光标所在文本后插入
            int itemPos = cursorItem.getEtFocudPostion();
            insertImageAfterText(itemPos + 1, itemList);
        } else if (cursorItem.isCursorFirst()) {//光标所在的文本前插入
            int itemPos = cursorItem.getEtFocudPostion();
            if (itemPos > 0) {
                TopicPublishModelNew.TypeContent prevItem = mItemList.get(itemPos - 1);
                if (TopicPublishModelNew.TypeContent.IMAGE_ITEM == prevItem.getType()) {
                    mItemList.add(itemPos, TopicPublishModelNew.TypeContent.createTextInstance(getItemUniqueId(), ""));
                    insertImageBeforeText(itemPos + 1, itemList);
                } else {
                    insertImageBeforeText(itemPos, itemList);
                }
            } else if (0 == itemPos) {
                addAllImages(0, itemList);
            }
        } else if (cursorIndex > 0) {//光标所在文本的位置中插入
            int itemPos = cursorItem.getEtFocudPostion();
            cursorItem.setCurCursorIndex(cursorIndex);//设置光标插入图片后的光标位置
            if (itemPos >= 0) {
                TopicPublishModelNew.TypeContent lastItem = mItemList.get(itemPos);
                lastItem.info.content = cursorItem.getTextBefore();
                insertImageBeteewnText(itemPos + 1, itemList);
            }
        } else {//没有光标,在最后插入
            int size = mItemList.size();
            if (size > 0) {
                TopicPublishModelNew.TypeContent lastItem = mItemList.get(size - 1);
                if (TopicPublishModelNew.TypeContent.IMAGE_ITEM == lastItem.getType()) {
                    mItemList.add(TopicPublishModelNew.TypeContent.createTextInstance(getItemUniqueId(), ""));
                }
            }
            addAllImages(mItemList.size(), itemList);
        }
        if (null != imageListener) {
            imageListener.selectedImgCount(selectedImgCount, selectedGifCount);
        }
    }

    private void addAllImages(int postison, List<TopicPublishModelNew.TypeContent> itemList) {
        List<TopicPublishModelNew.TypeContent> tempList = new ArrayList<>();
        int addSize = itemList.size() * 2;
        for (int i = 0; i < addSize; i++) {
            if (1 == i % 2) {
                tempList.add(TopicPublishModelNew.TypeContent.createTextInstance(getItemUniqueId(), ""));
            } else {//插入图片
                TopicPublishModelNew.TypeContent typeContent = itemList.get(i / 2);
                typeContent.position = getItemUniqueId();
                tempList.add(typeContent);
                addOneSelectedGif(typeContent.info);//gif个数
            }
        }
        mItemList.addAll(postison, tempList);
        notifyDataSetChanged();
    }

    /***文本中间插入*/
    private void insertImageBeteewnText(int postison, List<TopicPublishModelNew.TypeContent> itemList) {
        List<TopicPublishModelNew.TypeContent> tempList = new ArrayList<>();
        int addSize = itemList.size() * 2 - 1;//最后一个文本不需要
        for (int i = 0; i < addSize; i++) {
            if (1 == i % 2) {
                tempList.add(TopicPublishModelNew.TypeContent.createTextInstance(getItemUniqueId(), ""));
            } else {//插入图片
                TopicPublishModelNew.TypeContent typeContent = itemList.get(i / 2);
                typeContent.position = getItemUniqueId();
                tempList.add(typeContent);
                addOneSelectedGif(typeContent.info);//gif个数
            }
        }
        tempList.add(TopicPublishModelNew.TypeContent.createTextInstance(getItemUniqueId(), cursorItem.getTextAfter()));
        mItemList.addAll(postison, tempList);
        notifyDataSetChanged();
    }

    /***文本之前插入*/
    private void insertImageBeforeText(int postison, List<TopicPublishModelNew.TypeContent> itemList) {
        List<TopicPublishModelNew.TypeContent> tempList = new ArrayList<>();
        int addSize = itemList.size() * 2 - 1;
        for (int i = 0; i < addSize; i++) {
            if (1 == i % 2) {
                tempList.add(TopicPublishModelNew.TypeContent.createTextInstance(getItemUniqueId(), ""));
            } else {//插入图片
                TopicPublishModelNew.TypeContent typeContent = itemList.get(i / 2);
                typeContent.position = getItemUniqueId();
                tempList.add(typeContent);
                addOneSelectedGif(typeContent.info);//gif个数
            }
        }
        if (!ToolOthers.isListEmpty(tempList)) {
            mItemList.addAll(postison, tempList);
            notifyDataSetChanged();
        }
    }

    /***文本之后插入*/
    private void insertImageAfterText(int postison, List<TopicPublishModelNew.TypeContent> itemList) {
        int curSize = mItemList.size();
        long lastUniqueId = -1;
        if (postison < curSize) {
            TopicPublishModelNew.TypeContent nextItem = mItemList.get(postison);
            if (TopicPublishModelNew.TypeContent.IMAGE_ITEM == nextItem.getType()) {
                lastUniqueId = getItemUniqueId();//最后一个文本
                mItemList.add(postison, TopicPublishModelNew.TypeContent.createTextInstance(lastUniqueId, ""));
            }
        } else {//最后一行增加一行文本,之后就是直接加入图片 最后一个文本
            postison = curSize;
            lastUniqueId = getItemUniqueId();
            mItemList.add(curSize, TopicPublishModelNew.TypeContent.createTextInstance(lastUniqueId, ""));
        }
        List<TopicPublishModelNew.TypeContent> tempList = new ArrayList<>();
        int addSize = itemList.size() * 2 - 1;
        for (int i = 0; i < addSize; i++) {
            if (1 == i % 2) {//插入文本
                long tempId = getItemUniqueId();
                if (-1 == lastUniqueId && i == addSize - 2) {//最后一个文本
                    lastUniqueId = tempId;
                }
                tempList.add(TopicPublishModelNew.TypeContent.createTextInstance(tempId, ""));
            } else {//插入图片
                TopicPublishModelNew.TypeContent typeContent = itemList.get(i / 2);
                typeContent.position = getItemUniqueId();
                tempList.add(typeContent);
                addOneSelectedGif(typeContent.info);//gif个数
            }
        }
        if (-1 != lastUniqueId) {
            cursorItem.setEtFocusId(lastUniqueId);//光标定位在最后一行的位置
        }
        mItemList.addAll(postison, tempList);
        notifyDataSetChanged();
        final int scrollPos = postison + addSize;
        scrollHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (null != scrollPosListener) {//在文末加入图片后,滚动到位置的文本行,让光标定位
//                    Logcat.dLog("postison + addSize = " + (scrollPos));
                    scrollPosListener.onScrollToPos(scrollPos);
                }
            }
        }, 100);
    }

代码就不分析了,无非就是文本之后、文本之前以及文本之间插入图片;但这里简单说一下思路和不同点
首先获取光标所在的位置:文本首位、文本末尾还是文本之间
在插入数据源之前:
- 判断后一个item是否是图片,如果是,则添加一个EditText
记为lastItem,然后遍历要添加的图片在图片之间增加EditText,最将整个数据源集合插入到lastItem之前;
- 判断前一个是否是图片:是则在之后添加一个EditText,使插入图片和pre图片之间有个EditText;

<4>如何实现并发上传图片

思路:当点击发布日志时,使用线程池,若大于5张图,则线程池大小=imgSize/2+2;若小于5,则线程池大小= 5;先上传图片,拿到图片线上地址,更新图片字段,再将整个数据源封装成json,发布日志;
发起后台服务PublishTopicService:点击发布,直接在后台上传图片,发起日志、失败后保存草稿,成功后删除草稿,但这里草稿不做分析。。。
启动PublishTopicService后执行:

private void upload() {
        if (!isUploading) {
            isUploading = true;
        } else {
            return;
        }
        TopicPublishModelNew modelNew = pollTopicTask();
        if (null == modelNew) {
            stopSelf();
            return;
        }
        if (!ToolPhoneInfo.isNetworkAvailable(this)) {//没有网络,直接发帖失败
            uploadComplete(false,modelNew,null);
            return;
        }
        new PublishController(this).upload(modelNew, new PublishController.PublishCallBack() {
            @Override
            public void onStart(TopicPublishModelNew modelNew) {
                String percent = String.valueOf(90 - mRandom.nextInt(30));
                DraftTableDao.getInstance().updateDraftSendProgress(PublishTopicService.this,percent,modelNew.topic_sign);
                sendBroadcast();
            }

            @Override
            public void onSuccess(TopicPublishModelNew modelNew, String result) {
                super.onSuccess(modelNew, result);
                //Logcat.dLog("onSuccess==");
                uploadComplete(true,modelNew,result);
            }

            @Override
            public void onUploadImageError(TopicPublishModelNew modelNew, List<Integer> errIndexList) {//图片上传失败
                super.onUploadImageError(modelNew, errIndexList);
                //Logcat.dLog("onUploadImageError==");
                uploadComplete(false,modelNew,"发帖失败,已保存至草稿箱[01]");
            }

            @Override
            public void onPublishTopicError(TopicPublishModelNew modelNew, String error) {//发布帖子内容失败
                super.onPublishTopicError(modelNew, error);
                //Logcat.dLog("onPublishTopicError==");
                uploadComplete(false,modelNew,error);
            }
        });
    }
  1. onSuccess:当上传成功(图片和内容都上传成功)则回调
  2. onUploadImageError:图片上传失败时,回调,其它都不回调
  3. onPublishTopicError:图片上传成功的前提,内容上传失败,则回调,其它都不回调

上传图片:主要方法:

 private int uploadMultiImages(TopicPublishModelNew modelNew) {
            List<TopicPublishModelNew.TypeContent> contentList = modelNew.contentList;
//        List<TopicPublishModelNew.TypeContent> contentList = getTestData();
            if (!ToolOthers.isListEmpty(contentList)) {
                int size = contentList.size();
                for (int i = 0; i < size; i++) {//初始化图片大小、及相应任务
                    TopicPublishModelNew.TypeContent typeContent = contentList.get(i);
                    if (null != typeContent && null != typeContent.info && TopicPublishModelNew.TYPE_IMAGE.equals(typeContent.type)) {
                        imageCount++;
                        imageRunnables.add(new UploadImagesRunnable(i, typeContent.info, this));
                    }
                }
                if (imageCount > 0) {//开始上传图片
                    if (imageCount > 5) {
                        mExecutorService = Executors.newFixedThreadPool(imageCount / 2 + 2);
                    } else {
                        mExecutorService = Executors.newFixedThreadPool(imageCount);
                    }
                    mCallBack.onStart(modelNew);
                    for (UploadImagesRunnable runnable : imageRunnables) {
                        mExecutorService.execute(runnable);
                    }
                }
            }
            return imageCount;
        }

主要是计算要上传的图片个数imageCount,根据个数,开启线程池的大小;然后执行UploadImagesRunnable上传图片
那么如何知道多线程已经上传完毕?

/*** 上传图片成功的个数*/
        public void updateImageUploadCount() {
//        Logcat.dLog("updateImageUploadCount0 = " + imageCount);
            synchronized (this) {
                imageCount--;
                Logcat.dLog("updateImageUploadCount1 = " + imageCount);
                if (0 >= imageCount) {
                    handler.sendEmptyMessage(UPLOAD_IMG_OK);
                }
            }
        }

通过控制this互斥,统计已经上传图片的个数,当imageCount<=0时,表示已经上传完了;

/****先关闭线程池,不会影响已运行的线程*/
    public void cacelUploadImagesTask(int picIndex) {
        if (!mExecutorService.isShutdown()) {
            //图片上传失败
            handler.sendEmptyMessage(picIndex);
        }
        Logcat.dLog("===mExecutorService.isShutdown()===" + picIndex + " " + mExecutorService.isShutdown());
    }

当有一张图片上传失败时,会取消线程池中未开始的任务,但已经开始的,会等待执行完才会关闭线程池,期间是无法中断的;
当图片都上传成功后会通过handle发送消息,直接上传内容;
接下来看看上传图片的UploadImagesRunnable :

public class UploadImagesRunnable implements Runnable {
    private int picIndex;
    private TopicPublishModelNew.PublishContent imageItem;
    private PublishController controller;

    public UploadImagesRunnable(int picIndex, TopicPublishModelNew.PublishContent imageItem, PublishController controller) {
        this.picIndex = picIndex;
        this.imageItem = imageItem;
        this.controller = controller;
    }


    @Override
    public void run() {
        boolean isUploaded = false;
        boolean isGifUploaded = false;
        if (null != imageItem) {
            String srcFilePath = imageItem.local_img;
            if (!isImageUploaded()) {//则先上传第一帧成功后在上传gif:则服务器返回的图片地址gif字段就会覆盖它的静态图的img路径
                String filePath = imageItem.local_thumb;
                isUploaded = uploadImage(filePath, srcFilePath, imageItem.local_hash, false);//上传普通缩略图
            } else {
                isUploaded = true;
            }
            if (isUploaded && 1 == imageItem.is_gif) {//上传gif
                if (!isGifUploaded()) {
                    String filePath = imageItem.local_img;
                    isGifUploaded = uploadImage(filePath, srcFilePath, imageItem.local_hash, true);
                } else {
                    isGifUploaded = true;
                }
            }

        }
        if (isAllUploadedOk(isUploaded, isGifUploaded)) {//成功
            controller.updateImageUploadCount();
        } else {//失败
            controller.cacelUploadImagesTask(picIndex);
        }
    }

    /**
     * 是否gif都静态图都上传成功
     *
     * @param isUploaded
     * @param isGifUploaded
     * @return
     */
    private boolean isAllUploadedOk(boolean isUploaded, boolean isGifUploaded) {
        if (1 == imageItem.is_gif) {
            if (isUploaded && isGifUploaded) {
                return true;
            }
        } else if (isUploaded) {
            return true;
        }
        return false;
    }

    /*** gif图片是否已经上传过了**/
    private boolean isGifUploaded() {
        ...是否已经上传,通过后台回传的hash值和当前的hash值或者图片字段是否有值判断。。自己是实现吧
        return false;
    }

    /*** 图片是否已经上传过了**/
    private boolean isImageUploaded() {
        ...是否已经上传,通过后台回传的hash值和当前的hash值或者图片字段是否有值判断。。自己是实现吧
        return false;
    }

    /**
     * @param filePath    上传图的地址
     * @param srcFilePath 原图地址 :若用户清空缓存,则用原图压缩
     * @param localHash   原图hash值
     * @param isGif       是否是gif
     * @return
     */
    private boolean uploadImage(String filePath, String srcFilePath, String localHash, boolean isGif) {
        File uploadFile = null;
        if (!ToolString.isEmpty(filePath)) {
            uploadFile = new File(filePath);
        }
        if ((null == uploadFile || !uploadFile.exists()) && !ToolString.isEmpty(srcFilePath)) {//压缩图没有。则用原图重新压缩
            uploadFile = FileUtils.compressFile(srcFilePath);
            if (null != uploadFile) {
                imageItem.local_thumb = uploadFile.getAbsolutePath();
            }
        }
        if (null != uploadFile && uploadFile.exists()) {
           ....todo,网络上传图片。。。。okhttp就支持
                if (!ToolString.isEmpty(strResult)) {
                    return paseResult(strResult, isGif);
                }
            } catch (final Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    public boolean paseResult(String result, boolean isGif) {
        LmbRequestResult<JSONArray> resultData = null;
        try {
            resultData = BaseTools.getJsonResult(result, JSONArray.class);
        } catch (Exception e) {
            return false;
        }
        if (resultData == null || resultData.data == null || !"0".equals(resultData.ret)) {
            return false;
        }
        return TopicPublishModelNew.PublishContent.parseUploadImageData(resultData.data, imageItem, isGif);
    }
}

这里会判断:当前图片是否已经上传过,如果上传过,不再上传;
当前图片是否是gif,是的话先上船gif第一帧用于做封面,上传成功后再上传gif。。。这是后台的缺陷,才放到客户端做的。。。。这一步,很蛋疼。。。。。
上传完后,看解析:

public static boolean parseUploadImageData(JSONArray resultJsonAry, PublishContent content, boolean isGif) {
            JSONObject object = null;
            if (null != resultJsonAry) {
                object = resultJsonAry.optJSONObject(0);
            }
            if (null != object) {
                if (isGif) {//如果是gif
                    String imgPath = object.optString("xxxx");
                    if (!ToolString.isEmpty(imgPath)) {//图片路径为空,则为失败
                        content.service_img = imgPath;
                        content.gif_service_hash = object.optString("xxx");
//                    content.service_thumb = object.optString("xx");//用的是静态图
                        content.gif_size = object.optInt("xxx");
                        content.gif_width = object.optInt("xxx");
                        content.gif_height = object.optInt("xxx");
                        return true;
                    }
                } else {//静态图上传
                    String imgThumb = object.optString("xxx");
                    if (!ToolString.isEmpty(imgThumb)) {//图片路径为空,则为失败
                        if (1 != content.is_gif) {//上传的是gif的静态图,不用解析这个字段,否则正常发帖时,gif图上传不了
                            content.service_img = object.optString("xxx");
                        }
                        content.service_thumb = imgThumb;
                        content.service_hash = object.optString("xxxx");
                        content.size = object.optInt("xxx");
                        content.width = object.optInt("xxx");
                        content.height = object.optInt("xxxx");
                        return true;
                    }
                }
                //Logcat.dLog(isGif+" thumb = " + content.service_thumb);
                //Logcat.dLog(isGif+" img = " + content.service_img);
            }
            return false;
        }

主要是更新数据源的图片路径、宽高、大小
上传图片,写的比较马虎,有这个思路,直接根据源码应该比较好懂!!!!
source unexecutable
到此为止。。。所有都完成了。。。。大半年不写博客,真心累。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章