2

Its been a week I was trying to create a watch Face for Android wear. As a kick start I followed Google official documentation and found these Android official watch face app tutorial with source code

So my current issue is , In Google documentation they use canvas to create analogue watch faces . The watch hands are generated using paint

Here is the sample of code for creating dial hand

    public class AnalogWatchFaceService extends CanvasWatchFaceService {
private static final String TAG = "AnalogWatchFaceService";

/**
 * Update rate in milliseconds for interactive mode. We update once a second to advance the
 * second hand.
 */
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);

@Override
public Engine onCreateEngine() {
    return new Engine();
}

private class Engine extends CanvasWatchFaceService.Engine {
    static final int MSG_UPDATE_TIME = 0;

    static final float TWO_PI = (float) Math.PI * 2f;

    Paint mHourPaint;
    Paint mMinutePaint;
    Paint mSecondPaint;
    Paint mTickPaint;
    boolean mMute;
    Calendar mCalendar;

    /** Handler to update the time once a second in interactive mode. */
    final Handler mUpdateTimeHandler = new Handler() {
        @Override
        public void handleMessage(Message message) {
            switch (message.what) {
                case MSG_UPDATE_TIME:
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "updating time");
                    }
                    invalidate();
                    if (shouldTimerBeRunning()) {
                        long timeMs = System.currentTimeMillis();
                        long delayMs = INTERACTIVE_UPDATE_RATE_MS
                                - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
                        mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
                    }
                    break;
            }
        }
    };

    final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            mCalendar.setTimeZone(TimeZone.getDefault());
            invalidate();
        }
    };
    boolean mRegisteredTimeZoneReceiver = false;

    /**
     * Whether the display supports fewer bits for each color in ambient mode. When true, we
     * disable anti-aliasing in ambient mode.
     */
    boolean mLowBitAmbient;

    Bitmap mBackgroundBitmap;
    Bitmap mBackgroundScaledBitmap;

    @Override
    public void onCreate(SurfaceHolder holder) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "onCreate");
        }
        super.onCreate(holder);

        setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this)
                .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
                .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
                .setShowSystemUiTime(false)
                .build());

        Resources resources = AnalogWatchFaceService.this.getResources();
        Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg, null /* theme */);
        mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();

        mHourPaint = new Paint();
        mHourPaint.setARGB(255, 200, 200, 200);
        mHourPaint.setStrokeWidth(5.f);
        mHourPaint.setAntiAlias(true);
        mHourPaint.setStrokeCap(Paint.Cap.ROUND);

        mMinutePaint = new Paint();
        mMinutePaint.setARGB(255, 200, 200, 200);
        mMinutePaint.setStrokeWidth(3.f);
        mMinutePaint.setAntiAlias(true);
        mMinutePaint.setStrokeCap(Paint.Cap.ROUND);

        mSecondPaint = new Paint();
        mSecondPaint.setARGB(255, 255, 0, 0);
        mSecondPaint.setStrokeWidth(2.f);
        mSecondPaint.setAntiAlias(true);
        mSecondPaint.setStrokeCap(Paint.Cap.ROUND);

        mTickPaint = new Paint();
        mTickPaint.setARGB(100, 255, 255, 255);
        mTickPaint.setStrokeWidth(2.f);
        mTickPaint.setAntiAlias(true);

        mCalendar = Calendar.getInstance();
    }

    @Override
    public void onDestroy() {
        mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
        super.onDestroy();
    }

    @Override
    public void onPropertiesChanged(Bundle properties) {
        super.onPropertiesChanged(properties);
        mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
        }
    }

    @Override
    public void onTimeTick() {
        super.onTimeTick();
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
        }
        invalidate();
    }

    @Override
    public void onAmbientModeChanged(boolean inAmbientMode) {
        super.onAmbientModeChanged(inAmbientMode);
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
        }
        if (mLowBitAmbient) {
            boolean antiAlias = !inAmbientMode;
            mHourPaint.setAntiAlias(antiAlias);
            mMinutePaint.setAntiAlias(antiAlias);
            mSecondPaint.setAntiAlias(antiAlias);
            mTickPaint.setAntiAlias(antiAlias);
        }
        invalidate();

        // Whether the timer should be running depends on whether we're in ambient mode (as well
        // as whether we're visible), so we may need to start or stop the timer.
        updateTimer();
    }

    @Override
    public void onInterruptionFilterChanged(int interruptionFilter) {
        super.onInterruptionFilterChanged(interruptionFilter);
        boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
        if (mMute != inMuteMode) {
            mMute = inMuteMode;
            mHourPaint.setAlpha(inMuteMode ? 100 : 255);
            mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
            mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
            invalidate();
        }
    }

    @Override
    public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        if (mBackgroundScaledBitmap == null
                || mBackgroundScaledBitmap.getWidth() != width
                || mBackgroundScaledBitmap.getHeight() != height) {
            mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
                    width, height, true /* filter */);
        }
        super.onSurfaceChanged(holder, format, width, height);
    }

    @Override
    public void onDraw(Canvas canvas, Rect bounds) {
        mCalendar.setTimeInMillis(System.currentTimeMillis());

        int width = bounds.width();
        int height = bounds.height();

        // Draw the background, scaled to fit.
        canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);

        // Find the center. Ignore the window insets so that, on round watches with a
        // "chin", the watch face is centered on the entire screen, not just the usable
        // portion.
        float centerX = width / 2f;
        float centerY = height / 2f;

        // Draw the ticks.
        float innerTickRadius = centerX - 10;
        float outerTickRadius = centerX;
        for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
            float tickRot = tickIndex * TWO_PI / 12;
            float innerX = (float) Math.sin(tickRot) * innerTickRadius;
            float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
            float outerX = (float) Math.sin(tickRot) * outerTickRadius;
            float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
            canvas.drawLine(centerX + innerX, centerY + innerY,
                    centerX + outerX, centerY + outerY, mTickPaint);
        }

        float seconds =
                mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f;
        float secRot = seconds / 60f * TWO_PI;
        float minutes = mCalendar.get(Calendar.MINUTE) + seconds / 60f;
        float minRot = minutes / 60f * TWO_PI;
        float hours = mCalendar.get(Calendar.HOUR) + minutes / 60f;
        float hrRot = hours / 12f * TWO_PI;

        float secLength = centerX - 20;
        float minLength = centerX - 40;
        float hrLength = centerX - 80;

        if (!isInAmbientMode()) {
            float secX = (float) Math.sin(secRot) * secLength;
            float secY = (float) -Math.cos(secRot) * secLength;
            canvas.drawLine(centerX, centerY, centerX + secX, centerY + secY, mSecondPaint);
        }

        float minX = (float) Math.sin(minRot) * minLength;
        float minY = (float) -Math.cos(minRot) * minLength;
        canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY, mMinutePaint);

        float hrX = (float) Math.sin(hrRot) * hrLength;
        float hrY = (float) -Math.cos(hrRot) * hrLength;
        canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHourPaint);
    }

}

