This solution works up until API 28 when they introduced restrictions on non-SDK interfaces.
I ran into the same problem and I couldn't find a solution online. I managed to solve the problem myself by looking at the source code for TextView and referencing other answers that provide an option to control the scroll speed.
Looking at the source code, it will set the gap or blank space to be 1/3 the size of your TextView. So if your TextView is 600 pixels wide, it will create a 200 pixel gap between the start and end of the message. This solution will aim to make that value configurable.
The tricky part is that the logic you need to override is all within private classes, fields, and methods on the super class. This makes the solution far less trivial.
Add this class to your project... CustomTextView.java
public class CustomTextView extends AppCompatTextView {
private static final float NEW_GAP = 0F;
Object marqueeObject;
Field mStatusField;
Field mGhostStartField;
Field mMaxScrollField;
Field mGhostOffsetField;
Field mFadeStopField;
Field mMaxFadeScrollField;
public CustomTextView(Context context) {
super(context);
setSelected(true);
}
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setSelected(true);
}
public CustomTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setSelected(true);
}
@Override
protected void onDraw(Canvas canvas) {
try {
initMarqueeObject();
if (didMarqueeRestart()) {
// We need to update the values each time it restarts
updateMarqueeFieldValues();
}
}
catch(Exception exception) {
exception.printStackTrace();
}
super.onDraw(canvas);
}
private void initMarqueeObject() throws Exception {
if (marqueeObject == null) {
Field marqueeField = getClass().getSuperclass().getSuperclass().getDeclaredField("mMarquee"); // use if extending from AppCompatTextView
// Field marqueeField = getClass().getSuperclass().getDeclaredField("mMarquee"); // use if extending from TextView
marqueeField.setAccessible(true);
marqueeObject = marqueeField.get(this);
initMarqueeFields();
}
}
private void initMarqueeFields() throws Exception {
mStatusField = marqueeObject.getClass().getDeclaredField("mStatus");
mStatusField.setAccessible(true);
mGhostStartField = marqueeObject.getClass().getDeclaredField("mGhostStart");
mGhostStartField.setAccessible(true);
mMaxScrollField = marqueeObject.getClass().getDeclaredField("mMaxScroll");
mMaxScrollField.setAccessible(true);
mGhostOffsetField = marqueeObject.getClass().getDeclaredField("mGhostOffset");
mGhostOffsetField.setAccessible(true);
mFadeStopField = marqueeObject.getClass().getDeclaredField("mFadeStop");
mFadeStopField.setAccessible(true);
mMaxFadeScrollField = marqueeObject.getClass().getDeclaredField("mMaxFadeScroll");
mMaxFadeScrollField.setAccessible(true);
}
private boolean didMarqueeRestart() throws Exception {
byte currentState = mStatusField.getByte(marqueeObject);
return currentState == 0x1; // 0x1 is the byte object for the MARQUEE_STARTING state
}
private void updateMarqueeFieldValues() throws Exception {
float textWidth = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
float originalGap = textWidth / 3.0F;
// We have to calculate the lineWidth based on the original value
float originalValueStartValue = mGhostStartField.getFloat(marqueeObject);
float lineWidth = originalValueStartValue + textWidth - originalGap;
float mGhostStart = lineWidth - textWidth + NEW_GAP;
mGhostStartField.setFloat(marqueeObject, mGhostStart);
mMaxScrollField.setFloat(marqueeObject, mGhostStart + textWidth);
mGhostOffsetField.setFloat(marqueeObject, lineWidth + NEW_GAP);
mFadeStopField.setFloat(marqueeObject, lineWidth);
mMaxFadeScrollField.setFloat(marqueeObject, mGhostStart + lineWidth + lineWidth);
}
}
You can now easily adjust the extra blank space using the NEW_GAP
variable. In this example, it is set to 0 pixels. Set that constant to any pixel value that you wish to use.
NOTE: this solution is extending from AppCompatTextView
. If you are extending from TextView
you should uncomment the relevant code in the initMarqueeObject()
method.
Then just just use this new class in place of the old TextView in XML...
<com.yourpackagename.CustomTextView
android:focusable="true"
android:focusableInTouchMode="true"
android:singleLine="true"
android:scrollHorizontally="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
There is no need to call setSelected(true)
in your code as this is done internally within the new class.
This may not work forever as it will break if they rename certain variables or change the logic of TextView
in future releases. But it has worked great on all versions that I have tested it on.
Hopefully this helps!