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

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