The entire code can be found inside the official sample app . Below you can find the screen shot of application which I made using Google official tutorial . enter image description here

If anyone have any idea how to replace the clock hands with an drawable images ? . Any help would be appreciated .

Nikhil
  • 911
  • 15
  • 28
  • read the `Canvas` API, namely `drawBitmap()`, `rotate()` / `concat()` or just `drawBitmap()` with a `Matrix` parameter – pskink Jul 31 '15 at 11:14
  • it will be helpful if you be more specific please. – Nikhil Jul 31 '15 at 11:17
  • i am specific: i don't mean reading the whole class documentation, i pointed what methods to study... – pskink Jul 31 '15 at 11:20
  • i just tried what you were pointed out , the problem is for every canvas.drawBitmap() must include a **paint** object , where what i really need to add **drawable** instead of paint – Nikhil Jul 31 '15 at 11:28
  • "public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint) ... paint The paint used to draw the bitmap (**may be null**)" read the docs carefuly – pskink Jul 31 '15 at 11:30
  • ya i already did . Let me go deep in to it and i will get back soon – Nikhil Jul 31 '15 at 11:33
  • "have a look", at what? – pskink Aug 03 '15 at 09:21
  • Sorry , i just updated the code . please have a look – Nikhil Aug 03 '15 at 11:05
  • Instead of **canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHourPaint);** i tried **canvas.drawBitmap(bitmap,mCenterX,mCenterY,null);** .Even tho its work Unfortunately its cant work like a hand its just go parallel threw screen – Nikhil Aug 03 '15 at 11:09
  • just use `canvas.drawBitmap(bitmap, matrix, null)`, of course you have to setup the `Matrix`, but first try with an unmodified `Matrix`, then try to `postTranslate` the matrix so that the pivot is on the location [0, 0], for example in image: http://pasteboard.co/2rPyWVa5.png the pivot is a small red dot, when you have the pivot at location [0, 0] call `postRotate(degrees)` and finally `postTranslate` again to position it wherever you want – pskink Aug 03 '15 at 17:53

