PointerLocationView.java revision a032cc008618b83ecbbede537517d1e7998e3264
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.android.internal.widget;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.RectF;
23import android.graphics.Paint.FontMetricsInt;
24import android.util.Log;
25import android.view.InputDevice;
26import android.view.KeyEvent;
27import android.view.MotionEvent;
28import android.view.VelocityTracker;
29import android.view.View;
30import android.view.ViewConfiguration;
31import android.view.MotionEvent.PointerCoords;
32
33import java.util.ArrayList;
34
35public class PointerLocationView extends View {
36    private static final String TAG = "Pointer";
37
38    public static class PointerState {
39        // Trace of previous points.
40        private float[] mTraceX = new float[32];
41        private float[] mTraceY = new float[32];
42        private int mTraceCount;
43
44        // True if the pointer is down.
45        private boolean mCurDown;
46
47        // Most recent coordinates.
48        private PointerCoords mCoords = new PointerCoords();
49
50        // Most recent velocity.
51        private float mXVelocity;
52        private float mYVelocity;
53
54        public void clearTrace() {
55            mTraceCount = 0;
56        }
57
58        public void addTrace(float x, float y) {
59            int traceCapacity = mTraceX.length;
60            if (mTraceCount == traceCapacity) {
61                traceCapacity *= 2;
62                float[] newTraceX = new float[traceCapacity];
63                System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
64                mTraceX = newTraceX;
65
66                float[] newTraceY = new float[traceCapacity];
67                System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
68                mTraceY = newTraceY;
69            }
70
71            mTraceX[mTraceCount] = x;
72            mTraceY[mTraceCount] = y;
73            mTraceCount += 1;
74        }
75    }
76
77    private final ViewConfiguration mVC;
78    private final Paint mTextPaint;
79    private final Paint mTextBackgroundPaint;
80    private final Paint mTextLevelPaint;
81    private final Paint mPaint;
82    private final Paint mTargetPaint;
83    private final Paint mPathPaint;
84    private final FontMetricsInt mTextMetrics = new FontMetricsInt();
85    private int mHeaderBottom;
86    private boolean mCurDown;
87    private int mCurNumPointers;
88    private int mMaxNumPointers;
89    private int mActivePointerId;
90    private final ArrayList<PointerState> mPointers = new ArrayList<PointerState>();
91    private final PointerCoords mHoverCoords = new PointerCoords();
92
93    private final VelocityTracker mVelocity;
94
95    private final FasterStringBuilder mText = new FasterStringBuilder();
96
97    private boolean mPrintCoords = true;
98
99    public PointerLocationView(Context c) {
100        super(c);
101        setFocusableInTouchMode(true);
102
103        mVC = ViewConfiguration.get(c);
104        mTextPaint = new Paint();
105        mTextPaint.setAntiAlias(true);
106        mTextPaint.setTextSize(10
107                * getResources().getDisplayMetrics().density);
108        mTextPaint.setARGB(255, 0, 0, 0);
109        mTextBackgroundPaint = new Paint();
110        mTextBackgroundPaint.setAntiAlias(false);
111        mTextBackgroundPaint.setARGB(128, 255, 255, 255);
112        mTextLevelPaint = new Paint();
113        mTextLevelPaint.setAntiAlias(false);
114        mTextLevelPaint.setARGB(192, 255, 0, 0);
115        mPaint = new Paint();
116        mPaint.setAntiAlias(true);
117        mPaint.setARGB(255, 255, 255, 255);
118        mPaint.setStyle(Paint.Style.STROKE);
119        mPaint.setStrokeWidth(2);
120        mTargetPaint = new Paint();
121        mTargetPaint.setAntiAlias(false);
122        mTargetPaint.setARGB(255, 0, 0, 192);
123        mPathPaint = new Paint();
124        mPathPaint.setAntiAlias(false);
125        mPathPaint.setARGB(255, 0, 96, 255);
126        mPaint.setStyle(Paint.Style.STROKE);
127        mPaint.setStrokeWidth(1);
128
129        PointerState ps = new PointerState();
130        mPointers.add(ps);
131        mActivePointerId = 0;
132
133        mVelocity = VelocityTracker.obtain();
134
135        logInputDeviceCapabilities();
136    }
137
138    private void logInputDeviceCapabilities() {
139        int[] deviceIds = InputDevice.getDeviceIds();
140        for (int i = 0; i < deviceIds.length; i++) {
141            InputDevice device = InputDevice.getDevice(deviceIds[i]);
142            if (device != null) {
143                Log.i(TAG, device.toString());
144            }
145        }
146    }
147
148    public void setPrintCoords(boolean state) {
149        mPrintCoords = state;
150    }
151
152    @Override
153    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
154        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
155        mTextPaint.getFontMetricsInt(mTextMetrics);
156        mHeaderBottom = -mTextMetrics.ascent+mTextMetrics.descent+2;
157        if (false) {
158            Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
159                    + " descent=" + mTextMetrics.descent
160                    + " leading=" + mTextMetrics.leading
161                    + " top=" + mTextMetrics.top
162                    + " bottom=" + mTextMetrics.bottom);
163        }
164    }
165
166    // Draw an oval.  When angle is 0 radians, orients the major axis vertically,
167    // angles less than or greater than 0 radians rotate the major axis left or right.
168    private RectF mReusableOvalRect = new RectF();
169    private void drawOval(Canvas canvas, float x, float y, float major, float minor,
170            float angle, Paint paint) {
171        canvas.save(Canvas.MATRIX_SAVE_FLAG);
172        canvas.rotate((float) (angle * 180 / Math.PI), x, y);
173        mReusableOvalRect.left = x - minor / 2;
174        mReusableOvalRect.right = x + minor / 2;
175        mReusableOvalRect.top = y - major / 2;
176        mReusableOvalRect.bottom = y + major / 2;
177        canvas.drawOval(mReusableOvalRect, paint);
178        canvas.restore();
179    }
180
181    @Override
182    protected void onDraw(Canvas canvas) {
183        synchronized (mPointers) {
184            final int w = getWidth();
185            final int itemW = w/7;
186            final int base = -mTextMetrics.ascent+1;
187            final int bottom = mHeaderBottom;
188
189            final int NP = mPointers.size();
190
191            // Labels
192            if (mActivePointerId >= 0) {
193                final PointerState ps = mPointers.get(mActivePointerId);
194
195                canvas.drawRect(0, 0, itemW-1, bottom,mTextBackgroundPaint);
196                canvas.drawText(mText.clear()
197                        .append("P: ").append(mCurNumPointers)
198                        .append(" / ").append(mMaxNumPointers)
199                        .toString(), 1, base, mTextPaint);
200
201                final int N = ps.mTraceCount;
202                if ((mCurDown && ps.mCurDown) || N == 0) {
203                    canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom, mTextBackgroundPaint);
204                    canvas.drawText(mText.clear()
205                            .append("X: ").append(ps.mCoords.x, 1)
206                            .toString(), 1 + itemW, base, mTextPaint);
207                    canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom, mTextBackgroundPaint);
208                    canvas.drawText(mText.clear()
209                            .append("Y: ").append(ps.mCoords.y, 1)
210                            .toString(), 1 + itemW * 2, base, mTextPaint);
211                } else {
212                    float dx = ps.mTraceX[N - 1] - ps.mTraceX[0];
213                    float dy = ps.mTraceY[N - 1] - ps.mTraceY[0];
214                    canvas.drawRect(itemW, 0, (itemW * 2) - 1, bottom,
215                            Math.abs(dx) < mVC.getScaledTouchSlop()
216                            ? mTextBackgroundPaint : mTextLevelPaint);
217                    canvas.drawText(mText.clear()
218                            .append("dX: ").append(dx, 1)
219                            .toString(), 1 + itemW, base, mTextPaint);
220                    canvas.drawRect(itemW * 2, 0, (itemW * 3) - 1, bottom,
221                            Math.abs(dy) < mVC.getScaledTouchSlop()
222                            ? mTextBackgroundPaint : mTextLevelPaint);
223                    canvas.drawText(mText.clear()
224                            .append("dY: ").append(dy, 1)
225                            .toString(), 1 + itemW * 2, base, mTextPaint);
226                }
227
228                canvas.drawRect(itemW * 3, 0, (itemW * 4) - 1, bottom, mTextBackgroundPaint);
229                canvas.drawText(mText.clear()
230                        .append("Xv: ").append(ps.mXVelocity, 3)
231                        .toString(), 1 + itemW * 3, base, mTextPaint);
232
233                canvas.drawRect(itemW * 4, 0, (itemW * 5) - 1, bottom, mTextBackgroundPaint);
234                canvas.drawText(mText.clear()
235                        .append("Yv: ").append(ps.mYVelocity, 3)
236                        .toString(), 1 + itemW * 4, base, mTextPaint);
237
238                canvas.drawRect(itemW * 5, 0, (itemW * 6) - 1, bottom, mTextBackgroundPaint);
239                canvas.drawRect(itemW * 5, 0, (itemW * 5) + (ps.mCoords.pressure * itemW) - 1,
240                        bottom, mTextLevelPaint);
241                canvas.drawText(mText.clear()
242                        .append("Prs: ").append(ps.mCoords.pressure, 2)
243                        .toString(), 1 + itemW * 5, base, mTextPaint);
244
245                canvas.drawRect(itemW * 6, 0, w, bottom, mTextBackgroundPaint);
246                canvas.drawRect(itemW * 6, 0, (itemW * 6) + (ps.mCoords.size * itemW) - 1,
247                        bottom, mTextLevelPaint);
248                canvas.drawText(mText.clear()
249                        .append("Size: ").append(ps.mCoords.size, 2)
250                        .toString(), 1 + itemW * 6, base, mTextPaint);
251            }
252
253            // Pointer trace.
254            for (int p = 0; p < NP; p++) {
255                final PointerState ps = mPointers.get(p);
256
257                // Draw path.
258                final int N = ps.mTraceCount;
259                float lastX = 0, lastY = 0;
260                boolean haveLast = false;
261                boolean drawn = false;
262                mPaint.setARGB(255, 128, 255, 255);
263                for (int i=0; i < N; i++) {
264                    float x = ps.mTraceX[i];
265                    float y = ps.mTraceY[i];
266                    if (Float.isNaN(x)) {
267                        haveLast = false;
268                        continue;
269                    }
270                    if (haveLast) {
271                        canvas.drawLine(lastX, lastY, x, y, mPathPaint);
272                        canvas.drawPoint(lastX, lastY, mPaint);
273                        drawn = true;
274                    }
275                    lastX = x;
276                    lastY = y;
277                    haveLast = true;
278                }
279
280                // Draw velocity vector.
281                if (drawn) {
282                    mPaint.setARGB(255, 255, 64, 128);
283                    float xVel = ps.mXVelocity * (1000 / 60);
284                    float yVel = ps.mYVelocity * (1000 / 60);
285                    canvas.drawLine(lastX, lastY, lastX + xVel, lastY + yVel, mPaint);
286                }
287
288                if (mCurDown && ps.mCurDown) {
289                    // Draw crosshairs.
290                    canvas.drawLine(0, ps.mCoords.y, getWidth(), ps.mCoords.y, mTargetPaint);
291                    canvas.drawLine(ps.mCoords.x, 0, ps.mCoords.x, getHeight(), mTargetPaint);
292
293                    // Draw current point.
294                    int pressureLevel = (int)(ps.mCoords.pressure * 255);
295                    mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
296                    canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
297
298                    // Draw current touch ellipse.
299                    mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
300                    drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
301                            ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
302
303                    // Draw current tool ellipse.
304                    mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
305                    drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
306                            ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
307
308                    // Draw the orientation arrow.
309                    mPaint.setARGB(255, pressureLevel, 255, 0);
310                    float orientationVectorX = (float) (Math.sin(-ps.mCoords.orientation)
311                            * ps.mCoords.toolMajor * 0.7);
312                    float orientationVectorY = (float) (Math.cos(-ps.mCoords.orientation)
313                            * ps.mCoords.toolMajor * 0.7);
314                    canvas.drawLine(
315                            ps.mCoords.x - orientationVectorX, ps.mCoords.y - orientationVectorY,
316                            ps.mCoords.x + orientationVectorX, ps.mCoords.y + orientationVectorY,
317                            mPaint);
318                }
319            }
320        }
321    }
322
323    private void logPointerCoords(int action, int index, MotionEvent.PointerCoords coords, int id) {
324        final String prefix;
325        switch (action & MotionEvent.ACTION_MASK) {
326            case MotionEvent.ACTION_DOWN:
327                prefix = "DOWN";
328                break;
329            case MotionEvent.ACTION_UP:
330                prefix = "UP";
331                break;
332            case MotionEvent.ACTION_MOVE:
333                prefix = "MOVE";
334                break;
335            case MotionEvent.ACTION_CANCEL:
336                prefix = "CANCEL";
337                break;
338            case MotionEvent.ACTION_OUTSIDE:
339                prefix = "OUTSIDE";
340                break;
341            case MotionEvent.ACTION_POINTER_DOWN:
342                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
343                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
344                    prefix = "DOWN";
345                } else {
346                    prefix = "MOVE";
347                }
348                break;
349            case MotionEvent.ACTION_POINTER_UP:
350                if (index == ((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
351                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT)) {
352                    prefix = "UP";
353                } else {
354                    prefix = "MOVE";
355                }
356                break;
357            case MotionEvent.ACTION_HOVER_MOVE:
358                prefix = "HOVER MOVE";
359                break;
360            case MotionEvent.ACTION_HOVER_ENTER:
361                prefix = "HOVER ENTER";
362                break;
363            case MotionEvent.ACTION_HOVER_EXIT:
364                prefix = "HOVER EXIT";
365                break;
366            case MotionEvent.ACTION_SCROLL:
367                prefix = "SCROLL";
368                break;
369            default:
370                prefix = Integer.toString(action);
371                break;
372        }
373
374        Log.i(TAG, mText.clear()
375                .append("Pointer ").append(id + 1)
376                .append(": ")
377                .append(prefix)
378                .append(" (").append(coords.x, 3).append(", ").append(coords.y, 3)
379                .append(") Pressure=").append(coords.pressure, 3)
380                .append(" Size=").append(coords.size, 3)
381                .append(" TouchMajor=").append(coords.touchMajor, 3)
382                .append(" TouchMinor=").append(coords.touchMinor, 3)
383                .append(" ToolMajor=").append(coords.toolMajor, 3)
384                .append(" ToolMinor=").append(coords.toolMinor, 3)
385                .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
386                .append("deg")
387                .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1)
388                .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1)
389                .toString());
390    }
391
392    public void addPointerEvent(MotionEvent event) {
393        synchronized (mPointers) {
394            int action = event.getAction();
395
396            //Log.i(TAG, "Motion: action=0x" + Integer.toHexString(action)
397            //        + " pointers=" + event.getPointerCount());
398
399            int NP = mPointers.size();
400
401            //mRect.set(0, 0, getWidth(), mHeaderBottom+1);
402            //invalidate(mRect);
403            //if (mCurDown) {
404            //    mRect.set(mCurX-mCurWidth-3, mCurY-mCurWidth-3,
405            //            mCurX+mCurWidth+3, mCurY+mCurWidth+3);
406            //} else {
407            //    mRect.setEmpty();
408            //}
409            if (action == MotionEvent.ACTION_DOWN
410                    || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
411                final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
412                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for down
413                if (action == MotionEvent.ACTION_DOWN) {
414                    for (int p=0; p<NP; p++) {
415                        final PointerState ps = mPointers.get(p);
416                        ps.clearTrace();
417                        ps.mCurDown = false;
418                    }
419                    mCurDown = true;
420                    mCurNumPointers = 0;
421                    mMaxNumPointers = 0;
422                    mVelocity.clear();
423                }
424
425                mCurNumPointers += 1;
426                if (mMaxNumPointers < mCurNumPointers) {
427                    mMaxNumPointers = mCurNumPointers;
428                }
429
430                final int id = event.getPointerId(index);
431                while (NP <= id) {
432                    PointerState ps = new PointerState();
433                    mPointers.add(ps);
434                    NP++;
435                }
436
437                if (mActivePointerId < 0 ||
438                        !mPointers.get(mActivePointerId).mCurDown) {
439                    mActivePointerId = id;
440                }
441
442                final PointerState ps = mPointers.get(id);
443                ps.mCurDown = true;
444            }
445
446            final int NI = event.getPointerCount();
447
448            mVelocity.addMovement(event);
449            mVelocity.computeCurrentVelocity(1);
450
451            final int N = event.getHistorySize();
452            for (int historyPos = 0; historyPos < N; historyPos++) {
453                for (int i = 0; i < NI; i++) {
454                    final int id = event.getPointerId(i);
455                    final PointerState ps = mCurDown ? mPointers.get(id) : null;
456                    final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords;
457                    event.getHistoricalPointerCoords(i, historyPos, coords);
458                    if (mPrintCoords) {
459                        logPointerCoords(action, i, coords, id);
460                    }
461                    if (ps != null) {
462                        ps.addTrace(coords.x, coords.y);
463                    }
464                }
465            }
466            for (int i = 0; i < NI; i++) {
467                final int id = event.getPointerId(i);
468                final PointerState ps = mCurDown ? mPointers.get(id) : null;
469                final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords;
470                event.getPointerCoords(i, coords);
471                if (mPrintCoords) {
472                    logPointerCoords(action, i, coords, id);
473                }
474                if (ps != null) {
475                    ps.addTrace(coords.x, coords.y);
476                    ps.mXVelocity = mVelocity.getXVelocity(id);
477                    ps.mYVelocity = mVelocity.getYVelocity(id);
478                }
479            }
480
481            if (action == MotionEvent.ACTION_UP
482                    || action == MotionEvent.ACTION_CANCEL
483                    || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP) {
484                final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)
485                        >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; // will be 0 for UP
486
487                final int id = event.getPointerId(index);
488                final PointerState ps = mPointers.get(id);
489                ps.mCurDown = false;
490
491                if (action == MotionEvent.ACTION_UP
492                        || action == MotionEvent.ACTION_CANCEL) {
493                    mCurDown = false;
494                    mCurNumPointers = 0;
495                } else {
496                    mCurNumPointers -= 1;
497                    if (mActivePointerId == id) {
498                        mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
499                    }
500                    ps.addTrace(Float.NaN, Float.NaN);
501                }
502            }
503
504            //if (mCurDown) {
505            //    mRect.union(mCurX-mCurWidth-3, mCurY-mCurWidth-3,
506            //            mCurX+mCurWidth+3, mCurY+mCurWidth+3);
507            //}
508            //invalidate(mRect);
509            postInvalidate();
510        }
511    }
512
513    @Override
514    public boolean onTouchEvent(MotionEvent event) {
515        addPointerEvent(event);
516
517        if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) {
518            requestFocus();
519        }
520        return true;
521    }
522
523    @Override
524    public boolean onGenericMotionEvent(MotionEvent event) {
525        final int source = event.getSource();
526        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
527            addPointerEvent(event);
528        } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
529            Log.i(TAG, "Joystick: " + event);
530        } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) {
531            Log.i(TAG, "Position: " + event);
532        } else {
533            Log.i(TAG, "Generic: " + event);
534        }
535        return true;
536    }
537
538    @Override
539    public boolean onKeyDown(int keyCode, KeyEvent event) {
540        if (shouldLogKey(keyCode)) {
541            final int repeatCount = event.getRepeatCount();
542            if (repeatCount == 0) {
543                Log.i(TAG, "Key Down: " + event);
544            } else {
545                Log.i(TAG, "Key Repeat #" + repeatCount + ": " + event);
546            }
547            return true;
548        }
549        return super.onKeyDown(keyCode, event);
550    }
551
552    @Override
553    public boolean onKeyUp(int keyCode, KeyEvent event) {
554        if (shouldLogKey(keyCode)) {
555            Log.i(TAG, "Key Up: " + event);
556            return true;
557        }
558        return super.onKeyUp(keyCode, event);
559    }
560
561    private static boolean shouldLogKey(int keyCode) {
562        switch (keyCode) {
563            case KeyEvent.KEYCODE_DPAD_UP:
564            case KeyEvent.KEYCODE_DPAD_DOWN:
565            case KeyEvent.KEYCODE_DPAD_LEFT:
566            case KeyEvent.KEYCODE_DPAD_RIGHT:
567            case KeyEvent.KEYCODE_DPAD_CENTER:
568                return true;
569            default:
570                return KeyEvent.isGamepadButton(keyCode)
571                    || KeyEvent.isModifierKey(keyCode);
572        }
573    }
574
575    @Override
576    public boolean onTrackballEvent(MotionEvent event) {
577        Log.i(TAG, "Trackball: " + event);
578        return true;
579    }
580
581    // HACK
582    // A quick and dirty string builder implementation optimized for GC.
583    // Using String.format causes the application grind to a halt when
584    // more than a couple of pointers are down due to the number of
585    // temporary objects allocated while formatting strings for drawing or logging.
586    private static final class FasterStringBuilder {
587        private char[] mChars;
588        private int mLength;
589
590        public FasterStringBuilder() {
591            mChars = new char[64];
592        }
593
594        public FasterStringBuilder clear() {
595            mLength = 0;
596            return this;
597        }
598
599        public FasterStringBuilder append(String value) {
600            final int valueLength = value.length();
601            final int index = reserve(valueLength);
602            value.getChars(0, valueLength, mChars, index);
603            mLength += valueLength;
604            return this;
605        }
606
607        public FasterStringBuilder append(int value) {
608            return append(value, 0);
609        }
610
611        public FasterStringBuilder append(int value, int zeroPadWidth) {
612            final boolean negative = value < 0;
613            if (negative) {
614                value = - value;
615                if (value < 0) {
616                    append("-2147483648");
617                    return this;
618                }
619            }
620
621            int index = reserve(11);
622            final char[] chars = mChars;
623
624            if (value == 0) {
625                chars[index++] = '0';
626                mLength += 1;
627                return this;
628            }
629
630            if (negative) {
631                chars[index++] = '-';
632            }
633
634            int divisor = 1000000000;
635            int numberWidth = 10;
636            while (value < divisor) {
637                divisor /= 10;
638                numberWidth -= 1;
639                if (numberWidth < zeroPadWidth) {
640                    chars[index++] = '0';
641                }
642            }
643
644            do {
645                int digit = value / divisor;
646                value -= digit * divisor;
647                divisor /= 10;
648                chars[index++] = (char) (digit + '0');
649            } while (divisor != 0);
650
651            mLength = index;
652            return this;
653        }
654
655        public FasterStringBuilder append(float value, int precision) {
656            int scale = 1;
657            for (int i = 0; i < precision; i++) {
658                scale *= 10;
659            }
660            value = (float) (Math.rint(value * scale) / scale);
661
662            append((int) value);
663
664            if (precision != 0) {
665                append(".");
666                value = Math.abs(value);
667                value -= Math.floor(value);
668                append((int) (value * scale), precision);
669            }
670
671            return this;
672        }
673
674        @Override
675        public String toString() {
676            return new String(mChars, 0, mLength);
677        }
678
679        private int reserve(int length) {
680            final int oldLength = mLength;
681            final int newLength = mLength + length;
682            final char[] oldChars = mChars;
683            final int oldCapacity = oldChars.length;
684            if (newLength > oldCapacity) {
685                final int newCapacity = oldCapacity * 2;
686                final char[] newChars = new char[newCapacity];
687                System.arraycopy(oldChars, 0, newChars, 0, oldLength);
688                mChars = newChars;
689            }
690            return oldLength;
691        }
692    }
693}
694