現在の4.x系ではどうかちょっと調べてないのですが、 前に練習向けに作ってたアプリで、TextViewのマーキー動作が以下の仕様のため、思ったように動かせませんでした。
- フォーカスが当たっていないと動作しない
- テキストの長さがViewの幅以上でないと動かない
このため、マーキー動作を行うTextViewを自作したので、公開しておきます。 ・・・本当は1,2年位前に作ったのですが、書くのを怠けてました(´ω`)
で、最初はTextViewのソースから、そっくりそのままマーキー部分を改造したものを作ろうと したのですが、TextViewの内部のパッケージを利用している部分がどうにもならなかったので 断念しました。。。
そのため、TextViewを継承して、マーキー動作を行う部分をTextViewから移植することで なんとか思った動作が実現できました。 できたソースは以下になります。
import java.lang.ref.WeakReference;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.TextView;
public class MarqueeTextView extends TextView {
private Marquee mMarquee;
private int repeatLimit = 0;
private boolean repeatInfy = false;
private boolean repeatReverse = false;
private float marqueeSpeed = 0;
private boolean isConfigureChange = false;
public MarqueeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
ViewTreeObserver observer = this.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 画面方向が変更された際にマーキーをやり直す
if(isConfigureChange){
if(mMarquee!=null) {
stopMarquee();
startMarqueeAndRetry();
}
isConfigureChange = false;
}
}
});
}
@Override
protected void onDraw(Canvas canvas) {
// Draw the background for this view
// 描画時に座標をずらすことでマーキー動作を行っている
if (mMarquee != null && mMarquee.isRunning()) {
this.scrollTo((int) mMarquee.mScroll, 0);
}
super.onDraw(canvas);
}
@Override
protected float getLeftFadingEdgeStrength() {
if (mMarquee != null && !mMarquee.isStopped()) {
final Marquee marquee = mMarquee;
return marquee.mScroll / getHorizontalFadingEdgeLength();
}
return super.getLeftFadingEdgeStrength();
}
@Override
protected float getRightFadingEdgeStrength() {
if (mMarquee != null && !mMarquee.isStopped()) {
final Marquee marquee = mMarquee;
return (marquee.mMaxScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
}
return super.getRightFadingEdgeStrength();
}
@Override
public void onConfigurationChanged(Configuration newConfig){
// 画面方向の変更を検知
super.onConfigurationChanged(newConfig);
isConfigureChange = true;
}
/**
* setTextがfinalのため、Textセット用のメソッドを配置
*/
public void setMarqueeText(CharSequence text){
super.setText(text);
mMarquee.refreshMarqueeParams();
}
/**
* テキストがView幅以上か識別する
*/
public boolean isOverTextWidth(){
if(this.getLayout()!=null){
return (this.getLayout().getLineWidth(0) > this.getWidth());
}
else{
return false;
}
}
/**
* マーキー動作をリピート回数を設定する
*/
public void setRepeatLimit(int limit){
if(limit < 0){
limit = 0;
}
repeatLimit = limit;
}
/**
* マーキー動作を無制限でリピートする
*/
public void setRepeatInfy(boolean infy){
repeatInfy = infy;
if(mMarquee!=null){
mMarquee.setRepeatInfy(repeatInfy);
}
}
/**
* マーキー動作を右端まできたら折り返すような動作にする
*/
public void setRepeatReverse(boolean setReverse){
repeatReverse = setReverse;
if(mMarquee!=null){
mMarquee.setRepeatReverse(setReverse);
}
}
/**
* マーキー動作の速度を設定する
*/
void setMarqueeSpeed(float marqueeSpeed){
this.marqueeSpeed = marqueeSpeed;
if(mMarquee!=null){
mMarquee.setMarqueeSpeed(marqueeSpeed);
}
}
/**
* マーキー動作を開始する
*/
public void startMarquee() {
if ((mMarquee == null || mMarquee.isStopped())) {
if (mMarquee == null) mMarquee = new Marquee(this);
mMarquee.setMarqueeSpeed(marqueeSpeed);
mMarquee.setRepeatReverse(repeatReverse);
mMarquee.setRepeatInfy(repeatInfy);
if(repeatInfy){
mMarquee.start(1);
}
else{
mMarquee.start(repeatLimit);
}
}
}
/**
* テキスト幅がView以上の場合にマーキー動作を開始する
*/
public void startMarqueeIfTextWidthLarger(){
if(isOverTextWidth()){
startMarquee();
}
}
/**
* マーキー動作を開始する
*/
private void startMarqueeAndRetry(){
if ((mMarquee == null || mMarquee.isStopped())) {
if (mMarquee == null) mMarquee = new Marquee(this);
mMarquee.setMarqueeSpeed(marqueeSpeed);
mMarquee.setRepeatReverse(repeatReverse);
mMarquee.setRepeatInfy(repeatInfy);
if(repeatInfy){
mMarquee.startAndRetry(1);
}
else{
mMarquee.startAndRetry(repeatLimit);
}
}
}
/**
* マーキー動作を停止する
*/
public void stopMarquee() {
if (mMarquee != null && !mMarquee.isStopped()) {
mMarquee.stop();
}
}
/**
* マーキー動作用の内部クラス
* オリジナルのTextViewクラスから抜粋し、一部を改造しました。
*/
final class Marquee extends Handler {
// TODO: Add an option to configure this
// オリジナルのソースから抜粋したのでいらない定数があるかも。。。
// マーキーの最後の方で、テキストの頭がでてくる動作ができない。
// コピーしたViewを用意して最後辺りで頭を流すとか?
// private static final float MARQUEE_DELTA_MAX = 0.07f;
private static final int MARQUEE_DELAY = 1200;
private static final int MARQUEE_RESTART_DELAY = 1200;
private static final int MARQUEE_RETRY_DELAY = 1200;
private static final int MARQUEE_RESOLUTION = 1000 / 30;
private static final int MARQUEE_PIXELS_PER_SECOND = 30;
private static final byte MARQUEE_STOPPED = 0x0;
private static final byte MARQUEE_STARTING = 0x1;
private static final byte MARQUEE_RUNNING = 0x2;
private static final byte MARQUEE_RETRYING = 0x3;
private static final int MESSAGE_START = 0x1;
private static final int MESSAGE_TICK = 0x2;
private static final int MESSAGE_RESTART = 0x3;
private static final int MESSAGE_RETRY = 0x4;
private final WeakReference mView;
private byte mStatus = MARQUEE_STOPPED;
private float mScrollSpeed;
private float mMaxScroll;
// float mMaxFadeScroll;
// private float mGhostStart;
// private float mGhostOffset;
// private float mFadeStop;
private int mRepeatLimit;
private boolean mRepeatReverse;
private boolean mRepeatInfy;
private boolean reverse;
float mScroll;
Marquee(TextView v) {
final float density = v.getContext().getResources().getDisplayMetrics().density;
mScrollSpeed = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
mView = new WeakReference(v);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_START:
mStatus = MARQUEE_RUNNING;
tick();
break;
case MESSAGE_TICK:
tick();
break;
case MESSAGE_RETRY:
retry();
break;
case MESSAGE_RESTART:
if (mStatus == MARQUEE_RUNNING) {
TextView text = mView.get();
if(text!=null){
text.scrollTo(0, 0);
}
if(mRepeatInfy){
start(1);
}
else{
if (mRepeatLimit >= 0) {
mRepeatLimit--;
}
start(mRepeatLimit);
}
}
break;
}
}
void tick() {
if (mStatus != MARQUEE_RUNNING) {
return;
}
removeMessages(MESSAGE_TICK);
TextView textView = mView.get();
if (textView != null) {
if(reverse){
mScroll -= mScrollSpeed;
if (mScroll < 0) {
mScroll = 0;
reverse = !reverse;
sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
} else {
sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
}
}
else{
mScroll += mScrollSpeed;
if (mScroll > mMaxScroll) {
mScroll = mMaxScroll;
if(mRepeatReverse){
reverse = !reverse;
sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESTART_DELAY);
}
else{
sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
}
} else {
sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
}
}
textView.invalidate();
}
else {
stop();
}
}
void stop() {
mStatus = MARQUEE_STOPPED;
removeMessages(MESSAGE_START);
removeMessages(MESSAGE_RESTART);
removeMessages(MESSAGE_RETRY);
removeMessages(MESSAGE_TICK);
resetScroll();
}
private void resetScroll() {
mScroll = 0.0f;
final TextView textView = mView.get();
if (textView != null) {
textView.scrollTo(0, 0);
textView.invalidate();
}
}
private void retry(){
removeMessages(MESSAGE_RETRY);
final TextView textView = mView.get();
if(textView.getLayout()==null){
mStatus = MARQUEE_RETRYING;
sendEmptyMessageDelayed(MESSAGE_RETRY, MARQUEE_RETRY_DELAY);
}
else if(textView.getLayout().getLineWidth(0) > textView.getWidth()){
start(mRepeatLimit);
}
}
private void startAndRetry(int repeatLimit){
mRepeatLimit = repeatLimit;
retry();
}
void start(int repeatLimit) {
if (repeatLimit == 0) {
stop();
return;
}
mRepeatLimit = repeatLimit;
final TextView textView = mView.get();
if (textView != null) {
mStatus = MARQUEE_STARTING;
mScroll = 0.0f;
// テキストの幅
final int textWidth = (int)textView.getLayout().getLineWidth(0);
// TextViewの幅
final float lineWidth = textView.getWidth();
// スクロールする右端の座標
// 以下のソースでは、
// lineWidth/3 になっているのでView幅の 1/3 までテキストの右端がスクロールしたところでMarqueeが終了する
final float gap = lineWidth / 3.0f;
// mGhostStart = lineWidth - textWidth + gap;
// mMaxScroll = mGhostStart + textWidth;
mMaxScroll = textWidth - gap;
// mGhostOffset = lineWidth + gap;
// mFadeStop = lineWidth + textWidth / 6.0f;
// mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
textView.invalidate();
sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
}
}
void refreshMarqueeParams(){
TextView textView = mView.get();
mStatus = MARQUEE_STARTING;
mScroll = 0.0f;
int textWidth = (int)textView.getLayout().getLineWidth(0);
float lineWidth = textView.getWidth();
float gap = lineWidth / 3.0f;
// mGhostStart = lineWidth - textWidth + gap;
// mMaxScroll = mGhostStart + textWidth;
mMaxScroll = textWidth - gap;
// mGhostOffset = lineWidth + gap;
// mFadeStop = lineWidth + textWidth / 6.0f;
// mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
}
// この辺のGhostXXXは役割がわからなかった。
// float getGhostOffset() {
// return mGhostOffset;
// }
//
// boolean shouldDrawLeftFade() {
// return mScroll <= mFadeStop;
// }
//
// boolean shouldDrawGhost() {
// return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
// }
boolean isRunning() {
return mStatus == MARQUEE_RUNNING;
}
boolean isStopped() {
return mStatus == MARQUEE_STOPPED;
}
boolean isRetryed(){
return mStatus == MARQUEE_RETRYING;
}
void setRepeatReverse(boolean setReverse){
mRepeatReverse = setReverse;
}
void setRepeatInfy(boolean infy){
mRepeatInfy = infy;
}
void setMarqueeSpeed(float marqueeSpeed){
if(marqueeSpeed <= 0){
final float density = mView.get().getContext().getResources().getDisplayMetrics().density;
mScrollSpeed = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
return;
}
if(marqueeSpeed < 0.1){
mScrollSpeed = 0.1f;
}
else {
mScrollSpeed = marqueeSpeed;
}
}
}
}
で、使用する場合はこんな感じ
MarqueeTextView mtext = (MarqueeTextView) view.findViewById(R.id.text);
mtext.setRepeatInfy(true);
mtext.startMarqueeIfTextWidthLarger();
上記はマーキー回数無制限でテキストがView幅より大きい場合マーキー動作が行われます。 注意しないといけないのが、onPauseされたときに止めないと裏で動き続けます。。。
あと、TextViewを継承しているので、
singleLine="true"、ellipsize="marquee"で使った方がいいです。
というか上記の設定以外で使ったことがない。。。
2013/3/30追記
そういえば上の使ってマーキー動かした状態でフォーカス与えて
オリジナルのマーキー動作した場合ってどうなるんだ?
・・・そうだ、見なかったことにしよう。