仿網(wǎng)易新聞標(biāo)簽欄(動畫拖動變化標(biāo)簽位置)

網(wǎng)易新聞標(biāo)簽欄的實現(xiàn)效果我一直想實現(xiàn)試試,最近花了點時間終于實現(xiàn),初步實現(xiàn)效果如下,后面有時間還會繼續(xù)完善(最近突然發(fā)現(xiàn)其實可以通過RecyclerView實現(xiàn),但我這種實現(xiàn)方式也算是一種學(xué)習(xí)和練習(xí)吧)


效果

詳細Demo和源碼這里給出GitHub地址。
TabMoveLayout

實現(xiàn)功能

1.長按抖動
2.標(biāo)簽可隨意拖動,其他標(biāo)簽隨之變換位置
3.拖動變換子View順序

難點:

1.熟悉自定義ViewGroup過程,onMeasure、onLayout
2.ViewGroup事件處理
3.多種拖動情況考慮(位置移動計算)
4.ViewGroup中子View的變更替換添加

實現(xiàn)思路:

1.自定義ViewGroup,實現(xiàn)標(biāo)簽欄的排列,這里我以4列為例(onMeasure,onLayout)

2.實現(xiàn)觸摸標(biāo)簽的拖動,通過onTouch事件,在DOWN:獲取觸摸的x,y坐標(biāo),找到被觸摸的View,在MOVE:通過view.layout()方法動態(tài)改變View的位置

3.其他標(biāo)簽的位置變換,主要通過TranslateAnimation,在MOVE:找到拖動過程中經(jīng)過的View,并執(zhí)行相應(yīng)的Animation
(這里重點要考慮清楚所有拖動可能的情況)

4.拖動結(jié)束后,隨之變換ViewGroup中view的實際位置,通過removeViewAt和addView進行添加和刪除,中間遇到一點問題(博客)已分析。

關(guān)鍵代碼:

1.自定義ViewGroup

這里主要是onMeasure和onLayout方法。這里我要說一下我的布局方式

 /**
     * 標(biāo)簽個數(shù) 4
     * |Magin|View|Magin|Magin|View|Magin|Magin|View|Magin|Magin|View|Magin|
     * 總寬度:4*(標(biāo)簽寬度+2*margin)  按照比例 (總份數(shù)):4*(ITEM_WIDTH+2*MARGIN_WIDTH)
     * 則一個比例占的寬度為:組件總寬度/總份數(shù)
     * 一個標(biāo)簽的寬度為:組件寬度/總份數(shù) * ITEM_WIDTH(寬度占的比例)
     * 一個標(biāo)簽的MARGIN為:組件寬度/總份數(shù) * MARGIN_WIDTH(MARGIN占的比例)
     * 行高=(ITEN_HEIGHT+2*MARGIN_HEIGHT)*mItemScale
     * 一個組件占的寬度=(ITEM_WIDTH + 2*MARGIN_WIDTH)*mItemScale
     */

可能看起來比較復(fù)雜,其實理解起來就是:
一個標(biāo)簽所占的寬度=標(biāo)簽的寬度+2marginwidth
一個標(biāo)簽所占的高度=標(biāo)簽的高度+2
marginheight
這里都是用的權(quán)值計算的
一個比例占的長度為=總寬度/總份數(shù)
假如屏幕寬度為1000px,標(biāo)簽的寬度占10份,marginwidth占2份,標(biāo)簽的高度占5份,marginheight占1份
一個比例所占的長度(以一行4個標(biāo)簽為例) = 1000/((10+22)4)
一個標(biāo)簽所占的寬度 = (10+22)一個比例所占的長度
一個標(biāo)簽所占的高度 = (5+21)一個比例所占的長度

onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int width;
        int height;
        int childCount = getChildCount();
        if (modeWidth == MeasureSpec.EXACTLY) {
            width = sizeWidth;
        } else {
            width = Math.min(sizeWidth, getScreenWidth(mContext));
        }

        if (modeHeight == MeasureSpec.EXACTLY) {
            height = sizeHeight;
        } else {
            int rowNum = childCount / ITEM_NUM;
            if (childCount % ITEM_NUM != 0) {
                height = (int) Math.min(sizeHeight, (rowNum + 1) * (ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale);
            } else {
                height = (int) Math.min(sizeHeight, rowNum * (ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale);
            }
        }

        measureChildren(
                MeasureSpec.makeMeasureSpec((int) (mItemScale * ITEM_WIDTH), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec((int) (mItemScale * ITEM_HEIGHT), MeasureSpec.EXACTLY));
        setMeasuredDimension(width, height);
    }

這里也是自定義View常見的一個點,注意MeasureSpace的三種模式EXACITY,AT_MOST,UNSPECIFIED,三種模式的對應(yīng)關(guān)系可以簡單理解為:

EXACITY -> MATCH_PARENT或者具體值
AT_MOST -> WARP_CONTENT
UNSPECIFIED是未指定尺寸,這種情況不多,一般都是父控件是AdapterView,通過measure方法傳入的模式。

所以這里我處理方式為
寬度:當(dāng)EXACITY時:width = widthsize,當(dāng)其他模式時,width=sizewidth和屏幕寬度的較小值(這里注意sizeWidth的值為父組件傳給自己的寬度值,所以如果當(dāng)前組件處于第一層級,sizeWidth=屏幕寬度)
高度:當(dāng)EXACITY時:height = heightsize,當(dāng)其他模式時,計算行數(shù),height=行數(shù)一行的高度(height+2marginheight)
再執(zhí)行measureChildren

onLayout方法

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int left;
        int top;
        int right;
        int bottom;
        for (int i = 0; i < childCount; i++) {
            int row = i / ITEM_NUM;
            int column = i % ITEM_NUM;
            View child = getChildAt(i);
            left = (int) ((MARGIN_WIDTH + column * (ITEM_WIDTH + 2 * MARGIN_WIDTH)) * mItemScale);
            top = (int) ((MARGIN_HEIGHT + row * (ITEM_HEIGHT + 2 * MARGIN_HEIGHT)) * mItemScale);
            right = (int) (left + ITEM_WIDTH * mItemScale);
            bottom = (int) (top + ITEM_HEIGHT * mItemScale);
            child.layout(left, top, right, bottom);
        }

    }

所以onlayout也就比較好理解了,利用for循環(huán)遍歷child,計算每個child所在的行和列,再通過child.layout()布局。

2.onTouch事件

public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        if(isMove){
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mBeginX = x;
                    mBeginY = y;
                    mTouchIndex = findChildIndex(x, y);
                    mOldIndex = mTouchIndex;
                    if (mTouchIndex != -1) {
                        mTouchChildView = getChildAt(mTouchIndex);
                        mTouchChildView.clearAnimation();
                        //mTouchChildView.bringToFront();
                    }

                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mTouchIndex != -1 && mTouchChildView != null) {
                        moveTouchView(x, y);
                        //拖動過程中的View的index
                        int resultIndex = findChildIndex(x, y);
                        if (resultIndex != -1 && (resultIndex != mOldIndex)
                                && ((Math.abs(x - mBeginX) > mItemScale * 2 * MARGIN_WIDTH)
                                || (Math.abs(y - mBeginY) > mItemScale * 2 * MARGIN_HEIGHT))
                                ) {
                            beginAnimation(Math.min(mOldIndex, resultIndex)
                                    , Math.max(mOldIndex, resultIndex)
                                    , mOldIndex < resultIndex);
                            mOldIndex = resultIndex;
                            mOnHover = true;
                        }
                    }

                    break;
                case MotionEvent.ACTION_UP:
                    setTouchIndex(x, y);
                    mOnHover = false;
                    mTouchIndex = -1;
                    mTouchChildView = null;
                    return  true;
            }
        }
        return super.onTouchEvent(event);
    }

這個方法算是這個效果的主要方法了,詳細分析一下吧。首先看DOWN事件

