0

I have a custom view that has a blinking cursor. I make the blinking cursor using a Handler and posting a Runnable to it after a 500 milisecond delay.

When the activity that the view is in, I want to stop the blinking by removing the callbacks on the handler. However, I've noticed that when I switch to another app, the handler/runnable keep going, ie, the log says it is still blinking.

If I had control of the view I would just do something like this

@Override
protected void onPause() {
     handler.removeCallbacks(runnable);
     super.onPause();
}

But my custom view will be part of a library and so I don't have control over the Activities that other developers use my custom view in.

I tried onFocusChanged, onScreenStateChanged, and onDetachedFromWindow but none of these work for when the user switches to another app.

Here is my code. I simplified it by removing anything not pertinent to the problem.

public class MyCustomView extends View {

    static final int BLINK = 500;
    private Handler mBlinkHandler;

    private void init() {
        // ...
        mBlinkHandler = new Handler();

        mTextStorage.setOnChangeListener(new MongolTextStorage.OnChangeListener() {
            @Override
            public void onTextChanged(/*...*/) {
                // ...
                startBlinking();
            }
        });
    }

    Runnable mBlink = new Runnable() {
        @Override
        public void run() {
            mBlinkHandler.removeCallbacks(mBlink);
            if (shouldBlink()) {
                // ...
                Log.i("TAG", "Still blinking...");
                mBlinkHandler.postDelayed(mBlink, BLINK);
            }
        }
    };

    private boolean shouldBlink() {
        if (!mCursorVisible || !isFocused()) return false;
        final int start = getSelectionStart();
        if (start < 0) return false;
        final int end = getSelectionEnd();
        if (end < 0) return false;
        return start == end;
    }

    void startBlinking() {
        mBlink.run();
    }

    void stopBlinking() {
        mBlinkHandler.removeCallbacks(mBlink);
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        if (focused) {
            startBlinking();
        } else {
            stopBlinking();
        }
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
    }

    @Override
    public void onScreenStateChanged(int screenState) {
        switch (screenState) {
            case View.SCREEN_STATE_ON:
                startBlinking();
                break;
            case View.SCREEN_STATE_OFF:
                stopBlinking();
                break;
        }
    }

    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        startBlinking();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopBlinking();
    }
}
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • hope this helps: http://androidxref.com/1.6/xref/frameworks/base/core/java/android/widget/TextView.java#6607 – pskink Jul 26 '17 at 09:32
  • @pskink, good idea. I need to go back to the Android source code and study it more. Any reason you linked to the older version of `TextView` rather than the current one? Is it because it was a simpler implementation back then? – Suragch Jul 26 '17 at 10:31
  • yes, exactly, now its... i dont know really how it works now (i was too lazy to dig into it) - there is something like `mEditor.makeBlink()` so probably it makes something – pskink Jul 26 '17 at 10:34
  • @pskink, I finally got around to following your suggestion and got it working. Thank you. – Suragch Apr 29 '18 at 05:28
  • good, seems your project is not in a hurry (my comment was made 9 months ago) ;-) – pskink Apr 29 '18 at 05:43
  • @pskink, right, most users didn't notice the cursor blinking invisibly in the background. : ) – Suragch Apr 29 '18 at 06:01

2 Answers2

0

I guess you are starting the thread separately using thread.run(), instead just make a method and call it recursively Something like this:

public void blink(){
     mBlinkHandler.postDelayed(mBlink, BLINK);
} 

And in the runnable:

Runnable mBlink = new Runnable() {
    @Override
    public void run() {
        mBlinkHandler.removeCallbacks(mBlink);
        if (shouldBlink()) {
            // ...
            Log.i("TAG", "Still blinking...");
           blink();
        }
    }
};

As you are directly starting the thread using run method. So it won't stop by removing callbacks.

Hope this helps.

Sarthak Gandhi
  • 2,130
  • 1
  • 16
  • 23
  • It does stop it by removing callbacks in other situations (like losing focus or closing the activity), so I don't think it is just because I start it initially with `run`. – Suragch Jul 26 '17 at 10:27
0

I solved the problem by following @pskink's advice in the comments and adapted the code from Android 1.6. This may be an old version of Android but the blinking cursor part works well for my purposes. Overriding onWindowFocusChanged was the key.

My full code is on GitHub. Here are the pertinent parts:

public class MyCustomView extends View {

    private boolean mCursorVisible = true;
    private Blink mBlink;
    private long mShowCursor; // cursor blink timing based on system clock
    static final int BLINK = 500;

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        mShowCursor = SystemClock.uptimeMillis();
        if (focused) {
            makeBlink();
        }
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        int start = getSelectionStart();
        int end = getSelectionEnd();

        // draw the blinking cursor on top
        if (start == end && blinkShouldBeOn()) {
            canvas.drawRect(getCursorPath(start), mCursorPaint);
        }
    }

    private boolean blinkShouldBeOn() {
        if (!mCursorVisible || !isFocused()) return false;
        return (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK;
    }
    private void makeBlink() {
        if (!mCursorVisible) {
            if (mBlink != null) {
                mBlink.removeCallbacks(mBlink);
            }

            return;
        }

        if (mBlink == null)
            mBlink = new Blink(this);

        mBlink.removeCallbacks(mBlink);
        mBlink.postAtTime(mBlink, mShowCursor + BLINK);
    }

    public void setCursorVisible(boolean visible) {
        mCursorVisible = visible;
        invalidateCursorPath();

        if (visible) {
            makeBlink();
        } else if (mBlink != null) {
            mBlink.removeCallbacks(mBlink);
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);

        if (hasWindowFocus) {
            if (mBlink != null) {
                mBlink.uncancel();

                if (isFocused()) {
                    mShowCursor = SystemClock.uptimeMillis();
                    makeBlink();
                }
            }
        } else {
            if (mBlink != null) {
                mBlink.cancel();
            }
            hideSystemKeyboard();
        }
    }

    private static class Blink extends Handler implements Runnable {
        private WeakReference<MongolEditText> mView;
        private boolean mCancelled;

        Blink(MongolEditText v) {
            mView = new WeakReference<>(v);
        }

        public void run() {
            if (mCancelled) {
                return;
            }

            removeCallbacks(Blink.this);

            MongolEditText met = mView.get();

            if (met != null && met.isFocused()) {
                int st = met.getSelectionStart();
                int en = met.getSelectionEnd();

                if (st == en && st >= 0 && en >= 0) {
                    if (met.mLayout != null) {
                        met.invalidateCursorPath();
                    }

                    postAtTime(this, SystemClock.uptimeMillis() + BLINK);
                }
            }
        }

        void cancel() {
            if (!mCancelled) {
                removeCallbacks(Blink.this);
                mCancelled = true;
            }
        }

        void uncancel() {
            mCancelled = false;
        }
    }
}
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393