summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Garvit Narang <garvitnarang@google.com> 2024-09-23 21:57:26 +0000
committer Garvit Narang <garvitnarang@google.com> 2024-10-17 05:47:13 +0000
commitd154de1a39a6cf6215410bf4977b107f48e989b2 (patch)
treee4956772b925c5299f61b237423a1d3d3abbf4f7
parentb2b845cffdf50b36e0d0086aa7f7c81d883f3d52 (diff)
Modify scrollbar to include gap between thumb and track
This is an isolated change which only modifies the manner in which round scrollbars are drawn which is determined by screen shape. Only wearable devices will be affected by this The scrollbar uses drawArc method to draw track/thumb. The method accepts a start angle and a sweep angle to draw the arc with a ROUND stroke style. Based on the stroke width, above the start angle the arc is rounded so the top most angular point of the arc is actually not the start angle but formed by a small semi circular shape above it. Start angle of the top track is start angle of thumb reduced by 1. Stroke width of thumb 2. Gap between top track and thumb 3. Stroke width of top track 4. Sweep angle of top track Rest of the calculations are based on similar geometric considerations exploiting the properties of a circle. The implementation also assumed a circular display Bug: 343739581 Test: manual flash and check Flag: android.view.flags.use_refactored_round_scrollbar Change-Id: I42c73fdc0b94e3735cc257caac3447bedcd27c68
-rw-r--r--core/java/android/view/RoundScrollbarRenderer.java249
-rw-r--r--core/java/android/view/flags/view_flags.aconfig8
-rw-r--r--core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java146
3 files changed, 345 insertions, 58 deletions
diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java
index 5f6d5e29570e..59c2598f00f0 100644
--- a/core/java/android/view/RoundScrollbarRenderer.java
+++ b/core/java/android/view/RoundScrollbarRenderer.java
@@ -16,17 +16,26 @@
package android.view;
+import static android.util.MathUtils.acos;
+
+import static java.lang.Math.sin;
+
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.os.SystemProperties;
import android.util.DisplayMetrics;
+import android.view.flags.Flags;
/**
* Helper class for drawing round scroll bars on round Wear devices.
+ *
+ * @hide
*/
-class RoundScrollbarRenderer {
+public class RoundScrollbarRenderer {
+ private static final String BLUECHIP_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled";
// The range of the scrollbar position represented as an angle in degrees.
private static final float SCROLLBAR_ANGLE_RANGE = 28.8f;
private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90%
@@ -45,12 +54,15 @@ class RoundScrollbarRenderer {
private final Paint mTrackPaint = new Paint();
private final RectF mRect = new RectF();
private final View mParent;
- private final int mMaskThickness;
+ private final float mInset;
private float mPreviousMaxScroll = 0;
private float mMaxScrollDiff = 0;
private float mPreviousCurrentScroll = 0;
private float mCurrentScrollDiff = 0;
+ private float mThumbStrokeWidthAsDegrees = 0;
+ private boolean mDrawToLeft;
+ private boolean mUseRefactoredRoundScrollbar;
public RoundScrollbarRenderer(View parent) {
// Paints for the round scrollbar.
@@ -69,29 +81,36 @@ class RoundScrollbarRenderer {
// Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same
// way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so
// that it doesn't get clipped.
- mMaskThickness = parent.getContext().getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.circular_display_mask_thickness);
- }
+ int maskThickness =
+ parent.getContext()
+ .getResources()
+ .getDimensionPixelSize(
+ com.android.internal.R.dimen.circular_display_mask_thickness);
- public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) {
- if (alpha == 0) {
- return;
- }
- // Get information about the current scroll state of the parent view.
- float maxScroll = mParent.computeVerticalScrollRange();
- float scrollExtent = mParent.computeVerticalScrollExtent();
- float newScroll = mParent.computeVerticalScrollOffset();
+ float thumbWidth = dpToPx(THUMB_WIDTH_DP);
+ mThumbPaint.setStrokeWidth(thumbWidth);
+ mTrackPaint.setStrokeWidth(thumbWidth);
+ mInset = thumbWidth / 2 + maskThickness;
+
+ mUseRefactoredRoundScrollbar =
+ Flags.useRefactoredRoundScrollbar()
+ && SystemProperties.getBoolean(BLUECHIP_ENABLED_SYSPROP, false);
+ }
+ private float computeScrollExtent(float scrollExtent, float maxScroll) {
if (scrollExtent <= 0) {
if (!mParent.canScrollVertically(1) && !mParent.canScrollVertically(-1)) {
- return;
+ return -1f;
} else {
- scrollExtent = 0;
+ return 0f;
}
} else if (maxScroll <= scrollExtent) {
- return;
+ return -1f;
}
+ return scrollExtent;
+ }
+ private void resizeGradually(float maxScroll, float newScroll) {
// Make changes to the VerticalScrollRange happen gradually
if (Math.abs(maxScroll - mPreviousMaxScroll) > RESIZING_THRESHOLD_PX
&& mPreviousMaxScroll != 0) {
@@ -106,51 +125,81 @@ class RoundScrollbarRenderer {
|| Math.abs(mCurrentScrollDiff) > RESIZING_THRESHOLD_PX) {
mMaxScrollDiff *= RESIZING_RATE;
mCurrentScrollDiff *= RESIZING_RATE;
-
- maxScroll -= mMaxScrollDiff;
- newScroll -= mCurrentScrollDiff;
} else {
mMaxScrollDiff = 0;
mCurrentScrollDiff = 0;
}
+ }
- float currentScroll = Math.max(0, newScroll);
- float linearThumbLength = scrollExtent;
- float thumbWidth = dpToPx(THUMB_WIDTH_DP);
- mThumbPaint.setStrokeWidth(thumbWidth);
- mTrackPaint.setStrokeWidth(thumbWidth);
+ public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) {
+ if (alpha == 0) {
+ return;
+ }
+ // Get information about the current scroll state of the parent view.
+ float maxScroll = mParent.computeVerticalScrollRange();
+ float scrollExtent = mParent.computeVerticalScrollExtent();
+ float newScroll = mParent.computeVerticalScrollOffset();
- setThumbColor(applyAlpha(DEFAULT_THUMB_COLOR, alpha));
- setTrackColor(applyAlpha(DEFAULT_TRACK_COLOR, alpha));
+ scrollExtent = computeScrollExtent(scrollExtent, maxScroll);
+ if (scrollExtent < 0f) {
+ return;
+ }
- // Normalize the sweep angle for the scroll bar.
- float sweepAngle = (linearThumbLength / maxScroll) * SCROLLBAR_ANGLE_RANGE;
- sweepAngle = clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE);
- // Normalize the start angle so that it falls on the track.
- float startAngle = (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle))
- / (maxScroll - linearThumbLength) - SCROLLBAR_ANGLE_RANGE / 2f;
- startAngle = clamp(startAngle, -SCROLLBAR_ANGLE_RANGE / 2f,
- SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle);
+ // Make changes to the VerticalScrollRange happen gradually
+ resizeGradually(maxScroll, newScroll);
+ maxScroll -= mMaxScrollDiff;
+ newScroll -= mCurrentScrollDiff;
- // Draw the track and the thumb.
- float inset = thumbWidth / 2 + mMaskThickness;
- mRect.set(
- bounds.left + inset,
- bounds.top + inset,
- bounds.right - inset,
- bounds.bottom - inset);
-
- if (drawToLeft) {
- canvas.drawArc(mRect, 180 + SCROLLBAR_ANGLE_RANGE / 2f, -SCROLLBAR_ANGLE_RANGE, false,
- mTrackPaint);
- canvas.drawArc(mRect, 180 - startAngle, -sweepAngle, false, mThumbPaint);
+ applyThumbColor(alpha);
+
+ float sweepAngle = computeSweepAngle(scrollExtent, maxScroll);
+ float startAngle =
+ computeStartAngle(Math.max(0, newScroll), sweepAngle, maxScroll, scrollExtent);
+
+ updateBounds(bounds);
+
+ mDrawToLeft = drawToLeft;
+ drawRoundScrollbars(canvas, startAngle, sweepAngle, alpha);
+ }
+
+ private void drawRoundScrollbars(
+ Canvas canvas, float startAngle, float sweepAngle, float alpha) {
+ if (mUseRefactoredRoundScrollbar) {
+ draw(canvas, startAngle, sweepAngle, alpha);
} else {
- canvas.drawArc(mRect, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, false,
- mTrackPaint);
- canvas.drawArc(mRect, startAngle, sweepAngle, false, mThumbPaint);
+ applyTrackColor(alpha);
+ drawArc(canvas, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, mTrackPaint);
+ drawArc(canvas, startAngle, sweepAngle, mThumbPaint);
}
}
+ /** Returns true if horizontal bounds are updated */
+ private void updateBounds(Rect bounds) {
+ mRect.set(
+ bounds.left + mInset,
+ bounds.top + mInset,
+ bounds.right - mInset,
+ bounds.bottom - mInset);
+ mThumbStrokeWidthAsDegrees =
+ getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f);
+ }
+
+ private float computeSweepAngle(float scrollExtent, float maxScroll) {
+ // Normalize the sweep angle for the scroll bar.
+ float sweepAngle = (scrollExtent / maxScroll) * SCROLLBAR_ANGLE_RANGE;
+ return clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE);
+ }
+
+ private float computeStartAngle(
+ float currentScroll, float sweepAngle, float maxScroll, float scrollExtent) {
+ // Normalize the start angle so that it falls on the track.
+ float startAngle =
+ (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle)) / (maxScroll - scrollExtent)
+ - SCROLLBAR_ANGLE_RANGE / 2f;
+ return clamp(
+ startAngle, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle);
+ }
+
void getRoundVerticalScrollBarBounds(Rect bounds) {
float padding = dpToPx(OUTER_PADDING_DP);
final int width = mParent.mRight - mParent.mLeft;
@@ -164,10 +213,8 @@ class RoundScrollbarRenderer {
private static float clamp(float val, float min, float max) {
if (val < min) {
return min;
- } else if (val > max) {
- return max;
} else {
- return val;
+ return Math.min(val, max);
}
}
@@ -176,15 +223,17 @@ class RoundScrollbarRenderer {
return Color.argb(alphaByte, Color.red(color), Color.green(color), Color.blue(color));
}
- private void setThumbColor(int thumbColor) {
- if (mThumbPaint.getColor() != thumbColor) {
- mThumbPaint.setColor(thumbColor);
+ private void applyThumbColor(float alpha) {
+ int color = applyAlpha(DEFAULT_THUMB_COLOR, alpha);
+ if (mThumbPaint.getColor() != color) {
+ mThumbPaint.setColor(color);
}
}
- private void setTrackColor(int trackColor) {
- if (mTrackPaint.getColor() != trackColor) {
- mTrackPaint.setColor(trackColor);
+ private void applyTrackColor(float alpha) {
+ int color = applyAlpha(DEFAULT_TRACK_COLOR, alpha);
+ if (mTrackPaint.getColor() != color) {
+ mTrackPaint.setColor(color);
}
}
@@ -192,4 +241,88 @@ class RoundScrollbarRenderer {
return dp * ((float) mParent.getContext().getResources().getDisplayMetrics().densityDpi)
/ DisplayMetrics.DENSITY_DEFAULT;
}
+
+ private static float getVertexAngle(float edge, float base) {
+ float edgeSquare = edge * edge * 2;
+ float baseSquare = base * base;
+ float gapInRadians = acos(((edgeSquare - baseSquare) / edgeSquare));
+ return (float) Math.toDegrees(gapInRadians);
+ }
+
+ private static float getKiteEdge(float knownEdge, float angleBetweenKnownEdgesInDegrees) {
+ return (float) (2 * knownEdge * sin(Math.toRadians(angleBetweenKnownEdgesInDegrees / 2)));
+ }
+
+ private void draw(Canvas canvas, float thumbStartAngle, float thumbSweepAngle, float alpha) {
+ // Draws the top arc
+ drawTrack(
+ canvas,
+ // The highest point of the top track on a vertical scale. Here the thumb width is
+ // reduced to account for the arc formed by ROUND stroke style
+ -SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees,
+ // The lowest point of the top track on a vertical scale. Here the thumb width is
+ // reduced twice to (a) account for the arc formed by ROUND stroke style (b) gap
+ // between thumb and top track
+ thumbStartAngle - mThumbStrokeWidthAsDegrees * 2,
+ alpha);
+ // Draws the thumb
+ drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint);
+ // Draws the bottom arc
+ drawTrack(
+ canvas,
+ // The highest point of the bottom track on a vertical scale. Here the thumb width
+ // is added twice to (a) account for the arc formed by ROUND stroke style (b) gap
+ // between thumb and bottom track
+ (thumbStartAngle + thumbSweepAngle) + mThumbStrokeWidthAsDegrees * 2,
+ // The lowest point of the top track on a vertical scale. Here the thumb width is
+ // added to account for the arc formed by ROUND stroke style
+ SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees,
+ alpha);
+ }
+
+ private void drawTrack(Canvas canvas, float beginAngle, float endAngle, float alpha) {
+ // Angular distance between end and begin
+ float angleBetweenEndAndBegin = endAngle - beginAngle;
+ // The sweep angle for the track is the angular distance between end and begin less the
+ // thumb width twice to account for top and bottom arc formed by the ROUND stroke style
+ float sweepAngle = angleBetweenEndAndBegin - 2 * mThumbStrokeWidthAsDegrees;
+
+ float startAngle = -1f;
+ float strokeWidth = -1f;
+ if (sweepAngle > 0f) {
+ // The angle is greater than 0 which means a normal arc should be drawn with stroke
+ // width same as the thumb. The ROUND stroke style will cover the top/bottom arc of the
+ // track
+ startAngle = beginAngle + mThumbStrokeWidthAsDegrees;
+ strokeWidth = mThumbPaint.getStrokeWidth();
+ } else if (Math.abs(sweepAngle) < 2 * mThumbStrokeWidthAsDegrees) {
+ // The sweep angle is less than 0 but is still relevant in creating a circle for the
+ // top/bottom track. The start angle is adjusted to account for being the mid point of
+ // begin / end angle.
+ startAngle = beginAngle + angleBetweenEndAndBegin / 2;
+ // The radius of this circle forms a kite with the radius of the arc drawn for the rect
+ // with the given angular difference between the arc radius which is used to compute the
+ // new stroke width.
+ strokeWidth = getKiteEdge(((mRect.right - mRect.left) / 2), angleBetweenEndAndBegin);
+ // The opacity is decreased proportionally, if the stroke width of the track is 50% or
+ // less that that of the thumb
+ alpha = alpha * Math.min(1f, 2 * strokeWidth / mThumbPaint.getStrokeWidth());
+ // As we desire a circle to be drawn, the sweep angle is set to a minimal value
+ sweepAngle = Float.MIN_NORMAL;
+ } else {
+ return;
+ }
+
+ applyTrackColor(alpha);
+ mTrackPaint.setStrokeWidth(strokeWidth);
+ drawArc(canvas, startAngle, sweepAngle, mTrackPaint);
+ }
+
+ private void drawArc(Canvas canvas, float startAngle, float sweepAngle, Paint paint) {
+ if (mDrawToLeft) {
+ canvas.drawArc(mRect, /* startAngle= */ 180 - startAngle, -sweepAngle, false, paint);
+ } else {
+ canvas.drawArc(mRect, startAngle, sweepAngle, /* useCenter= */ false, paint);
+ }
+ }
}
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 1cf26ab64c09..bb61ae49259c 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -116,4 +116,12 @@ flag {
description: "Add a SurfaceView composition order control API."
bug: "341021569"
is_fixed_read_only: true
+}
+
+flag {
+ name: "use_refactored_round_scrollbar"
+ namespace: "wear_frameworks"
+ description: "Use refactored round scrollbar."
+ bug: "333417898"
+ is_fixed_read_only: true
} \ No newline at end of file
diff --git a/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java b/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java
new file mode 100644
index 000000000000..262bd5cd6c01
--- /dev/null
+++ b/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.flags.Flags;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link RoundScrollbarRenderer}.
+ *
+ * <p>Build/Install/Run: atest FrameworksCoreTests:android.view.RoundScrollbarRendererTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class RoundScrollbarRendererTest {
+
+ private static final int DEFAULT_VERTICAL_SCROLL_RANGE = 100;
+ private static final int DEFAULT_VERTICAL_SCROLL_EXTENT = 20;
+ private static final int DEFAULT_VERTICAL_SCROLL_OFFSET = 40;
+ private static final float DEFAULT_ALPHA = 0.5f;
+ private static final Rect BOUNDS = new Rect(0, 0, 200, 200);
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Mock private Canvas mCanvas;
+ @Captor private ArgumentCaptor<Paint> mPaintCaptor;
+ private RoundScrollbarRenderer mScrollbar;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ MockView view = spy(new MockView(ApplicationProvider.getApplicationContext()));
+ when(view.canScrollVertically(anyInt())).thenReturn(true);
+ when(view.computeVerticalScrollRange()).thenReturn(DEFAULT_VERTICAL_SCROLL_RANGE);
+ when(view.computeVerticalScrollExtent()).thenReturn(DEFAULT_VERTICAL_SCROLL_EXTENT);
+ when(view.computeVerticalScrollOffset()).thenReturn(DEFAULT_VERTICAL_SCROLL_OFFSET);
+ mPaintCaptor = ArgumentCaptor.forClass(Paint.class);
+
+ mScrollbar = new RoundScrollbarRenderer(view);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_USE_REFACTORED_ROUND_SCROLLBAR)
+ public void testScrollbarDrawn_legacy() {
+ mScrollbar.drawRoundScrollbars(mCanvas, DEFAULT_ALPHA, BOUNDS, /* drawToLeft= */ false);
+
+ // The arc will be drawn twice, i.e. once for track and once for thumb
+ verify(mCanvas, times(2))
+ .drawArc(any(), anyFloat(), anyFloat(), eq(false), mPaintCaptor.capture());
+
+ Paint thumbPaint = mPaintCaptor.getAllValues().getFirst();
+ assertEquals(Paint.Cap.ROUND, thumbPaint.getStrokeCap());
+ assertEquals(Paint.Style.STROKE, thumbPaint.getStyle());
+ Paint trackPaint = mPaintCaptor.getAllValues().get(1);
+ assertEquals(Paint.Cap.ROUND, trackPaint.getStrokeCap());
+ assertEquals(Paint.Style.STROKE, trackPaint.getStyle());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_USE_REFACTORED_ROUND_SCROLLBAR)
+ public void testScrollbarDrawn() {
+ mScrollbar.drawRoundScrollbars(mCanvas, DEFAULT_ALPHA, BOUNDS, /* drawToLeft= */ false);
+
+ // The arc will be drawn thrice, i.e. twice for track and once for thumb
+ verify(mCanvas, times(3))
+ .drawArc(any(), anyFloat(), anyFloat(), eq(false), mPaintCaptor.capture());
+
+ // Verify paint styles
+ Paint thumbPaint = mPaintCaptor.getAllValues().getFirst();
+ assertEquals(Paint.Cap.ROUND, thumbPaint.getStrokeCap());
+ assertEquals(Paint.Style.STROKE, thumbPaint.getStyle());
+ Paint trackPaint = mPaintCaptor.getAllValues().get(1);
+ assertEquals(Paint.Cap.ROUND, trackPaint.getStrokeCap());
+ assertEquals(Paint.Style.STROKE, trackPaint.getStyle());
+ }
+
+ public static class MockView extends View {
+
+ public MockView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int computeVerticalScrollRange() {
+ return super.getHeight();
+ }
+
+ @Override
+ public int computeVerticalScrollOffset() {
+ return super.computeVerticalScrollOffset();
+ }
+
+ @Override
+ public int computeVerticalScrollExtent() {
+ return super.computeVerticalScrollExtent();
+ }
+ }
+}