15

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!!

Community
  • 1
  • 1
midnite
  • 5,157
  • 7
  • 38
  • 52
  • Take the pixel density of the smallest android device and then multiply it by the device's width in Portrait. Then calculate how many words are about to be fit there and wrap the line accordingly (you might want to save that and use it in `onRestoreInstanceState()`). You want to achieve the same line wrapping in Portrait and Landscape - right? – g00dy Mar 28 '13 at 09:55
  • Thanks @g00dy for your reply. But in most cases, the line wrapping (horizontal width) of Portrait and Landscape are _not_ the same. If it is the same, we have no headache :-) – midnite Mar 28 '13 at 10:07
  • Yes I understand that :) However I'm still not sure if you want the same line wrapping for both Portrait and Landscape - please confirm that first. – g00dy Mar 28 '13 at 10:26
  • Does same line wrapping mean line-break at the same point of every line? For this, no. – midnite Mar 28 '13 at 10:29
  • Ok, to clear the things out - give your expected results. The examples you gave are all like: Portrait - ok ; Landscape -NOK and I can't clearly see what is the expected result looking like. – g00dy Mar 28 '13 at 12:20
  • Sorry if i didnt make it clear enough. 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. – midnite Mar 28 '13 at 12:42
  • Is it fine if the other lines don't stay the same? It seems like resizing the width of the scrollview would have an effect on everything else as well. – DeeV Mar 28 '13 at 13:19
  • I'm thinking one solution would be to override the `ScrollView` class and in `onMeasure()`, measure the view width to the dimensions of the device's portrait size. That way, no matter what orientation, the line wrapping will be exactly the same. Of course, this leaves a lot of open space on the sides, but you seem to be ok with that. – DeeV Mar 28 '13 at 13:24
  • @DeeV, Thanks for reply! What does "the other lines don't stay the same" mean? Hmm.... i want to let the width of the `ScrollView` to be flexible to fit the screen. As the width is variable, the line-break of the lines are not likely to be at the same position. – midnite Mar 28 '13 at 14:01
  • Ok. I think I understand what you want. You don't care that the lines are wrapping differently. The wrong paragraphs are being shifted to the top. You want the last one there. I thought you wanted to keep the same look. – DeeV Mar 28 '13 at 14:13
  • Yes! Thanks for your time and effort @DeeV!! What i want is, the user is viewing a particular line on Portrait screen. After the user switched to Landscape, that line he/she was reading is still on screen. (And i assume the first visible line is what the user is reading) – midnite Mar 28 '13 at 14:21
  • I found this because you linked to my answer in (3). My solution does not store the first visible line; it stores the first visible character. I believe it will do exactly what you were looking for. Did you try it? – Eric Simonton May 22 '13 at 00:29
  • @EricSimonton Sorry for this late reply. i dry run your code again and i believe your solution does the same as mine. When i first read your solution while posting this question, i was focused on your padding things. In fact, scrolling to the specific character offset, do we still need to take care the padding things? – midnite May 28 '13 at 22:10
  • 1
    @midnite: My solution considers whether you have a partial line showing at the top, and restores it so the same fraction of a line is showing again after the rotation. If the first line is fully visible, it instead (re)stores the fraction of the top padding that is visible. – Eric Simonton Jun 05 '13 at 17:53

1 Answers1

16

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!)

peterh
  • 11,875
  • 18
  • 85
  • 108
midnite
  • 5,157
  • 7
  • 38
  • 52
  • 2
    I love your enthusiasm! why didn't you just use getScrollY? http://developer.android.com/reference/android/view/View.html#getScrollY() – Chiatar Jul 08 '13 at 08:17
  • Thanks! Hmm... AFAIS most texts (except webpages) are only scrolling vertically. It's not hard to modify those codes to take care of horizontal scrolling also :) – midnite Jul 09 '13 at 00:55
  • 5
    `ScrollView` (and any view for that matter) must have an id in order for it to be persisted by Android. You shouldn't have to do this. Adding an id to your scrollview will force android to save/restore its state including its scroll position. Just see this: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.2_r1/android/widget/ScrollView.java#ScrollView.SavedState – dnkoutso May 07 '14 at 02:14
  • 1
    This works with `NestedScrollView` as well. @dnkoutso: While defining an `@+id` forces Android to persist scroll position, it does without taking on account the word wrapping. @midnite solution works almost perfectly (there are some pixels off due to the text rearrangement, but nothing serious. – miguelt Jul 06 '16 at 17:59