Introduction to the

We can set the Android: EllipSize =”marquee” property of the TextView to make it look like a marquee when the text is longer than one line. But the TextView requires focus, and only one control can get focus at a time. More importantly, the product requires scrolling regardless of whether the text goes beyond one line.

Here are the Github addresses and renderings of the final implementation

Github.com/dreamgyf/Ma…

Train of thought

The idea is actually very simple, as long as we will be a single line of TextView cut into a Bitmap, and then we will customize a View, rewrite its onDraw method, every once in a while, the Bitmap drawn in different coordinates (left and right sides of each draw once), so continuous looks like a horse-light effect.

Later, after discussing with my colleague, he suggested whether this function could be realized by using Canvas translation and drawText. I think it is also possible, but I did not try it. You may be interested in trying this scheme.

implementation

We first define a View inherited from AppCompatTextView, and then initialize a new TextView and rewrite the onMeasure and onLayout methods

private void init(a) {
    mTextView = new TextView(getContext(), attrs);
    //TextView without LayoutParams will crash NPE when setText is set
    mTextView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    mTextView.setMaxLines(1);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // The width is unlimited
    mTextView.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    // Make sure the layout contains the full Text content
    mTextView.layout(left, top, left + mTextView.getMeasuredWidth(), bottom);
}
Copy the code

This is to use the internal TextView to generate the Bitmap we need and borrow the onMeasure method written by TextView so that we don’t have to rewrite the onMeasure method so complicated

The next step is to generate the Bitmap

private void updateBitmap(a) {
    mBitmap = Bitmap.createBitmap(mTextView.getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(mBitmap);
    mTextView.draw(canvas);
}
Copy the code

If getWidth is used, the maximum value will be the width of the screen, which may result in an incomplete Bitmap. If getWidth is used, the measuredWidth of the screen will be used

The Bitmap needs to be updated and redrawn every time a setText or setTextSize is set

private void init(a) {
    mTextView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { updateBitmap(); restartScroll(); }}); }@Override
public void setText(CharSequence text, BufferType type) {
    super.setText(text, type);
    // When the parent constructor is executed, if the AttributeSet has a text argument, the setText is called first, and the mTextView is not initialized
    if(mTextView ! =null) { mTextView.setText(text); requestLayout(); }}@Override
public void setTextSize(int unit, float size) {
    super.setTextSize(unit, size);
    // When the parent constructor is executed, setTextSize is called first if the AttributeSet has textSize, and the mTextView is not initialized
    if(mTextView ! =null) { mTextView.setTextSize(size); requestLayout(); }}Copy the code

Next, I define some parameters for the MarqueeTextView, one is space (the minimum distance between the top and bottom of the text when scrolling), and the other is speed (the speed of the text when scrolling).

Let’s take a look at the implementation of onDraw

@Override
protected void onDraw(Canvas canvas) {
    if(mBitmap ! =null) {
        // No more than one line of text
	if (mTextView.getMeasuredWidth() <= getWidth()) {
            // Calculate the width between the head and tail
            int space = mSpace - (getWidth() - mTextView.getMeasuredWidth());
            if (space < 0) {
                space = 0;
            }

            // When the drawBitmap on the left exceeds the display width + interval width, i.e. after a loop, the Bitmap on the right has moved to the left, reset the coordinates
            if (mLeftX < -getWidth() - space) {
                mLeftX += getWidth() + space;
            }

            // Draw the left bitmap
            canvas.drawBitmap(mBitmap, mLeftX, 0, getPaint());
            if (mLeftX < 0) {
                // Draw the right bitmap with the rightmost coordinate - the width of the left bitmap that has disappeared + the width of the interval
                canvas.drawBitmap(mBitmap, getWidth() + mLeftX + space, 0, getPaint()); }}else {
            // When the text content exceeds one line
            // When the drawBitmap on the left exceeds the content width + interval width, that is, after completing a loop, the Bitmap on the right has moved to the far left, reset the coordinates
            if (mLeftX < -mTextView.getMeasuredWidth() - mSpace) {
		mLeftX += mTextView.getMeasuredWidth() + mSpace;
            }

            // Draw the left bitmap
            canvas.drawBitmap(mBitmap, mLeftX, 0, getPaint());
            // When the tail is already visible
            if (mLeftX + (mTextView.getMeasuredWidth() - getWidth()) < 0) {
                // Draw the bitmap on the right. The position is the coordinate of the tail + the spacing width
		canvas.drawBitmap(mBitmap, mTextView.getMeasuredWidth() + mLeftX + mSpace, 0, getPaint()); }}}}Copy the code

So that’s the basic idea

And then I need to get it moving, so I’m using Handler here, and every once in a while I’ll update the coordinates and redraw them

private final Runnable mMarqueeRunnable = new Runnable() {
    @Override
    public void run(a) {
	invalidate();
	mLeftX -= mSpeed;
	mHandler.postDelayed(this.15); }};public void startScroll(a) {
    mHandler.post(mMarqueeRunnable);
}

public void pauseScroll(a) {
    mHandler.removeCallbacks(mMarqueeRunnable);
}

public void stopScroll(a) {
    mLeftX = 0;
    mHandler.removeCallbacks(mMarqueeRunnable);
}

public void restartScroll(a) {
    stopScroll();
    startScroll();
}
Copy the code

Finally, cancel the HandlerMessage when the View removes the Window

@Override
protected void onDetachedFromWindow(a) {
    super.onDetachedFromWindow();
    mHandler.removeCallbacksAndMessages(null);
}
Copy the code