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
到此为止。。。所有都完成了。。。。大半年不写博客,真心累。。。。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Android Studio 是一款用于开发 Android 应用程序的集成开发环境。而豆瓣电影则是一个提供电影信息、影评、评分等服务的网站。在 Android Studio 中,可以使用豆瓣电影的 API 来获取电影信息,并将其展示在应用程序中。这样,用户就可以在应用程序中浏览电影信息,查看影评和评分等内容。 ### 回答2: Android Studio 是一个用于开发 Android 应用程序的集成开发环境。豆瓣电影是一个非常受欢迎的电影信息展示平台。在 Android Studio 中使用豆瓣电影 API 可以实现电影信息的获取和展示。 首先,我们需要获取豆瓣电影 API 的访问 Permission。在豆瓣开发者平台注册账号并创建应用,然后获取相应的 API Key 即可。 其次,我们可以创建一个新的 Android Studio 项目,并添加 Retrofit 相关依赖项。Retrofit 是一个用于与 HTTP REST Api 进行交互的库,它非常适合我们的电影信息获取需求。 然后,我们可以使用 Retrofit 建立一个网络请求,并从豆瓣电影 API 中获取电影信息。我们可以从 API 中获取电影的标题、上映日期、评分等多个信息,然后在 Android 应用程序中展示这些数据。 最后,我们需要将电影信息以适当的方式展示在 Android 应用程序中,可能是通过 RecyclerView 的卡片视图、列表视图或详细视图等。 总的来说,使用 Android Studio 和 Retrofit 结合豆瓣电影 API 可以帮助开发者快速构建一个实用的电影信息展示应用程序。这对于那些想创造一个电影信息应用程序的开发者来说非常有帮助。 ### 回答3: Android Studio是一个流行的集成开发环境,可以为Android应用程序开发人员提供强大的工具和功能。开发人员可以使用这个环境开发从简单的演示应用程序到复杂的商业应用程序。豆瓣电影是一个备受欢迎的电影评价网站,拥有丰富的电影信息和评论,广受全球电影迷的欢迎。 Android Studio与豆瓣电影的结合可以为开发者带来丰富的体验。首先,Android Studio提供了许多模板和工具,以帮助开发者快速构建电影相关应用,如电影搜索、电影预告等等。同时,豆瓣电影网站提供了可用于开发的公共API,这些API可以使Android Studio的优势得到进一步发扬。 使用Android Studio进行豆瓣电影应用开发,还可以充分利用豆瓣电影网站的其他丰富资源,如电影海报、简介、评价、影人信息等。同时,Android Studio提供了强大的数据库和网络通讯功能,可以更好地管理这些数据和信息。 此外,豆瓣电影网站拥有庞大的用户社区,Android Studio结合豆瓣电影网站可以充分利用用户的反馈和需求来完善应用。 总的来说,Android Studio和豆瓣电影网站的结合可以为Android应用程序开发带来丰富的资源和体验,这也是一个充满潜力的领域,需要了解开发技巧和不断创新思路。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值