case MotionEvent.ACTION_DOWN:
                    mBeginX = x;
                    mBeginY = y;
                    mTouchIndex = findChildIndex(x, y);
                    mOldIndex = mTouchIndex;
                    if (mTouchIndex != -1) {
                        mTouchChildView = getChildAt(mTouchIndex);
                        mTouchChildView.clearAnimation();
                        //mTouchChildView.bringToFront();
                    }

                    break;

可以看到,首先我先記錄了觸摸位置的x,y坐標(biāo),通過findChildIndex方法確定觸摸位置的child的index。

/**
     * 通過觸摸位置確定觸摸位置的View
     */
    private int findChildIndex(float x, float y) {
        int row = (int) (y / ((ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale));
        int column = (int) (x / ((ITEM_WIDTH + 2 * MARGIN_WIDTH) * mItemScale));
        int index = row * ITEM_NUM + column;
        if (index > getChildCount() - 1) {
            return -1;
        }
        return index;
    }

因為最初分析的時候已經(jīng)說到了
一行的高度 = 組件的高度+2marginheight
一列的寬度 = 組件的寬度+2
marginwidth
所以當(dāng)我們得到觸摸位置的x,y,就可以通過y/行高得到行數(shù),x/列寬
當(dāng)觸摸位置沒有child時返回-1。

得到觸摸坐標(biāo)后,獲得通過getChildAt()獲得觸摸坐標(biāo)的child,通過clearAnimation停止抖動。

MOVE事件:

case MotionEvent.ACTION_MOVE:
                    if (mTouchIndex != -1 && mTouchChildView != null) {
                        moveTouchView(x, y);
                        //拖動過程中的View的index
                        int resultIndex = findChildIndex(x, y);
                        if (resultIndex != -1 && (resultIndex != mOldIndex)
                                && ((Math.abs(x - mBeginX) > mItemScale * 2 * MARGIN_WIDTH)
                                || (Math.abs(y - mBeginY) > mItemScale * 2 * MARGIN_HEIGHT))
                                ) {
                            beginAnimation(Math.min(mOldIndex, resultIndex)
                                    , Math.max(mOldIndex, resultIndex)
                                    , mOldIndex < resultIndex);
                            mOldIndex = resultIndex;
                            mOnHover = true;
                        }
                    }

                    break;

首先根據(jù)move過程中的x,y,通過moveTouchView移動拖動的view隨手指移動。

    private void moveTouchView(float x, float y) {
        int left = (int) (x - mTouchChildView.getWidth() / 2);
        int top = (int) (y - mTouchChildView.getHeight() / 2);
        mTouchChildView.layout(left, top
                , (left + mTouchChildView.getWidth())
                , (top + mTouchChildView.getHeight()));
        mTouchChildView.invalidate();
    }

這里有個細節(jié),在移動的時候,將觸摸的位置移動到大概child的中心位置,這樣看起來正常一下,也就是我對x和y分別減去了child寬高的一半,不然會使得手指觸摸的位置一直在child的左上角(坐標(biāo)原點),看起來很變扭。最后通過layout和invalidate方法重繪child。

移動其他view

這個應(yīng)該算是這個組件最難實現(xiàn)的地方,我在這上面花了最長的時間。
1)首先什么時候執(zhí)行位移動畫,反過來想就是什么時候不執(zhí)行位移動畫
這里分了四種情況:
(1)拖動的位置沒有標(biāo)簽,也就是圖上的從標(biāo)簽9往右拖
(2)拖動的位置和上一次位置相同(也就是沒動)
(3)移動的位置不到一行的高度(也就是沒有脫離當(dāng)前標(biāo)簽的區(qū)域)
(4)移動的位置不到一列的寬度(也就是沒有脫離當(dāng)前標(biāo)簽的區(qū)域)

2)執(zhí)行位移動畫,下面會分析

3)mOldIndex = resultIndex這里是為了保存上一次移動的坐標(biāo)位置

4)mOnHover=true,記錄拖動不放的情況(和拖動就釋放的情況有區(qū)分)

