Android自定義頻道選擇器、頻道定制

1.頻道選擇器,頻道定制

??現(xiàn)在市場(chǎng)上的新聞軟件中,絕大多數(shù)都會(huì)有頻道選擇器,比如騰訊新聞、網(wǎng)易新聞、今日頭條等,頻道選擇器可以幫助用戶(hù)定制自己想要的新聞板塊,給用戶(hù)更好的體驗(yàn)。我們的項(xiàng)目正好也是一個(gè)新聞?lì)怉PP,為了更好的符合我們的產(chǎn)品,我們需要自己實(shí)現(xiàn)一套頻道選擇器,項(xiàng)目地址ChannelView,如果有需要的朋友可以看一下,先來(lái)看一下效果圖。

??從效果上看,我們的頻道選擇器已經(jīng)完全不弱于市面上的大多數(shù)主流應(yīng)用的選擇器,頻道拖動(dòng)、頻道刪除、頻道添加,動(dòng)畫(huà)效果都已經(jīng)包含,并且十分流暢沒(méi)有卡頓,下面我們就一起看看這款自定義View是如何實(shí)現(xiàn)的吧。

2.View結(jié)構(gòu)

??想實(shí)現(xiàn)這個(gè)選擇器不難,因?yàn)樗皇且恍?duì)子View的布局和位置調(diào)整,所以我們的重點(diǎn)就是確定每個(gè)子View的位置,并保存它的坐標(biāo),然后用動(dòng)畫(huà)讓子View之間可以交換位置也就是交換坐標(biāo),這些核心地方實(shí)現(xiàn)了,其他的一些拖動(dòng)、增刪功能也就不是問(wèn)題了,都是在它的基礎(chǔ)上實(shí)現(xiàn)的。

??現(xiàn)在,我們都知道要實(shí)現(xiàn)一個(gè)自定義View我們需要繼承View或者ViewGroup,這里我們一看擁有這么多子View就知道肯定要繼承ViewGroup了。但問(wèn)題又來(lái)了,在那么的多的ViewGroup中我們需要使用哪個(gè)呢?其實(shí)有很多的選擇,關(guān)鍵是哪個(gè)更方便,先讓我們來(lái)考慮一下選擇哪個(gè)吧,這是任何一個(gè)自定義View的開(kāi)始,選合適了我們可以事半功倍。

??如果我們的View繼承LinearLayout,雖然子View有一定的順序讓我們不用覆蓋它的onLayout()方法重寫(xiě),但由于橫向、豎向都有所以我們要嵌套的使用LinarLayout,這會(huì)讓View過(guò)度繪制。如果繼承RelativeLayout的話,它的子View似乎需要我們自己確定位置,我們需要在onLayout()里面進(jìn)行計(jì)算每個(gè)View,這似乎還不如直接繼承ViewGroup,其實(shí)我的第一個(gè)頻道選擇器就是繼承的ViewGroup實(shí)現(xiàn)的,功能效果跟現(xiàn)在的幾乎一樣,但代碼實(shí)現(xiàn)上慘不忍睹,所以又重新寫(xiě)了現(xiàn)在這個(gè)。好了,我們的這個(gè)頻道選擇器是繼承GridLayout實(shí)現(xiàn)的,其實(shí)一看它的布局就應(yīng)該能想到,結(jié)果我第一次實(shí)現(xiàn)的時(shí)候卻沒(méi)想到它,使用GridLayout的好處是我們不用自己去實(shí)現(xiàn)子View的位置,只需要添加子View后它就會(huì)根據(jù)根據(jù)我們對(duì)GridLayout的屬性設(shè)置自動(dòng)布局好每個(gè)子View的位置,然后我們只需要在onLayout()方法中遍歷每個(gè)子View得到它的坐標(biāo)位置保存就OK了,我們看一下整個(gè)選擇器的布局情況

??整個(gè)自定義View只有3層,最外層是繼承ScrollView的ChannelView,中間是繼承GridLayout的ChannelLayout,最里面一層是并列的TextView頻道,下面我們進(jìn)入正題吧。

3.代碼結(jié)構(gòu)

??我們的自定義View命名為ChannelView,包括兩個(gè)內(nèi)部類(lèi)和一個(gè)接口,其中ChannelView繼承ScrollView,可以實(shí)現(xiàn)上下滑動(dòng),ChannelAttr是頻道屬性,ChannelLayout繼承GridLayout是整個(gè)頻道選擇器的核心類(lèi)。

4.具體實(shí)現(xiàn)

  • 初始化數(shù)據(jù)
public ChannelLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

private void init() {
    setColumnCount(channelColumn);
    setPadding(channelPadding, channelPadding, channelPadding, channelPadding);
    addChannelView();
}

