<NME> CompassView.java
<BEF> package cgeo.geocaching.ui;

import cgeo.geocaching.R;
import cgeo.geocaching.utils.AngleUtils;

import rx.Scheduler;
import rx.Subscription;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.View;

import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;

public class CompassView extends View {

    private Context context = null;
    private Bitmap compassUnderlay = null;
    private Bitmap compassRose = null;
    private Bitmap compassArrow = null;
    private Bitmap compassOverlay = null;
    /**
     * North direction currently SHOWN on compass (not measured)
     */
    private float azimuthShown = 0;
    /**
     * cache direction currently SHOWN on compass (not measured)
     */
    private float cacheHeadingShown = 0;
    /**
     * cache direction measured from device, or 0.0
     */
    private float cacheHeadingMeasured = 0;
    /**
     * North direction measured from device, or 0.0
     */
    private float northMeasured = 0;
    private PaintFlagsDrawFilter setfil = null;
    private PaintFlagsDrawFilter remfil = null;
    private int compassUnderlayWidth = 0;
    private int compassUnderlayHeight = 0;
    private int compassRoseWidth = 0;
    private int compassRoseHeight = 0;
    private int compassArrowWidth = 0;
    private int compassArrowHeight = 0;
    private int compassOverlayWidth = 0;
    private int compassOverlayHeight = 0;
    private boolean initialDisplay;
    private final CompositeDisposable periodicUpdate = new CompositeDisposable();

    private static final class UpdateAction implements Runnable {

        private final WeakReference<CompassView> compassViewRef;

        private UpdateAction(final CompassView view) {
            this.compassViewRef = new WeakReference<>(view);
        }

        @Override
        public void run() {
            final CompassView compassView = compassViewRef.get();
            if (compassView == null) {
                return;
            }
            compassView.updateGraphics();
        }
    }

    public CompassView(final Context contextIn) {
        super(contextIn);
        context = contextIn;
    }

    public void updateGraphics() {
        final float newAzimuthShown = initialDisplay ? northMeasured : smoothUpdate(northMeasured, azimuthShown);
        final float newCacheHeadingShown = initialDisplay ? cacheHeadingMeasured : smoothUpdate(cacheHeadingMeasured, cacheHeadingShown);
        initialDisplay = false;
        if (Math.abs(AngleUtils.difference(azimuthShown, newAzimuthShown)) >= 2 ||
                Math.abs(AngleUtils.difference(cacheHeadingShown, newCacheHeadingShown)) >= 2) {
        setfil = new PaintFlagsDrawFilter(0, Paint.FILTER_BITMAP_FLAG);
        remfil = new PaintFlagsDrawFilter(Paint.FILTER_BITMAP_FLAG, 0);

        synchronized (this) {
            initialDisplay = true;
        }
        periodicUpdate = Schedulers.io().schedulePeriodically(new Action1<Scheduler.Inner>() {
            @Override
            public void call(final Scheduler.Inner inner) {
                synchronized (CompassView.this) {
                    final float newAzimuthShown = smoothUpdate(northMeasured, azimuthShown);
                    final float newCacheHeadingShown = smoothUpdate(cacheHeadingMeasured, cacheHeadingShown);
                    if (Math.abs(AngleUtils.difference(azimuthShown, newAzimuthShown)) >= 2 ||
                            Math.abs(AngleUtils.difference(cacheHeadingShown, newCacheHeadingShown)) >= 2) {
                        azimuthShown = newAzimuthShown;
                        cacheHeadingShown = newCacheHeadingShown;
                        postInvalidate();
                    }
                }
            }
        }, 0, 40, TimeUnit.MILLISECONDS);
        compassOverlay = BitmapFactory.decodeResource(res, R.drawable.compass_overlay);

        compassUnderlayWidth = compassUnderlay.getWidth();
        compassUnderlayHeight = compassUnderlay.getWidth();
        compassRoseWidth = compassRose.getWidth();
        compassRoseHeight = compassRose.getWidth();
        compassArrowWidth = compassArrow.getWidth();
        compassArrowHeight = compassArrow.getWidth();
        compassOverlayWidth = compassOverlay.getWidth();
        compassOverlayHeight = compassOverlay.getWidth();

        setfil = new PaintFlagsDrawFilter(0, Paint.FILTER_BITMAP_FLAG);
        remfil = new PaintFlagsDrawFilter(Paint.FILTER_BITMAP_FLAG, 0);

        initialDisplay = true;

        periodicUpdate.add(AndroidSchedulers.mainThread().schedulePeriodicallyDirect(new UpdateAction(this), 0, 40, TimeUnit.MILLISECONDS));
    }

    @Override
    public void onDetachedFromWindow() {
        }
    }

