Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best Way: Save & Restore TextView Position in ScrollView

What i want is, upon the device changes orientation, the top line on the screen when in Portrait remains the top line on screen in Landscape. And vice versa.

As the width of the screen is likely to be different between Portrait and Landscape, the line width of the text, which means also the width of the TextView and the ScrollView, will vary. Thus, the line-wrap will be different in different screen configurations (Portrait vs. Landscape, large vs. small). The line-break will be at different position in different cases.

There are three not-so-perfect solutions for your reference. Also explained the shortcomings of them.


Firstly, The very most basic approach:

(1) By just storing the y-offset in pixel

Please take a look at: http://eliasbland.wordpress.com/2011/07/28/how-to-save-the-position-of-a-scrollview-when-the-orientation-changes-in-android/

Why this is not-so-perfect:

In Portrait, lines are wrapped.

Line_1_Word_A Line_1_Word_B Line_1_Word_C
Line_1_Word_D
Line_2_Word_A Line_2_Word_B Line_2_Word_C
Line_2_Word_D
Line_3_Word_A Line_3_Word_B Line_3_Word_C
Line_3_Word_D

In Landscape, lines are not wrapped.

Line_1_Word_A Line_1_Word_B Line_1_Word_C Line_1_Word_D
Line_2_Word_A Line_2_Word_B Line_2_Word_C Line_2_Word_D
Line_3_Word_A Line_3_Word_B Line_3_Word_C Line_3_Word_D

Imagine reading Line_2_Word_A (at screen top) in Portrait while saving. When changed to Landscape, it will be showing Line_3_Word_A (at screen top). (Because of two-lines-offset-in-pixel from top.)


Then i come up with an approach,

(2) By saving the scroll-percentage

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    final ScrollView scrollView = (ScrollView) findViewById(R.id.Trial_C_ScrollViewContainer);
    outState.putFloatArray(ScrollViewContainerScrollPercentage,
            new float[]{
            (float) scrollView.getScrollX()/scrollView.getChildAt(0).getWidth(),
            (float) scrollView.getScrollY()/scrollView.getChildAt(0).getHeight() });
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    final float[] scrollPercentage = savedInstanceState.getFloatArray(ScrollViewContainerScrollPercentage);
    final ScrollView scrollView = (ScrollView) findViewById(R.id.Trial_C_ScrollViewContainer);
    scrollView.post(new Runnable() {
        public void run() {
            scrollView.scrollTo(
                    Math.round(scrollPercentage[0]*scrollView.getChildAt(0).getWidth()),
                    Math.round(scrollPercentage[1]*scrollView.getChildAt(0).getHeight()));
        }
    });
}

This works perfectly if and only if the length of every line is the same.

Why this is not-so-perfect:

In Portrait, lines are wrapped.

Line_1_Word_A Line_1_Word_B
Line_1_Word_C Line_1_Word_D
Line_1_Word_E Line_1_Word_F
Line_2_Word_A
Line_3_Word_A
Line_4_Word_A

In Landscape, lines are not wrapped.

Line_1_Word_A Line_1_Word_B Line_1_Word_C Line_1_Word_D Line_1_Word_E Line_1_Word_F
Line_2_Word_A
Line_3_Word_A
Line_4_Word_A

Imagine reading Line_2_Word_A (at screen top) in Portrait while saving. When changed to Landscape, it will be showing Line_3_Word_A (at screen top). (Because it is scrolled by 50%.)


Then i found this approach which indeed

(3) Storing the first visible line

Please take a look at (first answer): How to restore textview scrolling position after screen rotation?

Why this is not-so-perfect:

In Portrait, lines are wrapped.

Line_1_Word_A Line_1_Word_B
Line_1_Word_C Line_1_Word_D
Line_1_Word_E Line_1_Word_F
Line_2_Word_A
Line_3_Word_A
Line_4_Word_A

In Landscape, lines are not wrapped.

Line_1_Word_A Line_1_Word_B Line_1_Word_C Line_1_Word_D Line_1_Word_E Line_1_Word_F
Line_2_Word_A
Line_3_Word_A
Line_4_Word_A

Imagine reading Line_1_Word_E (at screen top) in Portrait while saving. When changed to Landscape, it will be showing Line_3_Word_A (at screen top). (Because it is the third line.)

A perfect one would be, in Landscape, showing Line_1_Word_A (as well as Line_1_Word_E) at screen top.


Could you please suggest a perfect approach?


Edit:

After a few thoughts, is method (3) identical to method (1) in fact? :-/


Edit 2:

Well, i have just come up with another not-so-perfect-yet-more-perfect approach than the above three:

(4) Paragraph-based storing

Separating paragraphs (or blocks of texts) into different TextView objects.

Then by the codes like method (3), or in any other ways, it is not hard to detect which paragraph (or block), i.e. which TextView object, is currently at the top of the screen.

Then restore and scroll down to that paragraph (or block). Bingo!

As i said, it is not-so-perfect. But at least the users can get back to that paragraph (or block) that he/she was reading. He/She just have to peek down a bit to find that particular line. (Or it might be even better to remind readers with a few previous lines, i.e. reading from the start of the paragraph.) i know it might be terribly bad if we have a long long long paragraph :-/

Well, we can actually "improve" this method. Make it down to word-level, a word a TextView. So it is logically "perfect". But, i guess, it is not a wise choice.

P.S. bathroom is always the best place for brainstorming (-:


i am still looking for your perfect answer!!

like image 608
midnite Avatar asked Mar 28 '13 09:03

midnite


1 Answers

I am so proud to say, I got a perfect solution to this now.

Sh.... (sorry I am too excited about it. If you find any mistakes/bugs/weakness on it, please DO give me your valuable suggestions and please feel free to correct me. :-)

Cut the crap. Here you go !!!

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    final ScrollView scrollView = (ScrollView) findViewById(R.id.Trial_C_ScrollViewContainer);
    final TextView textView = (TextView) scrollView.getChildAt(0);
    final int firstVisableLineOffset = textView.getLayout().getLineForVertical(scrollView.getScrollY());
    final int firstVisableCharacterOffset = textView.getLayout().getLineStart(firstVisableLineOffset);
    outState.putInt(ScrollViewContainerTextViewFirstVisibleCharacterOffset, firstVisableCharacterOffset);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    final int firstVisableCharacterOffset = savedInstanceState.getInt(ScrollViewContainerTextViewFirstVisibleCharacterOffset);

    final ScrollView scrollView = (ScrollView) findViewById(R.id.Trial_C_ScrollViewContainer);
    scrollView.post(new Runnable() {
        public void run() {
            final TextView textView = (TextView) scrollView.getChildAt(0);
            final int firstVisableLineOffset = textView.getLayout().getLineForOffset(firstVisableCharacterOffset);
            final int pixelOffset = textView.getLayout().getLineTop(firstVisableLineOffset);
            scrollView.scrollTo(0, pixelOffset);
        }
    });
}

That's it. :-)

If it helps you, please clap your hands. <-- this is important!!

And if you wish to, click that little upright triangle. (make sure you have clapped your hands first!)

like image 124
midnite Avatar answered Oct 19 '22 23:10

midnite