/**
 * 設(shè)置頻道View
 */
private void addChannelView() {
    if (channelContents != null) {
        groupChannelColumns = new int[channelContents.size()];
        int j = 0;
        int startRow = 0;
        for (String aKeySet : channelContents.keySet()) {//遍歷key值,設(shè)置標(biāo)題名稱(chēng)
            String[] channelContent = channelContents.get(aKeySet);
            if (channelContent == null) {
                channelContent = new String[]{};
            }
            groupChannelColumns[j] = channelContent.length % channelColumn == 0 ? channelContent.length / channelColumn : channelContent.length / channelColumn + 1;
            if (j == 0) {
                startRow = 0;
            } else {
                startRow += groupChannelColumns[j - 1] + 1;
            }
            Spec rowSpec = GridLayout.spec(startRow);
            //標(biāo)題要占channelColumn列
            Spec columnSpec = GridLayout.spec(0, channelColumn);
            LayoutParams layoutParams = new LayoutParams(rowSpec, columnSpec);
            View view = LayoutInflater.from(mContext).inflate(R.layout.cgl_my_channel, null);
            if (j == 0) {
                tipEdit = view.findViewById(R.id.tv_tip_edit);
                tipEdit.setVisibility(VISIBLE);
                tipEdit.setOnClickListener(this);
                tipFinish = view.findViewById(R.id.tv_tip_finish);
                tipFinish.setVisibility(INVISIBLE);
                tipFinish.setOnClickListener(this);
            }
            ChannelAttr channelTitleAttr = new ChannelAttr();
            channelTitleAttr.type = ChannelAttr.TITLE;
            channelTitleAttr.coordinate = new PointF();
            //為標(biāo)題View添加一個(gè)ChannelAttr屬性
            view.setTag(channelTitleAttr);
            TextView tvTitle = view.findViewById(R.id.tv_title);
            tvTitle.setText(aKeySet);
            addView(view, layoutParams);
            channelTitleGroups.add(view);
            ArrayList<View> channelGroup = new ArrayList<>();
            int remainder = channelContent.length % channelColumn;
            for (int i = 0; i < channelContent.length; i++) {//遍歷value中的頻道
                TextView textView = new TextView(mContext);
                ChannelAttr channelAttr = new ChannelAttr();
                channelAttr.type = ChannelAttr.CHANNEL;
                channelAttr.groupIndex = j;
                channelAttr.coordinate = new PointF();
                if (j != 0) {
                    channelAttr.belong = j;
                } else {
                    if (channelBelongs.indexOfKey(i) >= 0) {
                        int belongId = channelBelongs.get(i);
                        if (belongId > 0 && belongId < channelContents.size()) {
                            channelAttr.belong = belongId;
                        } else {
                            Log.w(getClass().getSimpleName(), "歸屬I(mǎi)D不存在,默認(rèn)設(shè)置為1");
                        }
                    }
                }
                //為頻道添加ChannelAttr屬性
                textView.setTag(channelAttr);
                textView.setText(channelContent[i]);
                textView.setGravity(Gravity.CENTER);
                textView.setBackgroundResource(channelNormalBackground);
                if (j == 0 && i <= channelFixedToPosition) {
                    textView.setTextColor(channelFixedColor);
                }
                textView.setOnClickListener(this);
                textView.setOnTouchListener(this);
                textView.setOnLongClickListener(this);
                //設(shè)置每個(gè)頻道的間距
                LayoutParams params = new LayoutParams();
                int leftMargin = verticalSpacing, topMargin = horizontalSpacing, rightMargin = verticalSpacing, bottomMargin = horizontalSpacing;
                if (i % channelColumn == 0) {
                    leftMargin = 0;
                }
                if ((i + 1) % channelColumn == 0) {
                    rightMargin = 0;
                }
                if (i < channelColumn) {
                    topMargin = 0;
                }
                if (remainder == 0) {
                    if (i >= channelContent.length - channelColumn) {
                        bottomMargin = 0;
                    }
                } else {
                    if (i >= channelContent.length - remainder) {
                        bottomMargin = 0;
                    }
                }
                params.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
                addView(textView, params);
                channelGroup.add(textView);
            }
            channelGroups.add(channelGroup);
            j++;
        }
    }
}

??通過(guò)setColumnCount(channelColumn)我們?cè)O(shè)置GridView的列數(shù)為channelColumn列。

