Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義控件:實現半圓滾動菜單效果

自定義控件:實現半圓滾動菜單效果

編輯:關於Android編程

前言

本自定義控件參考自鴻洋大神的自定義控件,基於原來的控件效果進行修改,著重實現了以下效果:位置自動修正以及滑動結束的回調。我們先來看看效果圖:

控件展示
上面的圖片是一個ImageView,與控件無關,是為了驗證回調功能。接著是位置自動修正:

位置修正
位置自動修正的意思是說,每個item view經過滑動後,停留的位置不是隨意的,而是固定在某個區域之內,就如每個item view裝在一個個格子裡面。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="滑動結束回調" src="/uploadfile/Collfiles/20160730/20160730092538750.gif" title="\" />
而滑動結束的回調是說,當滑動結束後,滑動到中央的item view會觸發一次回調,用戶可以利用該回調來進行別的邏輯處理,與別的控件進行交互,比如:某個item view滑動到中央,觸發回調,讓別的TextView或者ImageView來具體顯示該item項的具體信息。

用法

只要在Activity中寫上如下幾行代碼即可:

SemicircleMenu mSemicircleMenu = (SemicircleMenu) findViewById(R.id.circlemenu);
    mSemicircleMenu.setMenuItemIconsAndTexts(mItemImgs, mItemTexts);
    mSemicircleMenu.setOnMenuItemClickListener(new SemicircleMenu.OnMenuItemClickListener() {
        @Override
        public void itemClick(View view, int pos) {
            Toast.makeText(MainActivity.this, mItemTexts[pos], Toast.LENGTH_SHORT).show();

        }
    });

    mSemicircleMenu.setOnCentralItemCallback(new SemicircleMenu.OnCentralItemCallback() {
        @Override
        public void centralItemOperate(int pos) {
            imageView.setImageResource(mItemImgs[pos]);
        }
    });

布局文件:


(注意:clickable應該為true。)

實現原理

其實關於測量、布局、甚至事件分發的實現原理在原文章都有很詳細的說明了,有興趣的讀者可以先閱讀原文,這裡會作簡要的說明,本文重點在於講述位置修正即滑動結束回調的實現,本文所有代碼均作了刪減,讀者可直接到GitHub處閱讀源碼。

Part 1 設置itemView的內容及加載itemView

/**
 * 每個Item之間相距的角度
 */
private float mAngleDelay;

/**
 *  設置菜單的文本信息
 */
public void setMenuItemIconsAndTexts(int[] resIds,String[] texts)
{
    mItemIcons = resIds;
    mItemTexts = texts;

    if(resIds == null && texts == null)
    {
        throw new IllegalArgumentException("菜單文本和圖片必須設置其一");
    }

    //初始化mMenuItemCount
    mMenuItemCount = resIds == null ? texts.length : resIds.length;

    if(resIds != null && texts != null)
    {
        mMenuItemCount = Math.min(resIds.length,texts.length);
    }
    //計算每個Item之間相差的度數,該值直接影響後面的布局、滑動
    mAngleDelay = 360 / mMenuItemCount;
    addMenuItems();
}

private void addMenuItems() {
    LayoutInflater mInflater = LayoutInflater.from(getContext());

    /**
     *  初始化item view
     */
    for(int i = 0; i < mMenuItemCount; i++)
    {
        final int j = i;
        View view = mInflater.inflate(R.layout.circle_menu_item,this,false);
        view.setTag(i);  //為每個item view打上Tag
        ImageView iv = (ImageView) view.findViewById(R.id.id_circle_menu_item_image);
        TextView tv = (TextView) view.findViewById(R.id.id_circle_menu_item_text);

       //...

        addView(view);
    }
}

從以上代碼來看,暴露了setMenuItemIconsAndTexts方法,用戶可以通過該方法為該控件設置不同的Item的圖像及其文本信息。接著根據設置item的數量,來計算每個item之間應相隔多少度,即mAngleDelay值,例如,如果是6個Item,那麼mAngleDelay值就是60度,以此類推。接著,在addMenuItem方法內,是不斷加載Item View,並且添加至當前ViewGroup內。(注意:形如R.id.id_circle_menu_item_image的id是定義在values文件夾下的id文件內的)。

