Androidカスタム円形メニュー


クールでスタイリッシュなUI効果は、開発者一人一人にとって非常に魅力的です.
上の図は雛形で、文字盤、回転盤、円形メニューに拡張できます.下図は拡張円形メニューです.録画ツールがはっきりしていないので、UIの特効効果は本当にいいです.興味があれば、後ろを見てください.もちろんインスピレーションは上図に由来します.
タイトルはカスタマイズされた丸いメニューで、私は主に丸いメニューの開発の流れを説明して、もし上の図に興味があるならば、伝言を残してください.コードは次の通りです.
public class CircleMenuLayout extends ViewGroup {
    //    
    private int mRadius;
    //    
    private double mStartAngle = 0;
    //padding       0
    private float mPadding = 0;
    //    item   
    private int offsetRotation = 0;
    //      
    private long lastTouchTime;
    //              false
    boolean isRange = false;
    //     x,y 
    float x = 0, y = 0;
    //             
    boolean isLeft = false;
    //  
    private ListAdapter mAdapter;
    //          0
    private float speed = 0;
    //   item      
    private static final float ITEM_DIMENSION = 1 / 3f;
    //    
    private static final int ROTATION_DEGREE = 3;
    //distanceFromCenter Item      
    private static final float DISTANCE_FROM_CENTER = 2 / 3f;
    //speed attenuation     
    private static final int SPEED_ATTENUATION = 1;
    //       
    private static final int ANGLE = 6;
    //  
    private static final int EMPTY_MESSAGE = 1;
    // MenuItem       
    private OnItemClickListener mOnMenuItemClickListener;
    //     item   
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == EMPTY_MESSAGE) {
                if (speed > 0) {
                    if (isLeft) {
                        //    
                        offsetRotation -= ANGLE;
                    } else {
                        offsetRotation += ANGLE;
                    }
                    //    
                    speed -= SPEED_ATTENUATION;
                    invalidate();
                    handler.sendEmptyMessageDelayed(EMPTY_MESSAGE, 50);
                }
            }
        }
    };

    /** * @param context * @param attrs */
    public CircleMenuLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setPadding(0, 0, 0, 0);
    }

    /** * @param context */
    public CircleMenuLayout(Context context) {
        super(context);
        setPadding(0, 0, 0, 0);
    }

    public void setAdapter(ListAdapter mAdapter) {
        this.mAdapter = mAdapter;
    }

    //      
    private void buildMenuItems() {
        //          ,   menu item
        for (int i = 0; i < mAdapter.getCount(); i++) {
            final View itemView = mAdapter.getView(i, null, this);
            final int position = i;
            if (itemView != null) {
                itemView.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mOnMenuItemClickListener != null) {
                            mOnMenuItemClickListener.onItemClickListener(itemView, position);
                        }
                    }
                });
            }
            //   view    
            addView(itemView);
        }
    }

    //    
    @Override
    protected void onAttachedToWindow() {
        if (mAdapter != null) {
            buildMenuItems();
        }
        super.onAttachedToWindow();
    }

    //       ,   menu item  
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //       
        measureMyself(widthMeasureSpec, heightMeasureSpec);
        //        
        measureChildViews();
    }

    private void measureMyself(int widthMeasureSpec, int heightMeasureSpec) {
        int resWidth = 0;
        int resHeight = 0;
        //        ,            
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //                
        if (widthMode != MeasureSpec.EXACTLY
                || heightMode != MeasureSpec.EXACTLY) {
            //            
            resWidth = getSuggestedMinimumWidth();
            //          ,            
            resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;

            resHeight = getSuggestedMinimumHeight();
            //          ,            
            resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;
        } else {
            //          ,      ;
            resWidth = resHeight = Math.min(width, height);
        }
        setMeasuredDimension(resWidth, resHeight);
    }

    private void measureChildViews() {
        //     
        mRadius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2;
        // menu item  
        final int count = getChildCount();
        // menu item  
        int childSize = (int) (mRadius * ITEM_DIMENSION);
        // menu item    
        int childMode = MeasureSpec.EXACTLY;
        //     
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            //   menu item   ;         ,  item    
            int makeMeasureSpec = -1;
            makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
                    childMode);
            child.measure(makeMeasureSpec, makeMeasureSpec);
        }
    }

    //   menu item   
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        refresh();
    }

    //         item
    public void refresh() {
        final int childCount = getChildCount();
        //   menu item   ,  item        
        float angleDelay = 360 / childCount;
        //               
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }
            int x = (int) Math.round(Math.sin(Math.toRadians(angleDelay * (i + 1) - offsetRotation)) * (mRadius * DISTANCE_FROM_CENTER));
            int y = (int) Math.round(Math.cos(Math.toRadians(angleDelay * (i + 1) - offsetRotation)) * (mRadius * DISTANCE_FROM_CENTER));
            //  item              
            if (x <= 0 && y >= 0) {
                x = mRadius - Math.abs(x);
                y = mRadius - y;
            } else if (x <= 0 && y <= 0) {
                y = mRadius + Math.abs(y);
                x = mRadius - Math.abs(x);
            } else if (x >= 0 && y <= 0) {
                y = mRadius + Math.abs(y);
                x = mRadius + x;
            } else if (x >= 0 && y >= 0) {
                x = mRadius + x;
                y = mRadius - Math.abs(y);
            }
            //  item                 
            x = x - (int) (mRadius * ITEM_DIMENSION) / 2;
            y = y - (int) (mRadius * ITEM_DIMENSION) / 2;
            //   child view
            child.layout(x, y,
                    x + (int) (mRadius * ITEM_DIMENSION), y + (int) (mRadius * ITEM_DIMENSION));
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //                
                x = event.getX();
                y = event.getY();
                //          
                int error = 10;
                if ((x - mRadius) * (x - mRadius) + (y - mRadius) * (y - mRadius) < (mRadius + error) * (mRadius + error)) {
                    isRange = true;
                    lastTouchTime = System.currentTimeMillis();
                } else {
                    isRange = false;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                float x1 = event.getX();
                float y1 = event.getY();
                if (isRange) {
                    long timeStamp = System.currentTimeMillis() - lastTouchTime;
                    float distance = (float) Math.sqrt((x1 - x) * (x1 - x) + (y1 - y) * (y1 - y));
                    float speed = distance / timeStamp;
                    if (x1 - x > 0) {
                        isLeft = false;
                    } else {
                        isLeft = true;
                    }
                    //    
                    speed(speed);
                }
                break;
        }
        return true;
    }

    public void speed(float speed) {
        this.speed = speed * ROTATION_DEGREE;
        handler.sendEmptyMessage(EMPTY_MESSAGE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.parseColor("#dddddd"));
        canvas.drawCircle(mRadius, mRadius, mRadius, paint);
        refresh();
    }

    //      
    public interface OnItemClickListener {
        public void onItemClickListener(View v, int position);
    }

    //   MenuItem       
    public void setOnItemClickListener(OnItemClickListener listener) {
        this.mOnMenuItemClickListener = listener;
    }

    /** *      layout    * * @return */
    private int getDefaultWidth() {
        WindowManager wm = (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
    }
}