3 Answers3

2

Create a Bitmap of your drawable resource:

Bitmap hourHand = BitmapFactory.decodeResource(context.getResources(), R.drawable.hour_hand);

Do whatever transformations you need to your canvas and draw the bitmap:

canvas.save();
canvas.rotate(degrees, px, py);
canvas.translate(dx, dy);
canvas.drawBitmap(hourHand, centerX, centerY, null); // Or use a Paint if you need it
canvas.restore();
TofferJ
  • 4,678
  • 1
  • 37
  • 49
  • i tried you code but the problem i am facing is that the bitmap is not showing in center position . its showing around the corner – Nikhil Aug 03 '15 at 07:26
  • Then you need to change the values for the call to the translate and/or rotate method. You could either rotate the bitmap first and then move it, or the other way around. Just keep in mind what center point you use for the rotation. Typically if you have an asset representing a hand you would want to rotate it around the bottom - not the center. If you start by moving it to the center of the screen, then it's easy to visualize how the rotation will affect it. – TofferJ Aug 03 '15 at 15:15
  • You probably need to translate the bitmap by half its width and a little less than its height before rotating it (assuming the image of the watch hand is vertical). Then, draw it at the center of the screen or translate the canvas to the center and draw it at 0,0. – joshbodily Sep 03 '15 at 20:29
1

Use following method to rotate bitmap from canvas,

/**
 * To rotate bitmap on canvas
 *
 * @param canvas         : canvas on which you are drawing
 * @param handBitmap     : bitmap of hand
 * @param centerPoint    : center for rotation
 * @param rotation       : rotation angle in form of seconds
 * @param offset         : offset of bitmap from center point (If not needed then keep it 0)
 */

public void rotateBitmap(Canvas canvas, Bitmap handBitmap, PointF centerPoint, float rotation, float offset) {
    canvas.save();
    canvas.rotate(secondRotation - 90, centerPoint.x, centerPoint.y);
    canvas.drawBitmap(handBitmap, centerPoint.x - offset, centerPoint.y - handBitmap.getHeight() / Constants.INTEGER_TWO, new Paint(Paint.FILTER_BITMAP_FLAG));
    canvas.restore();
}
Umang Kothari
  • 3,674
  • 27
  • 36
0

I am a little bit late with the answer, but maybe it can be helpful for others

canvas.save()

val antialias = Paint()
antialias.isAntiAlias = true
antialias.isFilterBitmap = true
antialias.isDither = true

canvas.rotate(secondsRotation - minutesRotation, centerX, centerY)
canvas.drawBitmap(
     secondsHandBitmap,
     centerX - 10,
     centerY - 160,
     antialias
)
canvas.restore()

Here is my public Git Repo you can check the source code

Viktor Apoyan
  • 10,655
  • 22
  • 85
  • 147