Part 2 測量和布局

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
    //...

    //我們只需要半圓區域,因此把高度限制為一半
    setMeasuredDimension(resWidth,resHeight /2);

    mRadius = Math.max(getMeasuredWidth(),getMeasuredHeight());

    final  int count = getChildCount();
    int childSize = (int) (mRadius * DEFAULT_CHILD_DIMENSION);
    int childMode = MeasureSpec.EXACTLY;

    //遍歷所有子View,對其進行測量
    for(int i = 0; i < count;i++)
    {
        final  View child = getChildAt(i);
        if(child.getVisibility() == GONE)
        {
            continue;
        }

        int makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,childMode);
        child.measure(makeMeasureSpec,makeMeasureSpec);

    }

    mPadding = PADDING_LAYOUT * mRadius;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
    int layoutRadius = mRadius;

    final  int childCount = getChildCount();

    int left,top;
    //每個item view的寬度
    int cWidth = (int) (layoutRadius * DEFAULT_CHILD_DIMENSION);
    //坐標原點到item view中心的距離
    float tmp = layoutRadius / 2f - cWidth / 2 - mPadding;

    for(int i =0;i

在加載完item view後,我們便對所有item view進行測量、布局,其中布局流程中,我們根據mStartAngle這個角度來進行布局,通過計算三角關系把所有view的left、top坐標計算出來,然後布局,這樣每個Item view就一一形成了。順帶一說,這裡mStartAngle一開始等於90度,也就是說第一個選項出現的位置是正中央。在布局的最後,存在一個判斷語句,判斷我們在滑動結束後是否要進行回調,這個下面會詳細說明。

Part 3 事件分發

在初始化布局完畢後,一個半圓的菜單便顯示出來了,接下來我們需要對觸摸事件進行處理,以便能進行滑動。我們先看看代碼:

@Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
    float x = ev.getX();
    float y = ev.getY();


    switch (ev.getAction())
    {
        case MotionEvent.ACTION_DOWN:
            mLastX = x;
            mLastY = y;
            mDownTime = System.currentTimeMillis();
            mTmpAngle = 0;
            mTouchFlag = true;

            //如果按下的時候,正在自動滾動狀態,那麼取消滾動,並且進行位置矯正
            if(isFling)
            {
                removeCallbacks(mFlingRunnable);
                isFling = false;
                mCorrectPositionFlag = true;
                post(mFlingRunnable = new AutoFlingRunnable(getCorrectAngle(mAutoFlingAngle % mAngleDelay)));
                return true;
            }
            break;

        case MotionEvent.ACTION_MOVE:
            float start = getAngle(mLastX,mLastY);
            float end = getAngle(x,y);
            if(getQuadrant(x,y) == 4)
            {
                mStartAngle += end - start;
                mTmpAngle += end - start;
            }else{
                mStartAngle += start -end;
                mTmpAngle += start -end;
            }
            requestLayout();
            mLastX = x;
            mLastY = y;
            break;

        case MotionEvent.ACTION_UP:
            mTouchFlag = false;
            float anglePerSecond = mTmpAngle * 1000 / (System.currentTimeMillis() - mDownTime);

            //如果角速度超過規定的值,那麼認為是快速滾動,開啟快速滾動任務
            //否則,直接進行位置矯正
            if(Math.abs(anglePerSecond) >= mFlingableValue && !isFling)
            {
                mAutoFlingAngle = mTmpAngle;
                post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));
                return true;
            }else if(Math.abs(anglePerSecond) < mFlingableValue)
            {
                float mDeltaAngle = mTmpAngle % mAngleDelay ;
                if(mDeltaAngle != 0)
                {
                    post(mFlingRunnable = new AutoFlingRunnable(getCorrectAngle(mDeltaAngle)));
                    return true;
                }
            }

            // 如果當前旋轉角度超過NOCLICK_VALUE屏蔽點擊
            if (Math.abs(mTmpAngle) > NOCLICK_VALUE)
            {
                return true;
            }

            break;
    }
    return super.dispatchTouchEvent(ev);
}

大體思路是這樣的,當檢測到ACTION_DOWN事件時,記錄當前的觸摸坐標,接著在ACTION_MOVE事件中,不斷獲取實時的觸摸坐標,並計算角度,通過新的角度來累加到mStartAngle中,接著調用requestLayout()方法來重新布局,這樣便實現了該菜單跟隨手指而轉動的效果,這是最基礎的。但如果是快速滑動呢?那麼我們在ACTION_UP事件中,通過判斷滑動的距離與從觸摸到松開手指的時間的比值來判斷是否是快速滑動。如果是快速滑動,那麼啟動一個Runnable來完成快速滾動的事件,其實這也很簡單,在Runnable中不斷調用requestLayout()就可以實現快速滾動的效果了,這些在原文章都有詳細說明。

Part 4 位置自動修正