??在addChannelView()中,我們主要做了如下幾個(gè)方面:
??1. 從存儲(chǔ)頻道的集合Map<String, String[]> channelContents中獲取數(shù)據(jù)。channelContents的長(zhǎng)度代表有多少組頻道,channelContents中的key為每組頻道的標(biāo)題,比如有“我的頻道”、“推薦頻道”、“國(guó)內(nèi)頻道”、“國(guó)外頻道”等,channelContents中的value為頻道組中的具體頻道。默認(rèn)channelContents中的第0項(xiàng)為“我的頻道”,是可以拖拽排序的,其他組都為待添加的頻道,不能拖拽,如果channelContents的大小為1,也就是只有“我的頻道”,那么會(huì)默認(rèn)再添加一組頻道數(shù)量為0的頻道組,為的是可以刪除已選擇的頻道,這段邏輯沒(méi)有在上面的代碼中,上面代碼中的channelContents是已經(jīng)處理好的變量,具體處理的細(xì)節(jié)可以看工程中的代碼;

??2. 遍歷channelContents的key值,創(chuàng)建頻道的標(biāo)題View,將key的值設(shè)為頻道標(biāo)題;并讓這個(gè)View所占的列數(shù)為channelColumn,將標(biāo)題添加到channelTitleGroups集合中;

??3. 在遍歷key的同時(shí),遍歷value中的頻道,將每個(gè)頻道作為T(mén)extView添加到GridView中,并且所占的列數(shù)為1列,將頻道添加到channelGroups集合中;

??4. 為每個(gè)子View設(shè)置一個(gè)屬性ChannelAttr,屬性中包含了類(lèi)型、坐標(biāo)等:

/**
 * 頻道屬性
 */
private class ChannelAttr {
    static final int TITLE = 0x01;
    static final int CHANNEL = 0x02;

    /**
     * view類(lèi)型
     */
    private int type;

    /**
     * view坐標(biāo)
     */
    private PointF coordinate;

    /**
     * view所在的channelGroups位置
     */
    private int groupIndex;

    /**
     * 頻道歸屬,用于刪除頻道時(shí)該頻道的歸屬位置(推薦、國(guó)內(nèi)、國(guó)外),默認(rèn)都為1
     */
    private int belong = 1;
}

??groupIndex:說(shuō)明當(dāng)前頻道所在哪個(gè)頻道組,在添加頻道或刪除頻道時(shí)會(huì)發(fā)生變化,頻道標(biāo)題沒(méi)有該屬性;

??belong:是不會(huì)變化的,在初始化數(shù)據(jù)時(shí)已經(jīng)確定,它表明了該頻道原來(lái)是屬于什么地方的,當(dāng)從“我的頻道”中刪除時(shí)我們可以根據(jù)它知道該頻道應(yīng)該到哪去,頻道標(biāo)題沒(méi)有該屬性;

??coordinate:表示當(dāng)前頻道的坐標(biāo),會(huì)隨著增、刪、移動(dòng)頻道時(shí)發(fā)生變化;

??type:表示當(dāng)前View的類(lèi)別,只有兩種,頻道標(biāo)題或者頻道。

  • 測(cè)量和布局
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (isAgainMeasure) {
        int width = MeasureSpec.getSize(widthMeasureSpec);//ChannelLayout的寬
        //不是通過(guò)動(dòng)畫(huà)改變ChannelLayout的高度
        if (!isAnimateChangeHeight) {
            int height = 0;
            int allChannelTitleHeight = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View childAt = getChildAt(i);
                if (((ChannelAttr) childAt.getTag()).type == ChannelAttr.TITLE) {
                    //計(jì)算標(biāo)題View的寬高
                    childAt.measure(MeasureSpec.makeMeasureSpec(width - channelPadding * 2, MeasureSpec.EXACTLY), heightMeasureSpec);
                    allChannelTitleHeight += childAt.getMeasuredHeight();
                } else if (((ChannelAttr) childAt.getTag()).type == ChannelAttr.CHANNEL) {
                    //計(jì)算每個(gè)頻道的寬高
                    channelWidth = (width - verticalSpacing * (channelColumn * 2 - 2) - channelPadding * 2) / channelColumn;
                    childAt.measure(MeasureSpec.makeMeasureSpec(channelWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(channelHeight, MeasureSpec.EXACTLY));
                }
            }
            for (int groupChannelColumn : groupChannelColumns) {
                if (groupChannelColumn > 0) {
                    height += channelHeight * groupChannelColumn + (groupChannelColumn * 2 - 2) * horizontalSpacing;
                }
            }
            allChannelGroupsHeight = height;
            height += channelPadding * 2 + allChannelTitleHeight;//ChannelLayout的高
            setMeasuredDimension(width, height);
        } else {//通過(guò)動(dòng)畫(huà)改變ChannelLayout的高度
            setMeasuredDimension(width, animateHeight);
        }
    }
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    if (isAgainLayout) {
        super.onLayout(changed, left, top, right, bottom);
        for (int i = 0; i < getChildCount(); i++) {
            View childAt = getChildAt(i);
            ChannelAttr tag = (ChannelAttr) childAt.getTag();
            tag.coordinate.x = childAt.getX();
            tag.coordinate.y = childAt.getY();
        }
        isAgainLayout = false;
    }
}

