Android Scrolling/Fading Text Programatically Appended To

43 views Asked by At

I'm building an old-school type android game, and one of the things I'd like to have is a "combat results text window" where I can add text as actions happen, so the player knows what's going on.

I currently have used a textview and a valueAnimator to fade the text out:

public void updateCommentary(String updatedText){

    runOnUiThread(new Runnable() {

      @Override
      public void run() {
          ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0f);
          valueAnimator.setDuration(5000);
          valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
              @Override
              public void onAnimationUpdate(ValueAnimator animation) {
                  TextView tv = (TextView) findViewById(R.id.txtRunningCommentary);
                  tv.setText(updatedText);
                  float alpha = (float) animation.getAnimatedValue();
                  tv.setAlpha(alpha);
              }
          });
          valueAnimator.start();
      }
    });
}

This works to the point where any updates will drop in and fade out, but if called a quickly in succession, the following text over-write what's there already.

What I'd like to do, is instead have say, 5 lines of text, and let me keep adding new updates, which push the previous updates up, and as they get further up they fade out. If there are no updates for long enough, have the text fade out on it's own. Finally, in a perfect world, the widget would not have a fixed height, but understand how many lines can fit in it, and have the desired functionality based on that user's device/screen layout.

Currently Working with Single Textview

I have thought of doing this with a number of textviews, and at the start of the function moving the contents of textview2 into textview1, then moving trextview3 into textview2 and so on, and finally adding the newest update into textview5 - but this feels limited on a few points:

  • Firstly, it seems horribly clunky and inefficient in operation.
  • Secondly, I'm hoping that individual lines might fade on their own - so a lengthy update might fade out the top line, while keeping remaining text there.
  • Lastly, it would certainly not function in an auto-magical with it shrinking or expanding rows to the user device - but rather always be fixed to the number of textviews I have set up.

I have also considered adding a scrollable vertical linearLayout and appending new textviews to it... and somehow fading them out and deleting them... but again, starting to feel like it's getting overly complicated for the expected functionality?

Is there a android class that I could use in some way to achieve this, or is my horrid way about as good as it's going to get?

2

There are 2 answers

1
Tam Huynh On BEST ANSWER

If you only need to fade out the text, no additional transformation, or scaling at all, I have a small trick for you:

Create a gradient background drawable that smoothly fades from white to transparent:

bgr_shadow_white_down.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:angle="-90"
        android:endColor="@android:color/transparent"
        android:startColor="@color/white" />
</shape>

You can create another drawable that fading bottom up:

bgr_shadow_white_up.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <gradient
        android:angle="90"
        android:endColor="@android:color/transparent"
        android:startColor="@color/white" />
</shape>

Then you can have a layout like this:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="100dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginHorizontal="10dp"
        tools:text="@tools:sample/lorem/random" />

    <View
        android:layout_width="match_parent"
        android:layout_height="32dp"
        android:background="@drawable/bgr_shadow_white_down" />

    <View
        android:layout_width="match_parent"
        android:layout_height="32dp"
        android:layout_gravity="bottom"
        android:background="@drawable/bgr_shadow_white_up" />
</FrameLayout>

Visual:

Visualize fade overlay

You will have a TextView with a fade effect on top and bottom. Now in your use case, you can:

  • Have a single TextView like this, when a new line is appended, translate the TextView up by a lineHeight so the old line moves up, which will be covered by our fade gradient, and will be completely covered by white color or clipped out from the parent when it keeps moving up. Clear everything and reset the animation when timeout and no new line is appended.

  • Have a vertical LinearLayout and add multiple TextView to the stack, translate up, same as above. Clear everything on timeout.

0
Fluffeh On

What I ended up doing in my own code is closer to the exact requirement that I had, namely, for text to be added, and for it to fade out on it's own after a certain period of time, and for new text to be appended to the bottom of the journal on the screen.

In my layout, I've added a scrollview and a linearlayout inside it:

    <ScrollView
        android:id="@+id/svCommentaryHousing"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toTopOf="@+id/worldSurface"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent">

        <LinearLayout
            android:id="@+id/llRunningCommentary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:orientation="vertical"
            android:scrollbars="vertical"
            android:text="txtRunningCommentary"

            >

        </LinearLayout>

    </ScrollView>

Then I've modified my original function to display text in the following manner:

@Override
public void updateCommentary(String updatedText){

    runOnUiThread(new Runnable() {

      @Override
      public void run() {

          LinearLayout llRunningCommentary = (LinearLayout) findViewById(R.id.llRunningCommentary);
          TextView newComment = new TextView(getApplicationContext());
          newComment.setText(updatedText);
          newComment.setTag(10);
          llRunningCommentary.addView(newComment);

          ScrollView svCommentaryHousing = (ScrollView) findViewById(R.id.svCommentaryHousing);
          svCommentaryHousing.fullScroll(svCommentaryHousing.FOCUS_DOWN);
      }
    });
}

This creates a new text update with a lifetime of 10 cycles using the tag inside the view itself. I've also made it scroll down to the very end each time, so if updates are coming in thick and fast, the newest is always shown.

in my code, I already have some functionality that ticks over, so I connected that process to poll the following function as it does other things already, lining up the output lifecycles nicely:

@Override
public void updateJournal(){

    runOnUiThread(new Runnable() {
          @Override
          public void run() {
              LinearLayout llRunningCommentary = (LinearLayout) findViewById(R.id.llRunningCommentary);
              int viewAge=0;
              for(int index = 0; index < llRunningCommentary.getChildCount(); index++) {
                  View nextChild = llRunningCommentary.getChildAt(index);
                  viewAge=(Integer) nextChild.getTag();
                  viewAge--;
                  switch(viewAge){
                      case 4:
                          nextChild.setAlpha(0.7f);
                          break;
                      case 3:
                          nextChild.setAlpha(0.4f);
                          break;
                      case 2:
                          nextChild.setAlpha(0.2f);
                          break;
                      case 1:
                          nextChild.setAlpha(0.2f);
                          break;
                      case 0:
                          nextChild.setAlpha(0.0f);
                          break;
                  }
                  if(viewAge<=0){
                      llRunningCommentary.removeViewAt(index);
                  }
                  else{
                      nextChild.setTag(viewAge);
                  }
              }
          }
    });
}

Each cycle where this is called, the code will iterate through each child view, update the cycles it has left by one, then when they start approaching end of life, it changes the opacity of the individual textview to fade it out, then when it has become completely invisible, it removes the view entirely.

The benefits of this approach mean that I am able to potentially load a comment with a longer life into the journal if I want to, though I am not sure that is behaviour I will end up using, but the option is there to have "important" messages persist for a longer period.

The output is shown below, both when freshly added, and after spamming a few comments that are fading out.

Newly Added Comments

Comments Fading off the screen