/**
     * 移動動畫
     *
     * @param forward 拖動組件與經(jīng)過的index的前后順序 touchindex < resultindex
     *                true-拖動的組件在經(jīng)過的index前
     *                false-拖動的組件在經(jīng)過的index后
     */
    private void beginAnimation(int startIndex, int endIndex, final boolean forward) {
        TranslateAnimation animation;
        ViewHolder holder;
        List<TranslateAnimation> animList = new ArrayList<>();
        int startI = forward ? startIndex + 1 : startIndex;
        int endI = forward ? endIndex + 1 : endIndex;//for循環(huán)用的是<,取不到最后一個
        if (mOnHover) {//拖動沒有釋放情況
            if (mTouchIndex > startIndex) {
                if (mTouchIndex < endIndex) {
                    startI = startIndex;
                    endI = endIndex + 1;
                } else {
                    startI = startIndex;
                    endI = endIndex;
                }
            } else {
                startI = startIndex + 1;
                endI = endIndex + 1;
            }
        }

        //X軸的單位移動距離
        final float moveX = (ITEM_WIDTH + 2 * MARGIN_WIDTH) * mItemScale;
        //y軸的單位移動距離
        final float moveY = (ITEM_HEIGHT + 2 * MARGIN_HEIGHT) * mItemScale;
        //x軸移動方向
        final int directX = forward ? -1 : 1;
        final int directY = forward ? 1 : -1;
        boolean isMoveY = false;
        for (int i = startI; i < endI; i++) {
            if (i == mTouchIndex) {
                continue;
            }
            final View child = getChildAt(i);
            holder = (ViewHolder) child.getTag();
            child.clearAnimation();
            if (i % ITEM_NUM == (ITEM_NUM - 1) && !forward
                    && holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {
                //下移
                holder.row++;
                isMoveY = true;
                animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);
            } else if (i % ITEM_NUM == 0 && forward
                    && holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {
                //上移
                holder.row--;
                isMoveY = true;
                animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);
            } else if (mOnHover && holder.row < i / ITEM_NUM) {
                //onHover 下移
                holder.row++;
                isMoveY = true;
                animation = new TranslateAnimation(0, -(ITEM_NUM - 1) * moveX, 0, moveY);
            } else if (mOnHover && holder.row > i / ITEM_NUM) {
                //onHover 上移
                holder.row--;
                isMoveY = true;
                animation = new TranslateAnimation(0, (ITEM_NUM - 1) * moveX, 0, -moveY);
            } else {//y軸不動,僅x軸移動
                holder.column += directX;
                isMoveY = false;
                animation = new TranslateAnimation(0, directX * moveX, 0, 0);
            }
            animation.setDuration(mDuration);
            animation.setFillAfter(true);
            final boolean finalIsMoveY = isMoveY;
            animation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    child.clearAnimation();
                    if (finalIsMoveY) {
                        child.offsetLeftAndRight((int) (directY * (ITEM_NUM - 1) * moveX));
                        child.offsetTopAndBottom((int) (directX * moveY));
                    } else {
                        child.offsetLeftAndRight((int) (directX * moveX));
                    }
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            child.setAnimation(animation);
            animList.add(animation);
        }
        for (TranslateAnimation anim : animList) {
            anim.startNow();
        }


    }

位移動畫,這段代碼怎么解釋哪...我寫的時候是發(fā)現(xiàn)一個bug改一種情況,最后實現(xiàn)了這段代碼。


初步效果

1)這里首先確定開始位移的view的坐標(biāo)和結(jié)束位移的坐標(biāo)
這里分為兩種情況:
case1:手指拖動后抬起(down->move->up);
case2:手指來回拖動不放(down->move->move)

case1:是常見情況,這里我們就可以按照forward再分為兩種情況
case1.1:標(biāo)簽0->標(biāo)簽1(forward =true);
case1.2:標(biāo)簽5->標(biāo)簽1(forward=false)