??onMeasure()方法中測(cè)量出選擇器的寬高,寬width已經(jīng)計(jì)算出,高由子View來(lái)決定。這里首先通過(guò)measure(int widthMeasureSpec, int heightMeasureSpec)方法測(cè)量所有子View大小。子View只有兩種類(lèi)型,標(biāo)題子View和頻道子View,其中標(biāo)題View是從xml布局中獲取的,它的寬高是已經(jīng)確定的值,不需要我們自己計(jì)算(代碼中它的寬還是需要我們計(jì)算的,因?yàn)槲覀優(yōu)镃hannelView自定義的屬性中有padding,所以還需要減去padding的值)。頻道子View的高度由ChannelView的自定義屬性確定不需要計(jì)算,寬度我們可以通過(guò)ChannelView的寬除以列數(shù)再減去padding和頻道之間的間距就可以得到。最后,我們根據(jù)子View的行數(shù)和每行的高度確定ChannelView的高,然后調(diào)用setMeasuredDimension()方法就可以了。

??在onMeasure()方法中,有兩個(gè)setMeasuredDimension()方法,其中上面的是我們用來(lái)第一次計(jì)算ChannelLayout用的,下面的是動(dòng)態(tài)的改變高度時(shí)調(diào)用的,也就是頻道的行數(shù)有變化時(shí),能讓高度通過(guò)動(dòng)畫(huà)形式平滑的改變高度,而不是突然變高或者變矮。

??onLayout()方法中很簡(jiǎn)單,不需要我們自己確定子View的位置,只需要存儲(chǔ)它在布局好之后的位置坐標(biāo)就可以。

  • 效果需求

??上面的兩步完成之后我們已經(jīng)得到了想要的布局,也確定了每個(gè)子View的坐標(biāo)位置,現(xiàn)在我們已經(jīng)不需要GridLayout的幫助了,忘記它,它已經(jīng)完成了它的使命。接下來(lái)我們要做的就是拖動(dòng)改變頻道順序、增刪頻道。

??我們先來(lái)考慮一下我們想要的效果,當(dāng)長(zhǎng)按“我的頻道”時(shí)我們希望能編輯它,拖動(dòng)頻道可以改變它的順序,并且它的樣式也能改變,提示用戶(hù)能刪除這個(gè)頻道,然后后面的頻道能往前移動(dòng),刪除的頻道可以回歸到它所屬于的頻道組。當(dāng)點(diǎn)擊“完成”時(shí),我們希望樣式能恢復(fù)。對(duì)于其它的頻道組,我們希望點(diǎn)擊它的時(shí)候能添加到“我的頻道”中去,后面的頻道能往前排移動(dòng),這就是我們想要的效果。

??整個(gè)代碼中不需要實(shí)現(xiàn)特別復(fù)雜的觸摸事件,所以我們只需要繼承OnTouchListener、OnLongClickListener、OnClickListener就可以了

@Override
public boolean onTouch(View v, MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        downX = event.getRawX();
        downY = event.getRawY();
    }
    if (event.getAction() == MotionEvent.ACTION_MOVE && isChannelLongClick) {
        //手移動(dòng)時(shí)拖動(dòng)頻道
        channelDrag(v, event);
    }
    if (event.getAction() == MotionEvent.ACTION_UP && isChannelLongClick) {
        //手抬起時(shí)頻道狀態(tài)
        channelDragUp(v);
    }
    return false;
}

@Override
public void onClick(View v) {
    if (v == tipFinish) {//點(diǎn)擊完成按鈕時(shí)
        changeTip(false);
        List<String> myChannels = new ArrayList<>();
        for (View view : channelGroups.get(0)) {
            myChannels.add(((TextView) view).getText().toString());
        }
        if (onChannelListener != null) {
            onChannelListener.channelFinish(myChannels);
        }
    } else {
        ChannelAttr tag = (ChannelAttr) v.getTag();
        ArrayList<View> channels = channelGroups.get(tag.groupIndex);
        if (tag.groupIndex == 0) {//如果點(diǎn)擊的是我的頻道組中的頻道
            if (channelClickType == DELETE && channels.indexOf(v) > channelFixedToPosition) {
                forwardSort(v, channels);
                //減少我的頻道
                deleteMyChannel(v);
            } else if (channelClickType == NORMAL) {
                //普通狀態(tài)時(shí)進(jìn)行點(diǎn)擊事件回調(diào)
                if (onChannelListener != null) {
                    onChannelListener.channelItemClick(channels.indexOf(v), ((TextView) v).getText().toString());
                }
            }
        } else {//點(diǎn)擊的其他頻道組中的頻道
            forwardSort(v, channels);
            //增加我的頻道
            addMyChannel(v);
        }
    }
}

