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