case1.1:
標(biāo)簽0移動到標(biāo)簽1,標(biāo)簽0隨手指移動,所以需要執(zhí)行位移動畫的只有標(biāo)簽1,所以startI = 1,endI = 2(for循環(huán)<,所以取不到最后一個),而startindex = 0,endindex = 1;
所以forward = true,startI = startIndex+1,endI=endIndex+1;
case1.2:
標(biāo)簽4移動到標(biāo)簽0,標(biāo)簽4隨手指移動,所以需要執(zhí)行位移動畫的是標(biāo)簽0~標(biāo)簽3,所以startI=0,endI=4,所以而startindex=0,endindex=5;
所以forward = false,startI = startIndex,endI = endIndex

case2:是指手指拖動不放,來回拖動,所以通過mOnHover=true參數(shù)來確定是否是拖動沒放情況,這里面又要細分為三種情況
case2.1:標(biāo)簽0->標(biāo)簽2->標(biāo)簽1,將標(biāo)簽0拖動到2,再回到0的位置,這是標(biāo)簽0一直隨手指移動,
后面這段動畫,startindex = 1,endindex = 2,touchindex = 0,只有標(biāo)簽2需要執(zhí)行動畫,標(biāo)簽1不動,所以startI = 2,endI = 3
所以mOnHover = true,touchindex<starindex,startI = startIndex + 1;endI = endindex+1;

case2.2:標(biāo)簽8->標(biāo)簽4->標(biāo)簽5,將標(biāo)簽8拖到4,在拖到5的位置,后面這段動畫,startindex = 4,endindex = 5,touchindex = 8,只有標(biāo)簽4需要執(zhí)行動畫,其他標(biāo)簽不動,所以startI=4,endI=5
所以mOnHover = true,startIndex<endIndex<touchindex,startI=startIndex,endI=endIndex

case2.3:標(biāo)簽8->標(biāo)簽4->標(biāo)簽5->標(biāo)簽9,后面這段動畫,startindex = 5,endindex = 9,touchindex = 8,執(zhí)行動畫的有標(biāo)簽5~標(biāo)簽9,所以startI=5,endI=10
所以mOnHover=true.startIndex<touchindex<endIndex,startI=startIndex,endI=endIndex+1

接下來就是for循環(huán)的移動動畫,可以看到這里可以大致分為三種情況
case1:X軸的平移動畫,Y軸不動;
case2:Y軸上移一行,X軸左移三個(也就是一行的第一個上移到上一行的最后一個);
case3:Y軸下移一行,X軸右移三個(也就是一行的最后一個下移到下一行的第一個);
可以看到我還是總體分為了mOnHover=false和mOnHover=true兩種情況,我在初始化時,將每個child的所在行和列以Tag的形式保存到了child中

if (i % ITEM_NUM == (ITEM_NUM - 1) && !forward
                    && holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {
                //下移
                holder.row++;
                isMoveY = true;
                animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);
            } else if (i % ITEM_NUM == 0 && forward
                    && holder.row == i / ITEM_NUM && holder.column == i % ITEM_NUM) {
                //上移
                holder.row--;
                isMoveY = true;
                animation = new TranslateAnimation(0, directY * (ITEM_NUM - 1) * moveX, 0, directX * moveY);
            } else if (mOnHover && holder.row < i / ITEM_NUM) {
                //onHover 下移
                holder.row++;
                isMoveY = true;
                animation = new TranslateAnimation(0, -(ITEM_NUM - 1) * moveX, 0, moveY);
            } else if (mOnHover && holder.row > i / ITEM_NUM) {
                //onHover 上移
                holder.row--;
                isMoveY = true;
                animation = new TranslateAnimation(0, (ITEM_NUM - 1) * moveX, 0, -moveY);
            } else {//y軸不動,僅x軸移動
                holder.column += directX;
                isMoveY = false;
                animation = new TranslateAnimation(0, directX * moveX, 0, 0);
            }