@Override
public boolean onLongClick(View v) {
    v.bringToFront();
    ChannelAttr tag = (ChannelAttr) v.getTag();
    if (tag.groupIndex == 0) {//判斷是否點(diǎn)擊的我的頻道組
        ArrayList<View> views = channelGroups.get(0);
        int indexOf = views.indexOf(v);
        if (indexOf > channelFixedToPosition) {
            for (int i = channelFixedToPosition + 1; i < views.size(); i++) {
                if (i == indexOf) {
                    views.get(i).setBackgroundResource(channelFocusedBackground);
                } else {
                    views.get(i).setBackgroundResource(channelSelectedBackground);
                }
            }
            changeTip(true);
        }
    }
    //要返回true,否則會(huì)出發(fā)onclick事件
    return true;
}

??在各個(gè)點(diǎn)擊事件中,我們需要判斷每次點(diǎn)擊的View屬性,根據(jù)v.getTag()方法獲取到ChannelAttr,判斷它此時(shí)所在的頻道組(位于channelGroups中的位置),判斷它原來(lái)歸屬于哪個(gè)頻道組,以及它的坐標(biāo),然后做出相應(yīng)的操作,比如拖拽、增刪等,下面我們來(lái)具體看一下這部分的代碼。

  • 效果實(shí)現(xiàn)
/**
 * 后面的頻道向前排序
 *
 * @param v
 * @param channels
 */
private void forwardSort(View v, ArrayList<View> channels) {
    int size = channels.size();
    int indexOfValue = channels.indexOf(v);
    if (indexOfValue != size - 1) {
        for (int i = size - 1; i > indexOfValue; i--) {
            View lastView = channels.get(i - 1);
            ChannelAttr lastViewTag = (ChannelAttr) lastView.getTag();
            View currentView = channels.get(i);
            ChannelAttr currentViewTag = (ChannelAttr) currentView.getTag();
            currentViewTag.coordinate = lastViewTag.coordinate;
            currentView.animate().x(currentViewTag.coordinate.x).y(currentViewTag.coordinate.y).setDuration(DURATION_TIME);
        }
    }
}

/**
 * 增加我的頻道
 *
 * @param v
 */
private void addMyChannel(final View v) {
    //讓點(diǎn)擊的view置于最前方,避免遮擋
    v.bringToFront();
    ChannelAttr tag = (ChannelAttr) v.getTag();
    ArrayList<View> channels = channelGroups.get(tag.groupIndex);
    ArrayList<View> myChannels = channelGroups.get(0);
    View finalMyChannel;
    if (myChannels.size() == 0) {
        finalMyChannel = channelTitleGroups.get(0);
    } else {
        finalMyChannel = myChannels.get(myChannels.size() - 1);
    }
    ChannelAttr finalMyChannelTag = (ChannelAttr) finalMyChannel.getTag();
    myChannels.add(myChannels.size(), v);
    channels.remove(v);
    animateChangeGridViewHeight();
    final ViewPropertyAnimator animate = v.animate();
    if (myChannels.size() % channelColumn == 1 || channelColumn == 1) {
        if (myChannels.size() == 1) {
            tag.coordinate = new PointF(finalMyChannelTag.coordinate.x, finalMyChannelTag.coordinate.y + finalMyChannel.getMeasuredHeight());
            //我的頻道多一行,下面的view往下移
            viewMove(1, channelHeight);
        } else {
            ChannelAttr firstMyChannelTag = (ChannelAttr) myChannels.get(0).getTag();
            tag.coordinate = new PointF(firstMyChannelTag.coordinate.x, finalMyChannelTag.coordinate.y + channelHeight + horizontalSpacing * 2);
            //我的頻道多一行,下面的view往下移
            viewMove(1, channelHeight + horizontalSpacing * 2);
        }
        animate.x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
    } else {
        tag.coordinate = new PointF(finalMyChannelTag.coordinate.x + channelWidth + verticalSpacing * 2, finalMyChannelTag.coordinate.y);
        animate.x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
    }
    animate.setListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (channelClickType == DELETE) {
                v.setBackgroundResource(channelSelectedBackground);
                animate.setListener(null);
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    });
    //該頻道少一行,下面的view往上移
    if (channels.size() % channelColumn == 0) {
        if (channels.size() == 0) {
            viewMove(tag.groupIndex + 1, -channelHeight);
        } else {
            viewMove(tag.groupIndex + 1, -channelHeight - horizontalSpacing * 2);
        }
    }
    tag.groupIndex = 0;
}