但是,由於隨手指滑動,或者快速滑動完畢後,其最後的滑動角度一般是一個隨機的數值,這樣就會造成item view出現在不應該出現的位置,比如正中央恰好沒有item view出現,都出現了一定的偏移,這樣對於菜單來說是非常不理想的,所以我們需要進行位置的矯正,使得每一個的滑動完成後,其Item View都能在正確的位置出現,而解決這個問題,我們可以從以下思路來解決:首先把總的滾動角度先算出來,那麼這個總的滾動角度便直接決定了滾動完畢後各item的位置,既然我們不能影響其滾動過程,那麼我們可以在滾動結束後,通過對總滾動角度進行一系列的判斷來對最後的位置進行調整,並再一次requestLayout,使得位置得以矯正。
那麼這個總的滾動角度是怎樣與位置矯正聯系起來的呢?在代碼裡面,總滾動角度用mTmpAngle或者mAutoFlingAngle來記錄,我們可以先用它對mAngleDelay(該值上面提及,表示每個Item之間相隔的角度)求余,這樣得出的結果是任一item的偏移量。舉個例子:item view有6個,那麼每個Item相隔60°,我們轉動了80°,那麼我們可以這樣分解:先轉了60°,此時每個Item的位置一定是正確的,再轉20°,那麼此時item就會留在不正確的位置了,我們所要做的就是對這個“20°”進行處理,那麼以30°為分割線,沒到30°的,讓itemview往回轉到正確的位置;如果超過了30°的,讓Itemview轉動到下一個位置,那麼我們的問題便得以解決了。
下面的方法是計算還需要多少角度才能轉到正確的位置的:

/**
 * 獲取位置矯正所需的角度
 * @param angle 對mAngleDelay求余後的角度
 * @return
 */
private float getCorrectAngle(float angle)
{
    if(angle > 0 && angle <= mAngleDelay/2)
    {
        mCorrectPositionFlag = true;
        return -angle;
    }else if(angle >mAngleDelay/2)
    {
        mCorrectPositionFlag = true;
        return (mAngleDelay -angle);
    }else if(angle < 0 && Math.abs(angle) <= mAngleDelay/2)
    {
        mCorrectPositionFlag = true;
        return -angle;
    }else if(angle < 0 && Math.abs(angle) > mAngleDelay/2){
        mCorrectPositionFlag = true;
        return -(mAngleDelay -Math.abs(angle));
    }
    return 0;
}

在獲取到需要修正的角度後,我們可以直接通過Runnable來重新布局一下,把該值作為需要轉動的角度即可。
如下所示:

private class AutoFlingRunnable implements Runnable
{
    //...
    public void run()
    {
        if(mCorrectPositionFlag)
        {
            float angle = angelPerSecond;
            mStartAngle += angle;
            requestLayout();
            mCorrectPositionFlag = false;
        }else {
            // 如果小於20,則停止,同時進行位置矯正
            if ((int) Math.abs(angelPerSecond) < 20) {
                isFling = false;
                mCorrectPositionFlag = true;
                this.angelPerSecond = getCorrectAngle(mAutoFlingAngle % mAngleDelay);
                postDelayed(this,30);
                return;
            }
            isFling = true;
            // 不斷改變mStartAngle,讓其滾動,/30為了避免滾動太快
            mStartAngle += (angelPerSecond / 30);
            mAutoFlingAngle += (angelPerSecond / 30);
            // 逐漸減小這個值
            angelPerSecond /= 1.0666F;
            postDelayed(this, 30);
            // 重新布局
            requestLayout();
        }
    }
}

Part 5 滑動結束的回調

在滑動結束並且位置修正完畢後,在中央會有一個item view,有時候我們需要對該Item view進行交互操作,比如上面的演示圖內,每滑動完畢,便把中央的item view的圖片顯示到上面ImageView中,那麼我們就需要在滑動完畢的時候,判斷出居於中央的item View到底是哪一個。以下是實現思路:首先通過一個方法findChildViewUnder來獲取某個坐標點上的itemView的實例,如下所示:

/**
* 獲取某個坐標上的子View
* @param x
* @param y
* @return View
*/
private View findChildViewUnder(float x,float y)
{
    final int count = getChildCount();
    for(int i = count - 1; i >= 0; i--)
    {
        final View child = getChildAt(i);
        if(x >= child.getLeft() && x <= child.getRight() && y>= child.getTop() && y <= child.getBottom())
            return child;
    }
    return null;
}

實現原理很簡單,就是遍歷所有的item View,來判斷給定的x、y坐標在哪個Item View之內,提取到item View的實例後,我們再拿出該itemView的Tag(因為加載itemView的時候,給每個View都打上了不同的Tag),有了Tag,就知道了是哪一個itemView滑動到了最中央的位置,最後再利用回調的實現方法,來實現交互式操作。
上面提到,在onLayout方法的最後有如下語句:

//布局結束的時候,如果不在滾動同時也不在被觸摸的時候,觸發滾動結束回調
 if(!isFling && !mTouchFlag ) { 
    mOnCentralItemCallback.centralItemOperate((Integer) findChildViewUnder(layoutRadius/2,tmp).getTag());
 }

其中,OnCentralItemCallback是一個接口,類似於監聽器接口,centralItemOperate是一個回調方法。在Activity中調用該方法能實時獲取到滑動結束後中央位置的itemView的類型。

GitHub地址:https://github.com/chenyua1995/SemicircleMenu
歡迎各位star和fork,謝謝閱讀!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved