1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.example.android.apis.view;
18
19import com.example.android.apis.R;
20
21import android.content.ClipData;
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.graphics.*;
25import android.os.SystemClock;
26import android.text.TextPaint;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.DragEvent;
30import android.view.View;
31import android.widget.TextView;
32
33public class DraggableDot extends View {
34    static final String TAG = "DraggableDot";
35
36    private boolean mDragInProgress;
37    private boolean mHovering;
38    private boolean mAcceptsDrag;
39    TextView mReportView;
40
41    private Paint mPaint;
42    private TextPaint mLegendPaint;
43    private Paint mGlow;
44    private static final int NUM_GLOW_STEPS = 10;
45    private static final int GREEN_STEP = 0x0000FF00 / NUM_GLOW_STEPS;
46    private static final int WHITE_STEP = 0x00FFFFFF / NUM_GLOW_STEPS;
47    private static final int ALPHA_STEP = 0xFF000000 / NUM_GLOW_STEPS;
48
49    int mRadius;
50    int mAnrType;
51    CharSequence mLegend;
52
53    static final int ANR_NONE = 0;
54    static final int ANR_SHADOW = 1;
55    static final int ANR_DROP = 2;
56
57    void sleepSixSeconds() {
58        // hang forever; good for producing ANRs
59        long start = SystemClock.uptimeMillis();
60        do {
61            try { Thread.sleep(1000); } catch (InterruptedException e) {}
62        } while (SystemClock.uptimeMillis() < start + 6000);
63    }
64
65    // Shadow builder that can ANR if desired
66    class ANRShadowBuilder extends DragShadowBuilder {
67        boolean mDoAnr;
68
69        public ANRShadowBuilder(View view, boolean doAnr) {
70            super(view);
71            mDoAnr = doAnr;
72        }
73
74        @Override
75        public void onDrawShadow(Canvas canvas) {
76            if (mDoAnr) {
77                sleepSixSeconds();
78            }
79            super.onDrawShadow(canvas);
80        }
81    }
82
83    public DraggableDot(Context context, AttributeSet attrs) {
84        super(context, attrs);
85
86        setFocusable(true);
87        setClickable(true);
88
89        mLegend = "";
90
91        mPaint = new Paint();
92        mPaint.setAntiAlias(true);
93        mPaint.setStrokeWidth(6);
94        mPaint.setColor(0xFFD00000);
95
96        mLegendPaint = new TextPaint();
97        mLegendPaint.setAntiAlias(true);
98        mLegendPaint.setTextAlign(Paint.Align.CENTER);
99        mLegendPaint.setColor(0xFFF0F0FF);
100
101        mGlow = new Paint();
102        mGlow.setAntiAlias(true);
103        mGlow.setStrokeWidth(1);
104        mGlow.setStyle(Paint.Style.STROKE);
105
106        // look up any layout-defined attributes
107        TypedArray a = context.obtainStyledAttributes(attrs,
108                R.styleable.DraggableDot);
109
110        final int N = a.getIndexCount();
111        for (int i = 0; i < N; i++) {
112            int attr = a.getIndex(i);
113            switch (attr) {
114            case R.styleable.DraggableDot_radius: {
115                mRadius = a.getDimensionPixelSize(attr, 0);
116            } break;
117
118            case R.styleable.DraggableDot_legend: {
119                mLegend = a.getText(attr);
120            } break;
121
122            case R.styleable.DraggableDot_anr: {
123                mAnrType = a.getInt(attr, 0);
124            } break;
125            }
126        }
127
128        Log.i(TAG, "DraggableDot @ " + this + " : radius=" + mRadius + " legend='" + mLegend
129                + "' anr=" + mAnrType);
130
131        setOnLongClickListener(new View.OnLongClickListener() {
132            public boolean onLongClick(View v) {
133                ClipData data = ClipData.newPlainText("dot", "Dot : " + v.toString());
134                v.startDrag(data, new ANRShadowBuilder(v, mAnrType == ANR_SHADOW),
135                        (Object)v, 0);
136                return true;
137            }
138        });
139    }
140
141    void setReportView(TextView view) {
142        mReportView = view;
143    }
144
145    @Override
146    protected void onDraw(Canvas canvas) {
147        float wf = getWidth();
148        float hf = getHeight();
149        final float cx = wf/2;
150        final float cy = hf/2;
151        wf -= getPaddingLeft() + getPaddingRight();
152        hf -= getPaddingTop() + getPaddingBottom();
153        float rad = (wf < hf) ? wf/2 : hf/2;
154        canvas.drawCircle(cx, cy, rad, mPaint);
155
156        if (mLegend != null && mLegend.length() > 0) {
157            canvas.drawText(mLegend, 0, mLegend.length(),
158                    cx, cy + mLegendPaint.getFontSpacing()/2,
159                    mLegendPaint);
160        }
161
162        // if we're in the middle of a drag, light up as a potential target
163        if (mDragInProgress && mAcceptsDrag) {
164            for (int i = NUM_GLOW_STEPS; i > 0; i--) {
165                int color = (mHovering) ? WHITE_STEP : GREEN_STEP;
166                color = i*(color | ALPHA_STEP);
167                mGlow.setColor(color);
168                canvas.drawCircle(cx, cy, rad, mGlow);
169                rad -= 0.5f;
170                canvas.drawCircle(cx, cy, rad, mGlow);
171                rad -= 0.5f;
172            }
173        }
174    }
175
176    @Override
177    protected void onMeasure(int widthSpec, int heightSpec) {
178        int totalDiameter = 2*mRadius + getPaddingLeft() + getPaddingRight();
179        setMeasuredDimension(totalDiameter, totalDiameter);
180    }
181
182    /**
183     * Drag and drop
184     */
185    @Override
186    public boolean onDragEvent(DragEvent event) {
187        boolean result = false;
188        switch (event.getAction()) {
189        case DragEvent.ACTION_DRAG_STARTED: {
190            // claim to accept any dragged content
191            Log.i(TAG, "Drag started, event=" + event);
192            // cache whether we accept the drag to return for LOCATION events
193            mDragInProgress = true;
194            mAcceptsDrag = result = true;
195            // Redraw in the new visual state if we are a potential drop target
196            if (mAcceptsDrag) {
197                invalidate();
198            }
199        } break;
200
201        case DragEvent.ACTION_DRAG_ENDED: {
202            Log.i(TAG, "Drag ended.");
203            if (mAcceptsDrag) {
204                invalidate();
205            }
206            mDragInProgress = false;
207            mHovering = false;
208        } break;
209
210        case DragEvent.ACTION_DRAG_LOCATION: {
211            // we returned true to DRAG_STARTED, so return true here
212            Log.i(TAG, "... seeing drag locations ...");
213            result = mAcceptsDrag;
214        } break;
215
216        case DragEvent.ACTION_DROP: {
217            Log.i(TAG, "Got a drop! dot=" + this + " event=" + event);
218            if (mAnrType == ANR_DROP) {
219                sleepSixSeconds();
220            }
221            processDrop(event);
222            result = true;
223        } break;
224
225        case DragEvent.ACTION_DRAG_ENTERED: {
226            Log.i(TAG, "Entered dot @ " + this);
227            mHovering = true;
228            invalidate();
229        } break;
230
231        case DragEvent.ACTION_DRAG_EXITED: {
232            Log.i(TAG, "Exited dot @ " + this);
233            mHovering = false;
234            invalidate();
235        } break;
236
237        default:
238            Log.i(TAG, "other drag event: " + event);
239            result = mAcceptsDrag;
240            break;
241        }
242
243        return result;
244    }
245
246    private void processDrop(DragEvent event) {
247        final ClipData data = event.getClipData();
248        final int N = data.getItemCount();
249        for (int i = 0; i < N; i++) {
250            ClipData.Item item = data.getItemAt(i);
251            Log.i(TAG, "Dropped item " + i + " : " + item);
252            if (mReportView != null) {
253                String text = item.coerceToText(getContext()).toString();
254                if (event.getLocalState() == (Object) this) {
255                    text += " : Dropped on self!";
256                }
257                mReportView.setText(text);
258            }
259        }
260    }
261}