/**
 * 刪除我的頻道
 *
 * @param v
 */
private void deleteMyChannel(View v) {
    //讓點(diǎn)擊的view置于最前方,避免遮擋
    v.bringToFront();
    if (channelClickType == DELETE) {
        v.setBackgroundResource(channelNormalBackground);
    }
    ChannelAttr tag = (ChannelAttr) v.getTag();
    ArrayList<View> beLongChannels = channelGroups.get(tag.belong);
    if (beLongChannels.size() == 0) {
        tag.coordinate = new PointF(((ChannelAttr) channelTitleGroups.get(tag.belong).getTag()).coordinate.x, ((ChannelAttr) channelTitleGroups.get(tag.belong).getTag()).coordinate.y + channelTitleGroups.get(tag.belong).getMeasuredHeight());
    } else {
        ChannelAttr arriveTag = (ChannelAttr) beLongChannels.get(0).getTag();
        tag.coordinate = arriveTag.coordinate;
    }
    v.animate().x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
    beLongChannels.add(0, v);
    channelGroups.get(0).remove(v);
    animateChangeGridViewHeight();
    PointF newPointF;
    ChannelAttr finalChannelViewTag = (ChannelAttr) beLongChannels.get(beLongChannels.size() - 1).getTag();
    //這個(gè)地方要注意順序
    if (channelGroups.get(0).size() % channelColumn == 0) {
        //我的頻道中少了一行,底下的所有view全都上移
        if (channelGroups.get(0).size() == 0) {
            viewMove(1, -channelHeight);
        } else {
            viewMove(1, -channelHeight - horizontalSpacing * 2);
        }
    }
    if (beLongChannels.size() % channelColumn == 1) {
        //回收來(lái)頻道中多了一行,底下的所有view全都下移
        if (beLongChannels.size() == 1) {
            viewMove(tag.belong + 1, channelHeight);
        } else {
            viewMove(tag.belong + 1, channelHeight + horizontalSpacing * 2);
        }
        newPointF = new PointF(tag.coordinate.x, finalChannelViewTag.coordinate.y + channelHeight + horizontalSpacing * 2);
    } else {
        newPointF = new PointF(finalChannelViewTag.coordinate.x + channelWidth + verticalSpacing * 2, finalChannelViewTag.coordinate.y);
    }
    for (int i = 1; i < beLongChannels.size(); i++) {
        View currentView = beLongChannels.get(i);
        ChannelAttr currentViewTag = (ChannelAttr) currentView.getTag();
        if (i < beLongChannels.size() - 1) {
            View nextView = beLongChannels.get(i + 1);
            ChannelAttr nextViewTag = (ChannelAttr) nextView.getTag();
            currentViewTag.coordinate = nextViewTag.coordinate;
        } else {
            currentViewTag.coordinate = newPointF;
        }
        currentView.animate().x(currentViewTag.coordinate.x).y(currentViewTag.coordinate.y).setDuration(DURATION_TIME);
    }
    tag.groupIndex = tag.belong;
}

/**
 * 行數(shù)變化后的gridview高度并用動(dòng)畫(huà)改變
 */
private void animateChangeGridViewHeight() {
    int newAllChannelGroupsHeight = 0;
    for (int i = 0; i < channelGroups.size(); i++) {
        ArrayList<View> channels = channelGroups.get(i);
        groupChannelColumns[i] = channels.size() % channelColumn == 0 ? channels.size() / channelColumn : channels.size() / channelColumn + 1;
    }
    for (int groupChannelColumn : groupChannelColumns) {
        if (groupChannelColumn > 0) {
            newAllChannelGroupsHeight += channelHeight * groupChannelColumn + (groupChannelColumn * 2 - 2) * horizontalSpacing;
        }
    }
    int changeHeight = newAllChannelGroupsHeight - allChannelGroupsHeight;
    if (changeHeight != 0) {
        allChannelGroupsHeight = newAllChannelGroupsHeight;
        ValueAnimator valueAnimator = ValueAnimator.ofInt(getMeasuredHeight(), getMeasuredHeight() + changeHeight);
        valueAnimator.setDuration(DURATION_TIME);
        valueAnimator.start();
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                animateHeight = (int) animation.getAnimatedValue();
                isAnimateChangeHeight = true;
                requestLayout();
            }
        });
        valueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isAnimateChangeHeight = false;
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
    }
}

/**
 * 受到行數(shù)所影響的view進(jìn)行上移或下移操作
 */