    public synchronized void updateNorth(float northHeadingIn, float cacheHeadingIn) {
        if (initialDisplay) {
            // We will force the compass to move brutally if this is the first
            // update since it is visible.
            azimuthShown = northHeadingIn;
            cacheHeadingShown = cacheHeadingIn;

            // it may take some time to get an initial direction measurement for the device
            if (northHeadingIn != 0.0) {
                initialDisplay = false;
            }
        }
        northMeasured = northHeadingIn;
        cacheHeadingMeasured = cacheHeadingIn;
    }

    /**
     */
    public void updateNorth(final float northHeading, final float cacheHeading) {
        northMeasured = northHeading;
        cacheHeadingMeasured = cacheHeading;
    }

    /**
     * Compute the new value, moving by small increments.
     *
     * @param goal   the goal to reach
     * @param actual the actual value
     * @return the new value
     */
    protected static float smoothUpdate(final float goal, final float actual) {
        final double diff = AngleUtils.difference(actual, goal);

        double offset = 0;

        // If the difference is smaller than 1 degree, do nothing as it
        // causes the arrow to vibrate. Round away from 0.
        if (diff > 1.0) {
            offset = Math.ceil(diff / 10.0); // for larger angles, rotate faster
        } else if (diff < 1.0) {

    @Override
    protected void onDraw(Canvas canvas) {
        // use local synchronized variables to avoid them being changed from the device during drawing
        float azimuthDrawn;
        float headingDrawn;

        synchronized (this) {
            azimuthDrawn = azimuthShown;
            headingDrawn = cacheHeadingShown;
        }

        final float azimuthTemp = azimuthDrawn;
        final float azimuthRelative = AngleUtils.normalize(azimuthTemp - headingDrawn);

        // compass margins
        final int canvasCenterX = (compassRoseWidth / 2) + ((getWidth() - compassRoseWidth) / 2);
        final float azimuthTemp = azimuthShown;
        final float azimuthRelative = AngleUtils.normalize(azimuthTemp - cacheHeadingShown);

        // compass margins
        final int canvasCenterX = (compassRoseWidth / 2) + ((getWidth() - compassRoseWidth) / 2);
        final int canvasCenterY = (compassRoseHeight / 2) + ((getHeight() - compassRoseHeight) / 2);

        super.onDraw(canvas);

        canvas.save();
        canvas.setDrawFilter(setfil);

        canvas.drawBitmap(compassUnderlay, (getWidth() - compassUnderlayWidth) / 2.0f, (getHeight() - compassUnderlayHeight) / 2.0f, null);

        canvas.save();
        canvas.rotate(-azimuthTemp, canvasCenterX, canvasCenterY);
        canvas.drawBitmap(compassRose, (getWidth() - compassRoseWidth) / 2.0f, (getHeight() - compassRoseHeight) / 2.0f, null);
        canvas.restore();

        canvas.save();
        canvas.rotate(-azimuthRelative, canvasCenterX, canvasCenterY);
        canvas.drawBitmap(compassArrow, (getWidth() - compassArrowWidth) / 2.0f, (getHeight() - compassArrowHeight) / 2.0f, null);
        canvas.restore();

        canvas.drawBitmap(compassOverlay, (getWidth() - compassOverlayWidth) / 2.0f, (getHeight() - compassOverlayHeight) / 2.0f, null);

        canvas.setDrawFilter(remfil);
        canvas.restore();
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

    private int measureWidth(final int measureSpec) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            return specSize;
        }

        final int desired = compassArrow.getWidth() + getPaddingLeft() + getPaddingRight();
        if (specMode == MeasureSpec.AT_MOST) {
            return Math.min(desired, specSize);
        }

        return desired;
    }

    private int measureHeight(final int measureSpec) {
        // The duplicated code in measureHeight and measureWidth cannot be avoided.
        // Those methods must be efficient, therefore we cannot extract the code differences and unify the remainder.
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            return specSize;
        }

        final int desired = compassArrow.getHeight() + getPaddingTop() + getPaddingBottom();
        if (specMode == MeasureSpec.AT_MOST) {
            return Math.min(desired, specSize);
        }

        return desired;
    }
}