ここを見て、多くの人が気絶したと信じています.じゃ、一緒に片付けましょう.
せっけい構想
1、汎用モード
上の図は画像に文字を加えて、もし私がボタンに変えたいならば、あるいは私は画像だけが必要です.ここではカスタマイズが必要です.どうすればいいのか、私はアダプティブモードを採用しました.みんなListViewの使い方を覚えています.私もここで参考にしました.
    public void setAdapter(ListAdapter mAdapter) {
        this.mAdapter = mAdapter;
    }

これによりMenuの高度なカスタマイズが可能になります.
2、構築メニュー項目
コードはbuildMenuItems()を参照し、mAdapterに対してサブViewを遍歴し、クリックイベントを追加し、addView()を呼び出してView Groupに追加し、このときシステムはonMeasure()を呼び出してサブViewに対してサイズを計算します.
3、itemサイズの計算
コードはmeasureMyself()とmeasureChildViews()を参照し、各itemのサイズを決定します.
4、itemレイアウト
まずitem(x,y)の円心からの長さを計算し、スケッチを描きました.
 int x = (int) Math.round(Math.sin(Math.toRadians(a)) * temp);
 int y = (int) Math.round(Math.cos(Math.toRadians(a)) * temp);

temp半径の3分の2に値を付けます.もちろん、満足のいく長さに変更することができます.
次に、(x,y)の座標を計算するには、次のようにします.
            if (x <= 0 && y >= 0) {  //    
                x = mRadius - Math.abs(x);
                y = mRadius - y;
            } else if (x <= 0 && y <= 0) {//    
                y = mRadius + Math.abs(y);
                x = mRadius - Math.abs(x);
            } else if (x >= 0 && y <= 0) {//    
                y = mRadius + Math.abs(y);
                x = mRadius + x;
            } else if (x >= 0 && y >= 0) {//    
                x = mRadius + x;
                y = mRadius - Math.abs(y);
            }