case1:當(dāng)是一行的最后一個,forward=false(后面的標(biāo)簽往前擠),標(biāo)簽的Tag中的x,y沒有變化(也就是第一次拖動和mOnHover=true區(qū)分),這時下移
case2:當(dāng)是一行的第一個,forward=true(上面的標(biāo)簽往下擠),標(biāo)簽的Tag中的x,y沒有變化(也就是第一次拖動和mOnHover=true區(qū)分),這時上移
case3:當(dāng)mOnHover=true,標(biāo)簽當(dāng)前所在行<標(biāo)簽初始所在行,這時下移
case4:當(dāng)mOnHover=true,標(biāo)簽當(dāng)前所在行>標(biāo)簽初始所在行,這時上移
case5:X軸的平移,y軸不動

后面設(shè)置了child的動畫監(jiān)聽,當(dāng)動畫結(jié)束后,需要將child的實際位置設(shè)置為當(dāng)前位置(因為這里用的不是屬性動畫,所以執(zhí)行動畫后child的實際位置并沒有變化,還是原始位置)

UP事件:

case MotionEvent.ACTION_UP:
                    setTouchIndex(x, y);
                    mOnHover = false;
                    mTouchIndex = -1;
                    mTouchChildView = null;
                    return  true;

這里主要看setTouchIndex事件

/**
     * ---up事件觸發(fā)
     * 設(shè)置拖動的View的位置
     * @param x
     * @param y
     */
    private void setTouchIndex(float x,float y){
        if(mTouchChildView!= null){
            int resultIndex = findChildIndex(x, y);
            Log.e("resultindex", "" + resultIndex);
            if(resultIndex == mTouchIndex||resultIndex == -1){
                refreshView(mTouchIndex);
            }else{
                swapView(mTouchIndex, resultIndex);
            }
        }
    }

可以看到,這里拖動結(jié)束后就需要將拖動位置變化的child實際改變它在ViewGroup中的位置
這里有兩種情況
case1:拖動到最后,child的順序沒有改變,只有touchview小浮動的位置變化,這時只需要刷新touchview即可
case2:將位置變換的child刷新其在viewgroup中的順序。

/**
     *刷新View
     * ------------------------------重要------------------------------
     * 移除前需要先移除View的動畫效果,不然無法移除,可看源碼
     */
    private void refreshView(int index) {
        //移除原來的View
        getChildAt(index).clearAnimation();
        removeViewAt(index);
        //添加一個View
        TextView tv = new TextView(mContext);
        LayoutParams params = new ViewGroup.LayoutParams((int) (mItemScale * ITEM_WIDTH),
                (int) (mItemScale * ITEM_HEIGHT));
        tv.setText(mData.get(index));
        tv.setTextColor(TEXT_COLOR);
        tv.setBackgroundResource(ITEM_BACKGROUND);
        tv.setGravity(Gravity.CENTER);
        tv.setTextSize(TypedValue.COMPLEX_UNIT_PX,TEXT_SIZE);
        tv.setTag(new ViewHolder(index / ITEM_NUM, index % ITEM_NUM));
        this.addView(tv,index ,params);
        tv.startAnimation(mSnake);
    }

刷新index的View,這里有個需要注意的點,因為每個child都在執(zhí)行抖動動畫,這時候直接removeViewAt是沒有辦法起效果的,需要先clearAnimation再執(zhí)行,具體我已經(jīng)寫了一篇博客從源碼分析了
Animation導(dǎo)致removeView無效(源碼分析)

 private void swapView(int fromIndex, int toIndex) {
        if(fromIndex < toIndex){
            mData.add(toIndex+1,mData.get(fromIndex));
            mData.remove(fromIndex);
        }else{
            mData.add(toIndex,mData.get(fromIndex));
            mData.remove(fromIndex+1);
        }

        for (int i = Math.min(fromIndex, toIndex); i <= Math.max(fromIndex, toIndex); i++) {
            refreshView(i);
        }
    }

這里交換touch和最終位置的child,所以首先實際改變Data數(shù)據(jù)集,再利用for循環(huán),通過refreshView函數(shù),刷新位置變化的child。

實現(xiàn)過程比較坎坷,但也是一種實現(xiàn)思路供大家參考吧,寫完后對于自定義ViewGroup和動畫能有很好的學(xué)習(xí),這里再放一下Github地址TabMoveLayout,喜歡的可以star一下~~~

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

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

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