private void viewMove(int position, int offSetY) {
    for (int i = position; i < channelTitleGroups.size(); i++) {
        View view = channelTitleGroups.get(i);
        ChannelAttr tag = (ChannelAttr) view.getTag();
        tag.coordinate = new PointF(tag.coordinate.x, tag.coordinate.y + offSetY);
        view.animate().x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
    }
    for (int i = position; i < channelGroups.size(); i++) {
        ArrayList<View> otherChannels = channelGroups.get(i);
        for (int j = 0; j < otherChannels.size(); j++) {
            View view = otherChannels.get(j);
            ChannelAttr tag = (ChannelAttr) view.getTag();
            tag.coordinate = new PointF(tag.coordinate.x, tag.coordinate.y + offSetY);
            view.animate().x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
        }
    }
}

float downX, downY;
float moveX, moveY;

/**
 * 頻道拖動(dòng)
 */
private void channelDrag(View v, MotionEvent event) {
    moveX = event.getRawX();
    moveY = event.getRawY();
    v.setX(v.getX() + (moveX - downX));
    v.setY(v.getY() + (moveY - downY));
    downX = moveX;
    downY = moveY;
    ArrayList<View> myChannels = channelGroups.get(0);
    ChannelAttr vTag = (ChannelAttr) v.getTag();
    int vIndex = myChannels.indexOf(v);
    for (int i = 0; i < myChannels.size(); i++) {
        if (i > channelFixedToPosition && i != vIndex) {
            View iChannel = myChannels.get(i);
            ChannelAttr iChannelTag = (ChannelAttr) iChannel.getTag();
            int x1 = (int) iChannelTag.coordinate.x;
            int y1 = (int) iChannelTag.coordinate.y;
            int sqrt = (int) Math.sqrt((v.getX() - x1) * (v.getX() - x1) + (v.getY() - y1) * (v.getY() - y1));
            if (sqrt <= RANGE && !animatorSet.isRunning()) {
                animatorSet = new AnimatorSet();
                PointF tempPoint = iChannelTag.coordinate;
                ObjectAnimator[] objectAnimators = new ObjectAnimator[Math.abs(i - vIndex) * 2];
                if (i < vIndex) {
                    for (int j = i; j < vIndex; j++) {
                        TextView view = (TextView) myChannels.get(j);
                        ChannelAttr viewTag = (ChannelAttr) view.getTag();
                        ChannelAttr nextGridViewAttr = ((ChannelAttr) myChannels.get(j + 1).getTag());
                        viewTag.coordinate = nextGridViewAttr.coordinate;
                        objectAnimators[2 * (j - i)] = ObjectAnimator.ofFloat(view, "X", viewTag.coordinate.x);
                        objectAnimators[2 * (j - i) + 1] = ObjectAnimator.ofFloat(view, "Y", viewTag.coordinate.y);
                    }
                } else if (i > vIndex) {
                    for (int j = i; j > vIndex; j--) {
                        TextView view = (TextView) myChannels.get(j);
                        ChannelAttr viewTag = (ChannelAttr) view.getTag();
                        ChannelAttr preGridViewAttr = ((ChannelAttr) myChannels.get(j - 1).getTag());
                        viewTag.coordinate = preGridViewAttr.coordinate;
                        objectAnimators[2 * (j - vIndex - 1)] = ObjectAnimator.ofFloat(view, "X", viewTag.coordinate.x);
                        objectAnimators[2 * (j - vIndex - 1) + 1] = ObjectAnimator.ofFloat(view, "Y", viewTag.coordinate.y);
                    }
                }
                animatorSet.playTogether(objectAnimators);
                animatorSet.setDuration(DURATION_TIME);
                isAgainMeasure = false;
                animatorSet.start();
                vTag.coordinate = tempPoint;
                myChannels.remove(v);
                myChannels.add(i, v);
                break;
            }
        }
    }
}

/**
 * 頻道拖動(dòng)抬起
 *
 * @param v
 */
private void channelDragUp(View v) {
    isAgainMeasure = true;
    isChannelLongClick = false;
    ChannelAttr vTag = (ChannelAttr) v.getTag();
    v.animate().x(vTag.coordinate.x).y(vTag.coordinate.y).setDuration(DURATION_TIME);
    v.setBackgroundResource(channelSelectedBackground);
}

??上面的代碼主要有這幾個(gè)方法:
??forwardSort(View v, ArrayList<View> channels):讓被點(diǎn)擊頻道后面的所有頻道往前移動(dòng),不管是添加頻道還是刪除頻道,只要該頻道的后面還有其它頻道,那么它們一定要往前排移動(dòng)。具體做法就是先獲取該頻道所在的頻道組,然后遍歷被點(diǎn)擊頻道后面的所有頻道,改變它們的坐標(biāo),然后通過(guò)屬性動(dòng)畫(huà)v.animate().x().y()方法改變他們的位置;

