AndroidカスタムViewシリーズ(2)--2 Kゲームのようなロッカーを作る

33788 ワード

執筆理由:Androidの進級過程で避けられない話題がある--カスタムView.これは、アンドロイドプログラマーが機能の自主化をよりよく実現するために踏み出さなければならない一歩です.以下のシリーズのブロガーは、いくつかの例を実装することによって、アンドロイドがViewをカスタマイズする方法を認識する.カスタマイズされたViewからカスタマイズされたView Groupまで、Viewイベント処理からViewの詳細分析まで(この章ではレベルが低い場合は後で補足する可能性があります)、Canvasの使用、アニメーションなど、小さな知識が含まれます.これはこのシリーズの第2章で、ブロガーは1つのロッカーのインスタンスをカスタマイズすることによって前章の知識を強固にし、カスタムViewの中でユーザーのインタラクションとデータのコールバックの2つの方面の機能を実現することを導入します.また、本章では、カスタムViewにおける数学的知識(特に三角関数)の重要な役割(これもこの例の難点)を見て、脳と両手を解放しましょう.
ファイナルエフェクト
この例の最終的な効果は次のとおりです.
これが本例の最終効果であり,仮想ゲーム方向ロッカーを実現し,ロッカー操作をシミュレートする.さらに、リスナーが揺動方向や速度などのデータリターンを実現するためのデータを書きます(この例では、リスナーの一部のコードのみを実現し、データ読者は自分で追加することができます).
基本的な考え方
まず、XMLプロパティをカスタマイズし、プロパティを導入し、いくつかの部分を測定し、描画する(前の文章のブログをクリックして読んだり、ブロガーの簡単な本を見たりしたことがない)、これらの部分のほかにonTouchEvent()メソッドを書き換えてViewイベント処理を行い、コールバックを利用してリスナーを書く必要があります.全体的な考え方はこのようにして、難しくないように見えて、実際に操作すると罠が多いです.
具体的な実装
前期準備
新しいvalue/attrs.xml、XMLで次の属性を宣言して導入します.

<resources>
    <attr name="InnerColor" format="color"/>
    <attr name="OuterColor" format="color"/>
    <declare-styleable name="NavController">
        <attr name="InnerColor" />
        <attr name="OuterColor"/>
    declare-styleable>
resources>
今回は2つのアトリビュート、小さな円の色と大きな円の色しか必要ありません.次にjavaファイルを新規作成し、Viewを継承してNavControllerと命名し、javaで構築方法を書き換え、XMLプロパティをインポートし、ブラシオブジェクトを新規作成し、プロパティを設定します.キーコードは次のとおりです.
    public NavController(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = getResources().obtainAttributes(attrs,R.styleable.NavController);
        innerColor = ta.getColor(R.styleable.NavController_InnerColor,INNER_COLOR_DEFAULT);
        outerColor = ta.getColor(R.styleable.NavController_OuterColor,OUTER_COLOR_DEFAULT);
        ta.recycle();
        OUTER_WIDTH_SIZE = dip2px(context,125.0f);
        OUTER_HEIGHT_SIZE = dip2px(context,125.0f);
        outerPaint = new Paint();
        innerPaint = new Paint();
        outerPaint.setColor(outerColor);
        outerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        innerPaint.setColor(innerColor);
        innerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
上のOUTER_WIDTH_SIZEとOUTER_HEIGHT_SIZEはそれぞれ大きい円が具体的な値を設定していない下のデフォルトの大きさで、私達はdip 2 px()方法を使って私達の熟練して掌握したdipをjavaロジックの唯一認めたpx単位に転化して、具体的に実現します:
public static int dip2px(Context context, float dpValue){
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue*scale +0.5f);
    }
