自定義View之案列篇(二):扇形菜單

oqba6121 8年前發布 | 8K 次閱讀 安卓開發 Android開發 移動開發

今天帶給大家的是案例的第二篇(扇形菜單),先來啾啾它的容貌

sec

手指不斷的點擊,動畫來回收縮:

sec

對上面這個效果,大家一點不會陌生。下面一起來看看它的具體實現。

一、SectorLayout(扇形菜單)

我們首先來分析分析效果圖,展開時的動畫:

  • 縮放 0 到 1

  • 透明度 0 到 1

  • 旋轉 0° 到 360°

  • 半徑不斷增長,某角度的平移

收起菜單的動畫跟展開的動畫恰恰相反。

先來看看平移的動畫,看看下面的分析圖:

sec

我們這里利用三角函數的知識,很容易得到 B 點的坐標,那么 a 的角度為 90° / (childCount - 1) ,唯一變化的是 r 的值,那么它的區間取值為 [0,getHeight() - getChildAt(0).getWidth()] 。

縮放,旋轉,透明度的動畫處理比較簡單,這里就不再細講。文章的結尾處我會附上源碼下載地址。

1、onMeasure()

ViewGroup 的繪制流程 onMeasure() -> onLayout() -> onDraw() 。由于我們不會對視圖做任何繪制,所以 onDraw 方法這里就不做討論。先來理理 onMeasure 測量規格(計算尺寸)的方法, ViewGroup 測量規格包含了【測量自身】與【測量子視圖】。

【測量自身】:

int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        //處理 wrap_content問題
        int defaultDimension = dip2px(200);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(defaultDimension, defaultDimension);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(defaultDimension, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, defaultDimension);
        }

如果不處理 MeasureSpec.AT_MOST 模式下的情況,默認占據剩余空間的大小,那么當你設置屬性 wrap_content 的效果跟 match_parent 的效果是一樣的。如果包含 wrap_content 我們讓它占據 200dp 的一個大小,當然這個值可以根據你的需求而定。動態設置也是可以的。

注意了,本篇是沒有處理 padding 屬性的,如果你感興趣可以嘗試下。

【測量子視圖】:

private void measureChildViews() {
        final int childCount = getChildCount();
        final int childMode = MeasureSpec.EXACTLY;
        final int childSize = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 6;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE) {
                continue;
            }
            int measureSpec = -1;
            measureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode);
            childView.measure(measureSpec, measureSpec);
        }
    }

子視圖的測量模式為 MeasureSpec.EXACTLY 確定模式,如果你在布局文件 xml 中新增子視圖,那么你可能需要改寫這里的測量模式;子視圖的測量大小為 【寬高的最小值 / 常數】,這里的常數你可以根據你的需求修改。 測量模式與測量大小決定測試規格

MeasureSpec.makeMeasureSpec(childSize, childMode);
childView.measure(measureSpec, measureSpec);

完成子視圖的一個測量。

onLayout()

ViewGroup 的 onLayout 同樣也是包含兩個部分 【計算自身位置】 與 【計算子視圖位置】。【計算自身位置】是根據你當前的布局文件而定,所以這里也不對其進行處理。主要分析【計算子視圖位置】的方法如下:

childView.layout(int l, int t, int r, int b)

你可以這樣來理解 4 個參數的含義: 左上角與右下角對角線兩點的坐標。

sec

根據分析圖就可以很容易的得到 childView.layout 的各個參數:

childView.layout(
           //w-r*sina-(A,B小球的半徑之和)
           width - (int) (mRadius * Math.sin(Math.toRadians(angle * i))) - childSize,
           height - (int) (mRadius * Math.cos(Math.toRadians(angle * i))) - childSize,
           width - (int) (mRadius * Math.sin(Math.toRadians(angle * i))),
           height - (int) (mRadius * Math.cos(Math.toRadians(angle * i))));

處理旋轉,縮放已經透明度:

childView.setAlpha(mAlpha);
    childView.setScaleX(mScale);
    childView.setScaleY(mScale);
    childView.setRotation(mRotation);

講到這里繪制流程差不多就完了,接著來看看動畫的設計。

動畫設計

相信大家已經看出來了,這里使用的是屬性動畫 property animation ,上文已經分析了主要是【展開動畫】,【收起動畫】。

它們都比較簡單:

【展開動畫】:

private ValueAnimator initAnimator(final boolean openAnimatorEnable) {
        ValueAnimator animator = null;
        if (getChildAt(0) == null) {
            return animator;
        }

        animator = ValueAnimator.ofFloat(0f, 1.0f);
        animator.setInterpolator(new BounceInterpolator());
        animator.setDuration(2000);
        animator.addUpdateListener(new MyAnimatorUpdateListener(this) {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mRadius = (int) ((float) valueAnimator.getAnimatedValue() * (getHeight() - getChildAt(0).getWidth()));
                mAlpha = (float) valueAnimator.getAnimatedValue();
                mScale = (float) valueAnimator.getAnimatedValue();
                mRotation = (float) valueAnimator.getAnimatedValue() * ANGLE_360;
                requestLayout();
            }
        });
        return animator;
    }

當我這里處理動畫,連續點擊菜單的展開和收起比較突兀,比較不理想。我想實現的效果是:當動畫正在展開并有沒有達到最高點,我再次點擊,動畫會以當前點收起動畫;當動畫正在收起并沒有達到最低點,我再次點擊,動畫會以當前點展開動畫。做到一個連貫的效果,我是這樣處理的:

if (openAnimatorEnable) {
        mStartValue = 0f;
        mEndValue = (float) valueAnimator.getAnimatedValue();
    } else {
        mEndValue = 1.0f;
        mStartValue = (float) valueAnimator.getAnimatedValue();
    }

來保存它的一個當前值。最后來刷新從繪 requestLayout(); 。

為了能夠更好的擴展,我這里加入了適配的方式來新增子視圖。

private void buildItems() {
        for (int i = 0; i < mAdapter.getCount(); i++) {
            final View itemView = mAdapter.getView(i, null, this);
            addView(itemView);
        }
    }

二、使用方式

這里只需要注意一點:

@Override
    public View getView(final int position, View convertView, final ViewGroup parent) {
        holder holder = null;
        if (convertView == null) {
            holder = new holder();
            convertView = View.inflate(MainActivity.this, R.layout.activity_item, null);
            holder.mImage = (ImageView) convertView.findViewById(R.id.iv);
            convertView.setTag(holder);
        } else {
            holder = (holder) convertView.getTag();
        }
        holder.mImage.setBackgroundResource(mImages[position]);
        if (position != (getCount() - 1)) {
            holder.mImage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(MainActivity.this, "" + position, Toast.LENGTH_SHORT).show();
                }
            });
        }
        return convertView;
    }

if (position != (getCount() - 1)) 不要處理最后一個子視圖的點擊事件。因為最后一個子視圖固定在了右下角且點擊事件已經在 SectorLayout 中處理了。如果不加這句,你會發現點擊并沒有效果展示。

最后說一點,屬性動畫在監聽回調的方法當中可能持有外部類的一個引用,說白了就是持有 activity 的引用,導致 activity 不能被回收,造成內存泄漏。處理的方式是采用匿名的內部類結合弱引用處理。

 

 

 本文由用戶 oqba6121 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!