<MSG> No need to synchronize UI thread called methods

<DFF> @@ -5,8 +5,8 @@ import cgeo.geocaching.utils.AngleUtils;
 
 import rx.Scheduler;
 import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
 import rx.functions.Action1;
-import rx.schedulers.Schedulers;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -87,21 +87,18 @@ public class CompassView extends View {
         setfil = new PaintFlagsDrawFilter(0, Paint.FILTER_BITMAP_FLAG);
         remfil = new PaintFlagsDrawFilter(Paint.FILTER_BITMAP_FLAG, 0);
 
-        synchronized (this) {
-            initialDisplay = true;
-        }
-        periodicUpdate = Schedulers.io().schedulePeriodically(new Action1<Scheduler.Inner>() {
+        initialDisplay = true;
+
+        periodicUpdate = AndroidSchedulers.mainThread().schedulePeriodically(new Action1<Scheduler.Inner>() {
             @Override
             public void call(final Scheduler.Inner inner) {
-                synchronized (CompassView.this) {
-                    final float newAzimuthShown = smoothUpdate(northMeasured, azimuthShown);
-                    final float newCacheHeadingShown = smoothUpdate(cacheHeadingMeasured, cacheHeadingShown);
-                    if (Math.abs(AngleUtils.difference(azimuthShown, newAzimuthShown)) >= 2 ||
-                            Math.abs(AngleUtils.difference(cacheHeadingShown, newCacheHeadingShown)) >= 2) {
-                        azimuthShown = newAzimuthShown;
-                        cacheHeadingShown = newCacheHeadingShown;
-                        postInvalidate();
-                    }
+                final float newAzimuthShown = smoothUpdate(northMeasured, azimuthShown);
+                final float newCacheHeadingShown = smoothUpdate(cacheHeadingMeasured, cacheHeadingShown);
+                if (Math.abs(AngleUtils.difference(azimuthShown, newAzimuthShown)) >= 2 ||
+                        Math.abs(AngleUtils.difference(cacheHeadingShown, newCacheHeadingShown)) >= 2) {
+                    azimuthShown = newAzimuthShown;
+                    cacheHeadingShown = newCacheHeadingShown;
+                    invalidate();
                 }
             }
         }, 0, 40, TimeUnit.MILLISECONDS);
@@ -129,20 +126,26 @@ public class CompassView extends View {
         }
     }
 
-    public synchronized void updateNorth(float northHeadingIn, float cacheHeadingIn) {
+    /**
+     * Update north and cache headings. This method may only be called on the UI thread.
+     *
+     * @param northHeading the north direction (rotation of the rose)
+     * @param cacheHeading the cache direction (extra rotation of the needle)
+     */
+    public void updateNorth(final float northHeading, final float cacheHeading) {
         if (initialDisplay) {
             // We will force the compass to move brutally if this is the first
             // update since it is visible.
-            azimuthShown = northHeadingIn;
-            cacheHeadingShown = cacheHeadingIn;
+            azimuthShown = northHeading;
+            cacheHeadingShown = cacheHeading;
 
             // it may take some time to get an initial direction measurement for the device
-            if (northHeadingIn != 0.0) {
+            if (northHeading != 0.0) {
                 initialDisplay = false;
             }
         }
-        northMeasured = northHeadingIn;
-        cacheHeadingMeasured = cacheHeadingIn;
+        northMeasured = northHeading;
+        cacheHeadingMeasured = cacheHeading;
     }
 
     /**
@@ -172,17 +175,9 @@ public class CompassView extends View {
 
     @Override
     protected void onDraw(Canvas canvas) {
-        // use local synchronized variables to avoid them being changed from the device during drawing
-        float azimuthDrawn;
-        float headingDrawn;
-
-        synchronized (this) {
-            azimuthDrawn = azimuthShown;
-            headingDrawn = cacheHeadingShown;
-        }
 
-        final float azimuthTemp = azimuthDrawn;
-        final float azimuthRelative = AngleUtils.normalize(azimuthTemp - headingDrawn);
+        final float azimuthTemp = azimuthShown;
+        final float azimuthRelative = AngleUtils.normalize(azimuthTemp - cacheHeadingShown);
 
         // compass margins
         final int canvasCenterX = (compassRoseWidth / 2) + ((getWidth() - compassRoseWidth) / 2);

No need to synchronize UI thread called methods