これで前期の準備が整い、前編ではonMeasureとonDrawに関する理解と使い方について述べたので、ここでは簡単に述べ、この2つを同じ部分に書きます.
測量ペイント
測定時には,親Viewから与えられた値にpadding値(EXACTLY)を加え,大円のアスペクト(UNSPECIFIED)と親View許容最大値との間の最小値(AT_MOST)を返す3つのモードのサイズをそれぞれ異なる処理を行った.その後、コールバックonSizeChanged()から実際のアスペクト値を取り出し、その値を使用してViewペイントを行います.onDrawでは主に2つの円の半径(大きな円の半径がpaddingの幅の半分を除いた場合の4つの場合の最小値を決定し、コードを参照してこの文を見ます.小さな円の半径は大きな円の半分)と2つの円を描きました.さらに、小さな円の中心点onSizeChangedで値を割り当てました.小さな円の中心点座標値の変更がこの例の鍵であることに注意して、それを変更することで効果を実現します.これでViewの表示領域とViewの基本形状を定義します.
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width,height);
    }

    private int measureWidth(int widthMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthVal = MeasureSpec.getSize(widthMeasureSpec);
        //      
        if(widthMode==MeasureSpec.EXACTLY){
            return widthVal+getPaddingLeft()+getPaddingRight();
        }else if(widthMode==MeasureSpec.UNSPECIFIED){
            return OUTER_WIDTH_SIZE;
        }else{
            return Math.min(OUTER_WIDTH_SIZE,widthVal);
        }
    }
    private int measureHeight(int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightVal = MeasureSpec.getSize(heightMeasureSpec);
        //      
        if(heightMode==MeasureSpec.EXACTLY){
            return heightVal+getPaddingTop()+getPaddingBottom();
        }else if(heightMode==MeasureSpec.UNSPECIFIED){
            return OUTER_HEIGHT_SIZE;
        }else{
            return Math.min(OUTER_HEIGHT_SIZE,heightVal);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        realWidth = w;
        realHeight = h;
        innerCenterX = realWidth/2;
        innerCenterY = realHeight/2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        outRadius = Math.min(Math.min(realWidth/2-getPaddingLeft(),realWidth/2-getPaddingRight()),Math.min(realHeight/2-getPaddingTop(),realHeight/2-getPaddingBottom()));
        //    
        canvas.drawCircle(realWidth/2,realHeight/2,outRadius,outerPaint);
        //   
        innerRedius = outRadius*0.5f;
        canvas.drawCircle(innerCenterX,innerCenterY,innerRedius,innerPaint);
    }
Viewイベント処理
次のステップは、効果を達成するための重要なステップです.基本的な形状を描きましたが、Viewタッチでは何の効果もありません.このステップはまさにViewタッチの効果を実現します.この一歩の理解には一定の難易度があるかもしれないが、読者は基本的な三角関数と円の方程式の計算を繰り返し推測しなければならない.onTouchEvent()メソッドを書き換え、このコードを先に置きます.
@Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_DOWN){
            changeInnerCirclePosition(event);

        }
        if(event.getAction()==MotionEvent.ACTION_MOVE){
            changeInnerCirclePosition(event);
            Log.i("TAG","MOVED");
        }
        if(event.getAction()==MotionEvent.ACTION_UP){
            innerCenterX = realWidth/2;
            innerCenterY = realHeight/2;
            invalidate();
        }
        return true;
    }