ここまで計算すると、(x,y)座標でメニュー項目のleftとtop位置を表すと、item全体が親コントロールに対して右下にずれていることがわかります.オフセットの問題を解決するために、itemコントロールの中心点を使用して、メニュー項目のleftとtop位置を表します.
 x = x - item   / 2;
 y = y - item    / 2;

最後にlayout()メソッドを呼び出し、itemの位置を決定します.
5、ジェスチャー回転
上には静的なMenuが完成していますが、シャドウ部分をスライドさせることでMenuを回転させるにはどうすればいいのでしょうか.
onTouchEvent()メソッドを書き換え、戻り値をtrueに変更する必要があります.ジェスチャーを処理して(ACTION_DOWN)を押し、(ACTION_UP)の状態を上げます.
まず,指の押下がシャドウの局所内にあるかどうかを判断する.注意指を押すのは指先がスクリーンに局所的に接触しているので、少しの誤差ではありません.
x = event.getX();
y = event.getY();
if ((x -   x) * (x -   x) + (y -   y) * (y -   y) < (  x+   ) * (  y+   )) {
                    isRange = true;
                }

それから私たちは運動の速度を計算しなければなりません.私の最初の考えは重力で加速することです.同僚の賈さんに感謝しています.彼は私にもっと良い意見をくれました.速度=距離/時間です.
ACTION_DOWN:
 lastTouchTime = System.currentTimeMillis();

ACTION_UP:
long timeStamp = System.currentTimeMillis() - lastTouchTime;
float distance = (float) Math.sqrt((x1 - x) * (x1 - x) + (y1 - y) * (y1 - y));
 float speed = distance / timeStamp;

次に,指で押したxの座標と,xの座標を持ち上げることで,ユーザが左に滑るか,右に滑るかを判断する.
          if (x1 - x > 0) {
                        isLeft = false;
                    } else {
                        isLeft = true;
                    }

最後にhandlerによって各運動の角度を変え、Menuを自然に回転させた.
             if (isLeft) {
                        //    
                        offsetRotation -= ANGLE;
                    } else {
                        //    
                        offsetRotation += ANGLE;
                    }
                    //    
                    speed -= SPEED_ATTENUATION;
                    invalidate();//  
                    handler.sendEmptyMessageDelayed(EMPTY_MESSAGE, 50);

使用
1、xmlレイアウト
    <com.github.ws.viewdemo.widget.CircleMenuLayout
        android:id="@+id/cm"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#f0f0f0">

    </com.github.ws.viewdemo.widget.CircleMenuLayout>

2、classファイル
        circleMenuLayout.setAdapter(new MyAdapter());

        circleMenuLayout.setOnItemClickListener(new CircleMenuLayout.OnItemClickListener() {
            @Override
            public void onItemClickListener(View v, int position) {
                Toast.makeText(MainActivity.this, mList.get(position).text + "", Toast.LENGTH_SHORT).show();
            }
        });

ソースはgithubにアップロードしました.アドレスhttps://github.com/HpWens/ViewDemoと、改めてご注目ありがとうございました.