??addMyChannel(final View v):點(diǎn)擊其他頻道增加我的頻道時(shí)觸發(fā)的方法,在之前會(huì)先調(diào)用forwardSort()方法,該方法主要是讓點(diǎn)擊的頻道做位移動(dòng)畫(huà),移動(dòng)到需要到達(dá)的位置,該位置坐標(biāo)之前是不確定的,所以需要通過(guò)計(jì)算得到;

??deleteMyChannel(View v):該方法的作用同上個(gè)方法類(lèi)似,點(diǎn)擊我的頻道時(shí)刪除該頻道的操作,要讓被刪除的頻道移動(dòng)到它所屬(也就是ChannelAttr的belong值)的頻道組的第一個(gè)位置?,F(xiàn)在要注意的是,這個(gè)方法和上面的方法會(huì)發(fā)生一個(gè)問(wèn)題,行數(shù)可能會(huì)改變,行數(shù)改變整個(gè)View的高度也會(huì)發(fā)生改變,所以我們需要下面這個(gè)方法來(lái)計(jì)算到底改變了多少,如何去改變它的高度;

??animateChangeGridViewHeight():這個(gè)就是通過(guò)動(dòng)畫(huà)改變整個(gè)View高度的方法,原理很簡(jiǎn)單,通過(guò)channelGroups集合得到每次增刪后的行數(shù)(所以該方法在addMyChannel()和deleteMyChannel()方法中都需要調(diào)用),和上一次增刪操作的行數(shù)比較得到行數(shù)差就可以了,然后通過(guò)ValueAnimator動(dòng)畫(huà)改變高度值,調(diào)用requestLayout()方法重新測(cè)量高度即可,在onMeasure()方法中我們已經(jīng)說(shuō)過(guò)了為什么會(huì)有兩個(gè)不同的setMeasuredDimension()方法;

??viewMove(int position, int offSetY):行數(shù)改變除了導(dǎo)致高度發(fā)生變化外,它底部的頻道組也會(huì)發(fā)生變化。比如如果該頻道組行數(shù)增加,那么它下面的所有頻道組包括頻道標(biāo)題也都需要往下移,如果該頻道組行數(shù)減少,那它下面的需要往上移。該方法接收一個(gè)頻道組所在位置參數(shù)和一個(gè)高度變化量的參數(shù),通過(guò)這兩個(gè)參數(shù)遍歷頻道組和頻道標(biāo)題組,讓它們所有的View位置發(fā)生改變;

??channelDrag(View v, MotionEvent event):頻道拖動(dòng)方法,當(dāng)該頻道在拖動(dòng)時(shí),我們判斷該頻道的位置和離它最近的一個(gè)頻道的距離,如果該距離小于我們定義的最小距離,那就讓該頻道插入到這個(gè)位置,它之前或者之后的頻道往后或者往前排列。要注意這個(gè)時(shí)候?qū)τ谠擃l道組的順序也要相應(yīng)調(diào)整,通過(guò)List中的add()和remove()方法實(shí)現(xiàn),還要注意他們的坐標(biāo)也發(fā)生了改變;

??channelDragUp(View v):這個(gè)方法是手抬起時(shí)觸發(fā)的方法,讓該頻道通過(guò)動(dòng)畫(huà)回到它應(yīng)該呆著的位置。

5.總結(jié)

??以上就是頻道選擇器的核心代碼,其他像接口的暴露、自定義屬性的實(shí)現(xiàn),在工程中都寫(xiě)的很詳細(xì)。總之,這篇文章是介紹我們實(shí)現(xiàn)這個(gè)自定義View的思路,大家看的時(shí)候不必完全盯著代碼細(xì)看,因?yàn)橐恍?shí)現(xiàn)細(xì)節(jié)可能并不是你想寫(xiě)的,一千個(gè)人眼中有一千個(gè)哈姆雷特,每個(gè)人的思路都是不同的,遇到問(wèn)題能想到一個(gè)具體的思路比實(shí)現(xiàn)上面那些代碼細(xì)節(jié)要高明的多。如果想看代碼細(xì)節(jié),那就看一些優(yōu)秀的開(kāi)源項(xiàng)目,那些開(kāi)源項(xiàng)目中出色的設(shè)計(jì)模式、優(yōu)雅的接口、完善的內(nèi)存管理才是我們應(yīng)該學(xué)習(xí)的。

項(xiàng)目鏈接:https://github.com/chengzhicao/ChannelView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容