上記のonTouchEvent()メソッドの書き換えは,ユーザがViewの領域で押す,移動する,離れるの3つの場合を処理し,指が離れると内円の中心点をViewの真ん中(初期状態に戻る)に移動し,リフレッシュし,押すまたは移動するとchangeInnerCirclePosition(event)メソッドを呼び出し,この方法は、内円を指の位置から調整位置を判断するように処理するために用いられる.次に、changeInnerCirclePosition()メソッドを見てみましょう.
private void changeInnerCirclePosition(MotionEvent e) {
        //    :(x-realWidth/2)^2 +(y - realHeight/2)^2 <= outRadius^2
        //   ,         
        float X = e.getX();
        float Y = e.getY();
        if(mCallBack!=null){
            mCallBack.onNavAndSpeed(X,Y);
        }
        boolean isPointInOutCircle = Math.pow(X-realWidth/2,2) +Math.pow(Y-realHeight/2,2)<=Math.pow(outRadius,2);
        if(isPointInOutCircle){
            Log.i("TAG","inCircle");
            //    :    
            boolean isPointInFree = Math.pow(X-realWidth/2,2) +Math.pow(Y-realHeight/2,2)<=Math.pow(outRadius-innerRedius,2);
            if(isPointInFree){
                innerCenterX = X;
                innerCenterY = Y;
            }else{
                //      ,                           
                //             
                //     ,                 (pointTri)     
                float pointTriX = Math.abs(realWidth/2-X);//  
                float pointTriY = Math.abs(realHeight/2-Y);//  
                float pointTriZ = (float) Math.sqrt((Math.pow(pointTriX,2)+Math.pow(pointTriY,2)));
                float TriSin = pointTriY/pointTriZ;
                float TriCos = pointTriX/pointTriZ;
                //                   
                float limitCircleTriY = (outRadius-innerRedius)*TriSin;
                float limitCircleTriX = (outRadius-innerRedius)*TriCos;
                //          ,     
                if(X>=realWidth/2 && Y>=realHeight/2){
                    innerCenterX = realWidth/2+limitCircleTriX;
                    innerCenterY = realHeight/2+limitCircleTriY;
                }else if(X2 && Y>=realHeight/2){
                    innerCenterX = realWidth/2-limitCircleTriX;
                    innerCenterY= realHeight/2+limitCircleTriY;
                }else if(X>=realWidth/2 && Y2){
                    innerCenterX = realWidth/2+limitCircleTriX;
                    innerCenterY= realHeight/2-limitCircleTriY;
                }else{
                    innerCenterX = realWidth/2-limitCircleTriX;
                    innerCenterY= realHeight/2-limitCircleTriY;
                }
                Log.i("TAG","inLimit");
            }
            invalidate();
        }else{
            Log.i("TAG","notInCircle");
        }
    }
この方法は長いが,前述したように,内円をユーザの指の位置に応じて位置変動させる鍵である.次は一歩一歩分析する.本人が実現中に描いた画像を添付します.いくつかの論理を実現する際に、思考の記録と啓発を助けるために絵を描くことをお勧めします.
まず、有効なタッチ範囲(個人の実際の状況に応じて)を取得します.
この例では,ブロガーはユーザが大きな円の外の範囲をクリックしたときに内円が動き続けることを望んでいないので,まずタッチの有効範囲を特定する.円の方程式を用いて、ユーザのタッチポイントが大きな円内にあるかどうかを判断し、コード:boolean isPointInOutCircle = Math.pow(X-realWidth/2,2) +Math.pow(Y-realHeight/2,2)<=Math.pow(outRadius,2);.大円内で論理を実行しない場合は、大円内で次の判断を行います.
第2歩、タッチポイントはフリードメイン内にありますか?
ここのブロガーは自由域という言葉を使っています(よし、自分で話した).大きな円の半径から小さな円の半径を引いた値を半径として新しい円を生成し(制限円と命名し、以下に用いる)、下図のS 1部分を自由域、S 2を非自由域と呼ぶ.
自由域にはどんな特徴がありますか?ユーザーがタッチポイントを自由域内の私たちの小さな円の中心点に置くと、タッチポイントの座標に従えばいいので、制限はありません.非自由領域内に落ちると、小さな円の中心点が制限されます.ここでは二つの部分に分けて討論する.フリードメインの処理は次のとおりです.
if(isPointInFree){
                innerCenterX = X;
                innerCenterY = Y;
            }
非自由ドメインについてどう処理しますか?ユーザのタッチポイントが非自由領域に落ちた場合、タッチポイントと制限円を小さな円の中心点の座標として、下図に示すようにします.
次に、直線と円の交点座標を解く問題です(ブロガーは丸1時間やっていましたが...)、この問題を解決すればいいのですが、ここの座標は数学の座標系とは少し異なり、横軸はxで右、縦軸はyで下に向いていることに注意してください.私の処理方法は上のコードを見て、主に似たような三角形と三角関数の知識を利用しています.具体的な注釈には上に記載があります.このステップは、カスタムViewの品質の高低を表現することがよくあります.
Listenerの構造
このロッカーがあれば、最後にデータを返す機能を実現するために、リスナーを使用して実現します.(Androidコールバックメカニズムを利用してDialogを簡単にカプセル化してコールバックの傍受に関する知識を参考にすることができます)まずリスニングのコールバックインタフェースを作成し、
public interface OnNavAndSpeedListener{
        public void onNavAndSpeed(float nav,float speed);
    }
インタフェースオブジェクトmCallBackを宣言し、コールバックしてデータを返す必要がある場所でインタフェースを変更する方法:mCallBack.onNavAndSpeed(float nav,float speed)を呼び出し、mCallBackオブジェクトがnullであるかどうかを判断することに注意します.次に
public void setOnNavAndSpeedListener(OnNavAndSpeedListener listener){
        mCallBack = listener;
    }
Activityで呼び出し元にインタフェースを新規作成して入力します.Activityでは、以下のように具体的に使用されます.
navController.setOnNavAndSpeedListener(new NavController.OnNavAndSpeedListener() {
            @Override
            public void onNavAndSpeed(float nav, float speed) {
                Log.i("TAG",nav+speed+"");
            }
        });
これで速度と方向の戻りを実現し、ゲームロッカーをカスタマイズしました.読者は必要に応じて最適化して変更することができます.
まとめ
本章では,ゲームロッカーを実装する例を用いて,カスタムViewの基本手順とイベント処理とリスナーの導入に関する実装をレビューし,以下の文章では,ViewGroupについてナイフ学習を開始する.もしあなたに役に立つと感じたら、私のブログや簡単な本に注目することができます.
付録:View具体コード
public class NavController extends View {
    private int innerColor;
    private int outerColor;
    private final static int INNER_COLOR_DEFAULT = Color.parseColor("#d32f2f");
    private final static int OUTER_COLOR_DEFAULT = Color.parseColor("#f44336");
    private int OUTER_WIDTH_SIZE;
    private int OUTER_HEIGHT_SIZE;
    private int realWidth;//      
    private int realHeight;//      
    private float innerCenterX;
    private float innerCenterY;
    private float outRadius;
    private float innerRedius;
    private Paint outerPaint;
    private Paint innerPaint;
    private OnNavAndSpeedListener mCallBack = null;
    public interface OnNavAndSpeedListener{
        public void onNavAndSpeed(float nav,float speed);
    }
    public NavController(Context context) {
        this(context,null);
    }

    public NavController(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = getResources().obtainAttributes(attrs,R.styleable.NavController);
        innerColor = ta.getColor(R.styleable.NavController_InnerColor,INNER_COLOR_DEFAULT);
        outerColor = ta.getColor(R.styleable.NavController_OuterColor,OUTER_COLOR_DEFAULT);
        ta.recycle();
        OUTER_WIDTH_SIZE = dip2px(context,125.0f);
        OUTER_HEIGHT_SIZE = dip2px(context,125.0f);
        outerPaint = new Paint();
        innerPaint = new Paint();
        outerPaint.setColor(outerColor);
        outerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        innerPaint.setColor(innerColor);
        innerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width,height);
    }

    private int measureWidth(int widthMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthVal = MeasureSpec.getSize(widthMeasureSpec);
        //      
        if(widthMode==MeasureSpec.EXACTLY){
            return widthVal+getPaddingLeft()+getPaddingRight();
        }else if(widthMode==MeasureSpec.UNSPECIFIED){
            return OUTER_WIDTH_SIZE;
        }else{
            return Math.min(OUTER_WIDTH_SIZE,widthVal);
        }
    }
    private int measureHeight(int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightVal = MeasureSpec.getSize(heightMeasureSpec);
        //      
        if(heightMode==MeasureSpec.EXACTLY){
            return heightVal+getPaddingTop()+getPaddingBottom();
        }else if(heightMode==MeasureSpec.UNSPECIFIED){
            return OUTER_HEIGHT_SIZE;
        }else{
            return Math.min(OUTER_HEIGHT_SIZE,heightVal);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        realWidth = w;
        realHeight = h;
        innerCenterX = realWidth/2;
        innerCenterY = realHeight/2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        outRadius = Math.min(Math.min(realWidth/2-getPaddingLeft(),realWidth/2-getPaddingRight()),Math.min(realHeight/2-getPaddingTop(),realHeight/2-getPaddingBottom()));
        //    
        canvas.drawCircle(realWidth/2,realHeight/2,outRadius,outerPaint);
        //   
        innerRedius = outRadius*0.5f;
        canvas.drawCircle(innerCenterX,innerCenterY,innerRedius,innerPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_DOWN){
            changeInnerCirclePosition(event);

        }
        if(event.getAction()==MotionEvent.ACTION_MOVE){
            changeInnerCirclePosition(event);
            Log.i("TAG","MOVED");
        }
        if(event.getAction()==MotionEvent.ACTION_UP){
            innerCenterX = realWidth/2;
            innerCenterY = realHeight/2;
            invalidate();
        }
        return true;
    }

    private void changeInnerCirclePosition(MotionEvent e) {
        //    :(x-realWidth/2)^2 +(y - realHeight/2)^2 <= outRadius^2
        //   ,         
        float X = e.getX();
        float Y = e.getY();
        if(mCallBack!=null){
            mCallBack.onNavAndSpeed(X,Y);
        }
        boolean isPointInOutCircle = Math.pow(X-realWidth/2,2) +Math.pow(Y-realHeight/2,2)<=Math.pow(outRadius,2);
        if(isPointInOutCircle){
            Log.i("TAG","inCircle");
            //    :    
            boolean isPointInFree = Math.pow(X-realWidth/2,2) +Math.pow(Y-realHeight/2,2)<=Math.pow(outRadius-innerRedius,2);
            if(isPointInFree){
                innerCenterX = X;
                innerCenterY = Y;
            }else{
                //      ,                           
                //             
                //     ,                 (pointTri)     
                float pointTriX = Math.abs(realWidth/2-X);//  
                float pointTriY = Math.abs(realHeight/2-Y);//  
                float pointTriZ = (float) Math.sqrt((Math.pow(pointTriX,2)+Math.pow(pointTriY,2)));
                float TriSin = pointTriY/pointTriZ;
                float TriCos = pointTriX/pointTriZ;
                //                   
                float limitCircleTriY = (outRadius-innerRedius)*TriSin;
                float limitCircleTriX = (outRadius-innerRedius)*TriCos;
                //          ,     
                if(X>=realWidth/2 && Y>=realHeight/2){
                    innerCenterX = realWidth/2+limitCircleTriX;
                    innerCenterY = realHeight/2+limitCircleTriY;
                }else if(X2 && Y>=realHeight/2){
                    innerCenterX = realWidth/2-limitCircleTriX;
                    innerCenterY= realHeight/2+limitCircleTriY;
                }else if(X>=realWidth/2 && Y2){
                    innerCenterX = realWidth/2+limitCircleTriX;
                    innerCenterY= realHeight/2-limitCircleTriY;
                }else{
                    innerCenterX = realWidth/2-limitCircleTriX;
                    innerCenterY= realHeight/2-limitCircleTriY;
                }
                Log.i("TAG","inLimit");
            }
            invalidate();
        }else{
            Log.i("TAG","notInCircle");
        }
    }
    public void setOnNavAndSpeedListener(OnNavAndSpeedListener listener){
        mCallBack = listener;
    }
    public static int dip2px(Context context, float dpValue){
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue*scale +0.5f);
    }
}