├── demo
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ ├── drawable-v24
│ │ │ ├── marker.png
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ └── tiger.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ ├── activity_demos_tileview.xml
│ │ │ ├── activity_demos_scrollview_universal.xml
│ │ │ ├── activity_demos_scrollview_horizontal.xml
│ │ │ ├── activity_demos_scalingscrollview_textviews.xml
│ │ │ ├── activity_demos_scrollview_vertical.xml
│ │ │ ├── activity_demos_scalingscrollview_tiger.xml
│ │ │ └── main.xml
│ │ └── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ └── com
│ │ │ └── moagrius
│ │ │ └── demo
│ │ │ ├── ScrollViewDemoVertical.java
│ │ │ ├── ScalingScrollViewDemoTiger.java
│ │ │ ├── ScrollViewDemoUniversal.java
│ │ │ ├── ScrollViewDemoHorizontal.java
│ │ │ ├── Helpers.java
│ │ │ ├── ScalingScrollViewDemoTextViews.java
│ │ │ └── MainActivity.java
│ │ ├── AndroidManifest.xml
│ │ └── asset-files
│ │ └── min
│ │ ├── totri_paging_styles.css
│ │ ├── highlight_script.js
│ │ ├── dom_script.js
│ │ ├── annotator_init.js
│ │ ├── xpath_script.js
│ │ ├── annotator_script.js
│ │ ├── exploder_script.js
│ │ ├── chapter_styles.css
│ │ ├── chapter_nightmode.css
│ │ └── totri_scripts.js
├── proguard-rules.pro
└── build.gradle
├── scrollview
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ └── values
│ │ │ └── attrs.xml
│ │ └── java
│ │ └── com
│ │ └── moagrius
│ │ ├── view
│ │ ├── TouchUpGestureDetector.java
│ │ └── PointerDownGestureDetector.java
│ │ └── widget
│ │ ├── ScalingScrollView.java
│ │ └── ScrollView.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/scrollview/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':demo', ':scrollview'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ScrollView Demo
3 |
4 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-v24/marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/drawable-v24/marker.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moagrius/ScrollView/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/scrollview/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | /*/build/
3 | local.properties
4 | .gradle/
5 | .idea/
6 | .signing/
7 | .DS_Store
8 | .DS_Store?
9 | ._*
10 | .Trashes
11 | proguard/
12 | *.log
13 | .navigation/
14 | captures/
15 | *.iml
--------------------------------------------------------------------------------
/scrollview/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #222222
4 | #000000
5 | #FF9900
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Apr 26 18:19:38 CDT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_demos_tileview.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_demos_scrollview_universal.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_demos_scrollview_horizontal.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_demos_scalingscrollview_textviews.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/moagrius/demo/ScrollViewDemoVertical.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.demo;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.support.annotation.Nullable;
6 | import android.widget.LinearLayout;
7 |
8 | /**
9 | * @author Mike Dunn, 6/11/17.
10 | */
11 |
12 | public class ScrollViewDemoVertical extends Activity {
13 |
14 | @Override
15 | protected void onCreate(@Nullable Bundle savedInstanceState) {
16 | super.onCreate(savedInstanceState);
17 | setContentView(R.layout.activity_demos_scrollview_vertical);
18 | LinearLayout linearLayout = findViewById(R.id.linearlayout);
19 | Helpers.populateLinearLayout(linearLayout, 20);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/scrollview/src/main/java/com/moagrius/view/TouchUpGestureDetector.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.view;
2 |
3 | import android.view.MotionEvent;
4 |
5 | public class TouchUpGestureDetector {
6 |
7 | private OnTouchUpListener mOnTouchUpListener;
8 |
9 | public TouchUpGestureDetector(OnTouchUpListener listener) {
10 | mOnTouchUpListener = listener;
11 | }
12 |
13 | public boolean onTouchEvent(MotionEvent event) {
14 | if (event.getActionMasked() == MotionEvent.ACTION_UP) {
15 | if (mOnTouchUpListener != null) {
16 | return mOnTouchUpListener.onTouchUp(event);
17 | }
18 | }
19 | return true;
20 | }
21 |
22 | public interface OnTouchUpListener {
23 | boolean onTouchUp(MotionEvent event);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_demos_scrollview_vertical.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/moagrius/demo/ScalingScrollViewDemoTiger.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.demo;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.support.annotation.Nullable;
6 |
7 | import com.moagrius.widget.ScalingScrollView;
8 |
9 |
10 | /**
11 | * @author Mike Dunn, 2/3/18.
12 | */
13 |
14 | public class ScalingScrollViewDemoTiger extends Activity {
15 |
16 | @Override
17 | protected void onCreate(@Nullable Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | setContentView(R.layout.activity_demos_scalingscrollview_tiger);
20 | ScalingScrollView scalingScrollView = findViewById(R.id.scalingscrollview);
21 | scalingScrollView.setScaleLimits(0, 10);
22 | scalingScrollView.setShouldVisuallyScaleContents(true);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_demos_scalingscrollview_tiger.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/moagrius/demo/ScrollViewDemoUniversal.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.demo;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.support.annotation.Nullable;
6 | import android.widget.LinearLayout;
7 |
8 | /**
9 | * @author Mike Dunn, 6/11/17.
10 | */
11 |
12 | public class ScrollViewDemoUniversal extends Activity {
13 |
14 | @Override
15 | protected void onCreate(@Nullable Bundle savedInstanceState) {
16 | super.onCreate(savedInstanceState);
17 | setContentView(R.layout.activity_demos_scrollview_universal);
18 | LinearLayout linearLayout = findViewById(R.id.linearlayout);
19 | for (int i = 0; i < 20; i++) {
20 | LinearLayout row = new LinearLayout(this);
21 | Helpers.populateLinearLayout(row, 100);
22 | linearLayout.addView(row);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/scrollview/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/demo/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | defaultConfig {
6 | android.defaultConfig.vectorDrawables.useSupportLibrary = true
7 | applicationId "com.moagrius"
8 | minSdkVersion 19
9 | targetSdkVersion 28
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | compileOptions {
21 | sourceCompatibility = '1.8'
22 | targetCompatibility = '1.8'
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: 'libs', include: ['*.jar'])
28 | implementation 'com.android.support:appcompat-v7:28.0.0'
29 | implementation project(path: ':scrollview')
30 | }
31 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/moagrius/demo/ScrollViewDemoHorizontal.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.demo;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.support.annotation.Nullable;
6 | import android.widget.LinearLayout;
7 | import android.widget.LinearLayout.LayoutParams;
8 |
9 | /**
10 | * @author Mike Dunn, 6/11/17.
11 | */
12 |
13 | public class ScrollViewDemoHorizontal extends Activity {
14 |
15 | @Override
16 | protected void onCreate(@Nullable Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | setContentView(R.layout.activity_demos_scrollview_horizontal);
19 | LinearLayout linearLayout = findViewById(R.id.linearlayout);
20 | Helpers.populateLinearLayout(
21 | linearLayout, 100,
22 | new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT),
23 | "This TextView (number %1$d)\n" +
24 | "is longer than other so that\n" +
25 | "the horizontal ScrollView shows\n" +
26 | "a more pager-like display");
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/demo/src/main/asset-files/min/totri_paging_styles.css:
--------------------------------------------------------------------------------
1 | body.oreilly-paging{padding:0 !important;margin:0 !important;position:relative}.oreilly-paging #sbo-rt-content{overflow-x:visible !important;position:relative !important}.oreilly-paging #sbo-rt-content a{overflow-wrap:break-word !important}.sbo-rt-unselectable{-webkit-user-select:none !important}.oreilly-paging #sbo-rt-content div,.oreilly-paging #sbo-rt-content table,.oreilly-paging #sbo-rt-content pre{max-height:none !important}body.oreilly-expander{padding:0 !important}.oreilly-exploder-button{display:block;height:30px;width:30px;margin-left:-30px;position:absolute;opacity:.95;background-color:#f9f9f9;z-index:1000;box-shadow:-1px 1px 1px 0 rgba(0,0,0,.25)}.oreilly-exploder-button::after{top:7px;left:7px;position:absolute;height:16px;width:16px;content:"";background-image:url(file:///android_asset/images/totri_exploder_explode.png);background-repeat:no-repeat;background-size:100%}.night .oreilly-exploder-button{background-color:#444}.night .oreilly-exploder-button::after{background-image:url(file:///android_asset/images/totri_exploder_explode_night.png)}
--------------------------------------------------------------------------------
/demo/src/main/java/com/moagrius/demo/Helpers.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.demo;
2 |
3 | import android.content.Context;
4 | import android.widget.LinearLayout;
5 | import android.widget.LinearLayout.LayoutParams;
6 | import android.widget.TextView;
7 |
8 | import java.util.Locale;
9 |
10 | /**
11 | * @author Mike Dunn, 6/11/17.
12 | */
13 |
14 | public class Helpers {
15 |
16 | public static void populateLinearLayout(LinearLayout linearLayout, int quantity) {
17 | populateLinearLayout(linearLayout, quantity, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT), "TextView #%1$d");
18 | }
19 |
20 | public static void populateLinearLayout(LinearLayout linearLayout, int quantity, LayoutParams lp, String text) {
21 | Context context = linearLayout.getContext();
22 | for (int i = 0; i < quantity; i++) {
23 | TextView textView = new TextView(context);
24 | textView.setText(String.format(Locale.US, text, i));
25 | textView.setTextSize(30);
26 | textView.setPadding(100, 100, 100, 100);
27 | textView.setBackgroundColor(0xFF383838);
28 | textView.setTextColor(0xFFD8D8D8);
29 | linearLayout.addView(textView, lp);
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/moagrius/demo/ScalingScrollViewDemoTextViews.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.demo;
2 |
3 | import android.app.ActionBar;
4 | import android.app.Activity;
5 | import android.os.Bundle;
6 | import android.support.annotation.Nullable;
7 | import android.widget.LinearLayout;
8 |
9 | import com.moagrius.widget.ScalingScrollView;
10 |
11 | /**
12 | * @author Mike Dunn, 2/3/18.
13 | */
14 |
15 | public class ScalingScrollViewDemoTextViews extends Activity {
16 |
17 | @Override
18 | protected void onCreate(@Nullable Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 | setContentView(R.layout.activity_demos_scalingscrollview_textviews);
21 | ScalingScrollView scalingScrollView = findViewById(R.id.scalingscrollview);
22 | scalingScrollView.setShouldVisuallyScaleContents(true);
23 | scalingScrollView.setMaximumScale(5);
24 | LinearLayout linearLayout = findViewById(R.id.linearlayout);
25 | for (int i = 0; i < 25; i++) {
26 | LinearLayout row = new LinearLayout(this);
27 | Helpers.populateLinearLayout(row, 25);
28 | linearLayout.addView(row, new LinearLayout.LayoutParams(ActionBar.LayoutParams.WRAP_CONTENT, ActionBar.LayoutParams.WRAP_CONTENT));
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/demo/src/main/java/com/moagrius/demo/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.demo;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 |
7 | public class MainActivity extends Activity {
8 |
9 | @Override
10 | protected void onCreate(Bundle savedInstanceState) {
11 | super.onCreate(savedInstanceState);
12 | setContentView(R.layout.main);
13 | findViewById(R.id.textview_demos_scrollview_vertical).setOnClickListener(view -> startDemo(ScrollViewDemoVertical.class));
14 | findViewById(R.id.textview_demos_scrollview_horizontal).setOnClickListener(view -> startDemo(ScrollViewDemoHorizontal.class));
15 | findViewById(R.id.textview_demos_scrollview_universal).setOnClickListener(view -> startDemo(ScrollViewDemoUniversal.class));
16 | findViewById(R.id.textview_demos_scalingscrollview_textviews).setOnClickListener(view -> startDemo(ScalingScrollViewDemoTextViews.class));
17 | findViewById(R.id.textview_demos_scalingscrollview_tiger).setOnClickListener(view -> startDemo(ScalingScrollViewDemoTiger.class));
18 | }
19 |
20 | private void startDemo(Class extends Activity> activityClass) {
21 | Intent intent = new Intent(this, activityClass);
22 | startActivity(intent);
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/scrollview/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.novoda.bintray-release'
2 | apply plugin: 'com.android.library'
3 |
4 | def version = "1.0.11"
5 |
6 | buildscript {
7 | repositories {
8 | jcenter()
9 | }
10 | dependencies {
11 | classpath 'com.novoda:bintray-release:0.9'
12 | }
13 | }
14 |
15 | android {
16 | compileSdkVersion 28
17 |
18 | defaultConfig {
19 | minSdkVersion 19
20 | targetSdkVersion 28
21 | versionCode 4
22 | versionName version
23 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
24 | }
25 |
26 | buildTypes {
27 | release {
28 | minifyEnabled false
29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility = '1.8'
34 | targetCompatibility = '1.8'
35 | }
36 |
37 | }
38 |
39 | dependencies {
40 | implementation fileTree(dir: 'libs', include: ['*.jar'])
41 | implementation 'com.android.support:appcompat-v7:28.0.0'
42 | }
43 |
44 | publish {
45 | userOrg = 'moagrius'
46 | groupId = 'com.moagrius'
47 | artifactId = 'scrollview'
48 | publishVersion = version
49 | desc = 'Universal (2D) ScrollView, and a version with zoom (scaling)'
50 | website = 'https://github.com/moagrius/ScrollView'
51 | }
52 |
--------------------------------------------------------------------------------
/demo/src/main/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
16 |
17 |
21 |
22 |
26 |
27 |
31 |
32 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/demo/src/main/asset-files/min/highlight_script.js:
--------------------------------------------------------------------------------
1 | var Highlighter=function(a){this.options=a||{};this.xpath=new XPath(this.options.rootNode,this.options.blacklist)};Highlighter.IS_ONLY_WHITESPACE_REGEX=/^\s*$/;Highlighter.PROHIBITED_PARENT_TAGS="ul ol option select table tbody thead tr textarea".split(" ");
2 | Highlighter.prototype={options:null,xpath:null,shouldHighlightNode:function(a){return null!=a&&a.nodeType==Node.TEXT_NODE&&null!=a.parentNode&&-1==Highlighter.PROHIBITED_PARENT_TAGS.indexOf(a.parentNode.nodeName)&&!Highlighter.IS_ONLY_WHITESPACE_REGEX.test(a.wholeText+a.nodeValue)},highlightNode:function(a,b){if(!this.shouldHighlightNode(a))return null;var c=document.createElement(this.options.tagName);c.appendChild(document.createTextNode(a.nodeValue));c.classList.add(this.options.className);b&&
3 | c.classList.add(this.options.className+"-"+b);a.parentNode.replaceChild(c,a);return c},clearHighlight:function(a){for(;0b)return{node:c,offset:e-(d-b)}}c=Dom.next(c)}return null},getTextNodesBetween:function(a,b){for(var d=[],c=a;null!=c;){c.nodeType==Node.TEXT_NODE&&0
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/demo/src/main/asset-files/min/annotator_init.js:
--------------------------------------------------------------------------------
1 | (function(){var f={},c=new Annotator({tagName:"span",className:"annotator-highlight",noteClassName:"annotator-note",leadClassName:"annotator-note-first",rootNode:document.getElementById("sbo-rt-content"),blacklist:Oreilly.configuration.blacklist}),h=function(){};c.addValidationHook(function(a){if(a.quote.length>Annotator.MAXIMUM_HIGHLIGHT_LENGTH||a.exception==Annotator.HIGHLIGHT_MAXIMUM_LENGTH_EXCEEDED_MESSAGE)throw new h;});c.addValidationHook(function(a){if(null==a.quote)throw Error();});c.addValidationHook(function(a){if(null!=
2 | a.text&&a.text.length>Annotator.MAXIMUM_HIGHLIGHT_LENGTH)throw Error();});c.setValidationHandler(function(a){a instanceof h&&(window.AnnotationsInterface.onAnnotationTooLargeToSave(),window.getSelection().removeAllRanges())});c.start();var g={pattern:/[xy]/g,placeholder:"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",replacer:function(a){var b=16*Math.random()|0;return("x"==a?b:b&3|8).toString(16)},get:function(){return g.placeholder.replace(g.pattern,g.replacer)}},e={draw:function(a){var b=a.ranges[0];try{c.highlightRange(b,
3 | !!a.text).forEach(function(b){b.annotation=a;b.classList.add("highlight-for-annotation-"+a.identifier)})}catch(d){console.log(d),console.log(d.stack),console.log(JSON.stringify(a))}},clear:function(a){for(a=c.options.rootNode.getElementsByClassName("highlight-for-annotation-"+a.identifier);0=--e)return a;return null},flattenChildren:function(a,b){b=b||[];for(var e=0;eAnnotator.MAXIMUM_HIGHLIGHT_LENGTH)this.selection.exception=Annotator.HIGHLIGHT_MAXIMUM_LENGTH_EXCEEDED_MESSAGE;else{this.selection.exception=null;var b=a.getRangeAt(0);this.selection.anchorNode=b.startContainer;this.selection.anchorOffset=
6 | b.startOffset;this.selection.anchorNode.nodeType==Node.ELEMENT_NODE&&(this.selection.anchorNode=this.selection.anchorNode.childNodes[this.selection.anchorOffset],this.selection.anchorOffset=0);this.selection.focusNode=b.endContainer;this.selection.focusOffset=b.endOffset;this.selection.focusNode.nodeType==Node.ELEMENT_NODE&&0b)var e=Math.ceil(-b/f),b=b+e*f,g=g-e*this.windowSizeProvider.computedWidth;var e=a.offsetHeight,d=0,n=a.offsetHeight>=this.exploderButtonHeight*Oreilly.Exploder.COMPARATIVE_HEIGHT_THRESHOLD;if(a.offsetWidth>=l*Oreilly.Exploder.RELATIVE_WIDTH_THRESHOLD&&n&&(this.addExploderButton(b,g,h,d),this.isPaging))for(;f=this.exploderButtonHeight&&
7 | this.addExploderButton(b+c,g,h,d);++h
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/demo/src/main/asset-files/min/chapter_nightmode.css:
--------------------------------------------------------------------------------
1 | .night,.night #sbo-rt-content,.night #sbo-rt-content h1,.night #sbo-rt-content h2,.night #sbo-rt-content h3,.night #sbo-rt-content h4,.night #sbo-rt-content h5,.night #sbo-rt-content h6,.night #sbo-rt-content .heading-1,body.night #sbo-rt-content .h1,.night #sbo-rt-content .paragraph-head,.night #sbo-rt-content .cn-chapter-number,.night #sbo-rt-content .heading-3,.night #sbo-rt-content .title_document,.night #sbo-rt-content .fm_title_document,.night #sbo-rt-content .box_title,.night #sbo-rt-content .chaptertitle,.night #sbo-rt-content hr,.night #sbo-rt-content .figure-title,.night #sbo-rt-content .title{background-color:#000 !important;color:#ddd !important;text-shadow:none !important}body.night #sbo-rt-content p,body.night #sbo-rt-content li,body.night #sbo-rt-content dt,body.night #sbo-rt-content strong,body.night #sbo-rt-content a,body.night #sbo-rt-content code,body.night #sbo-rt-content em,body.night #sbo-rt-content sup{color:#ddd !important;box-shadow:none !important}body.night #sbo-rt-content p.pre{color:#000}.night #sbo-rt-content div,.night #sbo-rt-content span{color:inherit !important}.night #sbo-rt-content img{background-color:#ddd}body.night #sbo-rt-content div,body.night #sbo-rt-content table,body.night #sbo-rt-content table th,body.night #sbo-rt-content table thead,body.night #sbo-rt-content table tr{background-color:#000 !important;border:none !important}body.night #sbo-rt-content pre,body.night #sbo-rt-content .pre,body.night #sbo-rt-content p.pre,body.night #sbo-rt-content .pre1,body.night #sbo-rt-content .pre-ex,body.night #sbo-rt-content .pre_w,body.night #sbo-rt-content #lesson-fragment pre,body.night #sbo-rt-content #lesson-fragment .pre,body.night #sbo-rt-content #lesson-fragment p.pre,body.night #sbo-rt-content #lesson-fragment .pre1,body.night #sbo-rt-content #lesson-fragment .pre-ex,body.night #sbo-rt-content #lesson-fragment .pre_w{background-color:#222 !important;color:#aaa !important;border:none !important}body.night #sbo-rt-content div.sidebar p,body.night #sbo-rt-content div.sidebar1 p,body.night #sbo-rt-content div.note p,body.night #sbo-rt-content p.note,body.night #sbo-rt-content div.tip p,body.night #sbo-rt-content div.warning p,body.night #sbo-rt-content div.bgbox p,body.night #sbo-rt-content p.pre-ex,body.night #sbo-rt-content div.boxg p{color:#ddd}body.night #sbo-rt-content div.sidebar p a,body.night #sbo-rt-content div.sidebar1 p a,body.night #sbo-rt-content div.note p a,body.night #sbo-rt-content p.note a,body.night #sbo-rt-content div.note a,body.night #sbo-rt-content div.tip p a,body.night #sbo-rt-content div.warning p a,body.night #sbo-rt-content div.bgbox p a,body.night #sbo-rt-content p.pre-ex a,body.night #sbo-rt-content div.boxg p a{color:#ddd;text-decoration:none}body.night #sbo-rt-content div.sidebar li,body.night #sbo-rt-content div.sidebar1 li,body.night #sbo-rt-content div.note li,body.night #sbo-rt-content div.tip li,body.night #sbo-rt-content div.warning li,body.night #sbo-rt-content div.bgbox li,body.night #sbo-rt-content div.boxg li{color:#ddd}body.night #sbo-rt-content div.sidebar dt,body.night #sbo-rt-content div.sidebar1 dt,body.night #sbo-rt-content div.note dt,body.night #sbo-rt-content div.tip dt,body.night #sbo-rt-content div.warning dt,body.night #sbo-rt-content div.bgbox dt,body.night #sbo-rt-content div.feature dt,body.night #sbo-rt-content div.boxg dt{color:#ddd}body.night #sbo-rt-content div.sidebar dd,body.night #sbo-rt-content div.sidebar1 dd,body.night #sbo-rt-content div.note dd,body.night #sbo-rt-content div.tip dd,body.night #sbo-rt-content div.warning dd,body.night #sbo-rt-content div.bgbox dd,body.night #sbo-rt-content div.feature dd,body.night #sbo-rt-content div.boxg dt{color:#ddd}body.night #sbo-rt-content div.sidebar-title,body.night #sbo-rt-content div.sidebar p.title,body.night #sbo-rt-content div.sidebar h5,body.night #sbo-rt-content div.note h3,body.night #sbo-rt-content div.note p.title,body.night #sbo-rt-content p.box_title,body.night #sbo-rt-content div.tip h3,body.night #sbo-rt-content div.warning h3{background-color:transparent;color:#ddd !important}body.night #sbo-rt-content div.tip,body.night #sbo-rt-content .tip,body.night #sbo-rt-content .note,body.night #sbo-rt-content div.note,body.night #sbo-rt-content .warning,body.night #sbo-rt-content .note1,body.night #sbo-rt-content .sidebar1,body.night #sbo-rt-content .boxbg,body.night #sbo-rt-content div.sidebar,body.night #sbo-rt-content div.warning,body.night #sbo-rt-content div.tip{box-shadow:none;color:#ddd !important}body.night #sbo-rt-content td,body.night #sbo-rt-content td p,body.night #sbo-rt-content td p a,body.night #sbo-rt-content td a,body.night #sbo-rt-content td li,body.night #sbo-rt-content th,body.night #sbo-rt-content th p{color:#ddd}body.night #sbo-rt-content span.annotator-hl{background-color:#fed000;background-color:rgba(254,208,0,0.85);color:#000 !important}body.night #sbo-rt-content a span.annotator-hl{text-decoration:underline !important}body.night ::selection,body.night #sbo-rt-content .annotator-wrapper ::selection,body.night .sbo-inpage-toc .annotator-wrapper ::selection{background-color:#55c5ed;background-color:rgba(85,197,237,0.4)}
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # ScrollView
4 |
5 |
6 |
7 | The Android framework provides `android.widget.ScrollView` and `android.widget.HorizontalScrollView`, each providing scrolling along one axix.
8 |
9 | `com.moagrius.widgets.ScrollView` can do either, or scroll in any direction.
10 |
11 | It alogrithmically attempts to determine this for you, although it'd be trivial to add an API to enforce a specific behavior. For example, if you have a content wrapped in a scrolling mechanism, whose layout features are MATCH_PARENT for width, and WRAP_CONTENT for height, you'll scroll vertically. The inverse would scroll horizontally. If you have content wrapped in a scrolling mechanism in a 250,000DP square, the widget would scroll in any direction.
12 |
13 | This can be useful for large images, or table- or grid-style layouts.
14 |
15 | ## ScalingScrollView
16 |
17 | This is a subclass of `com.moagrius.widgets.ScrollView` with additional functionality for pinch and double tap to scale. You can allow the `ScalingScrollView` to automatically scale your content by calling `setShouldVisuallyScaleContents(true)` or you can handle the actaul output of the scale yourself by passing `false` to the same method. For example, you might want a grid of icons that reduces the space between them but does not actually scale the icons - you'd use the latter invocation `setShouldVisuallyScaleContents(false)` for that.
18 |
19 | ## Installation
20 |
21 | `ScrollView` is available on jcenter. Use the gradle `implementation` function in your `build.gradle`
22 |
23 | ```
24 | implementation 'com.moagrius:scrollview:1.0.11'
25 | ```
26 |
27 | ## Usage
28 |
29 | The `com.moagrius.widgets.ScrollView` should be very familiar to users of `android.widget.ScrollView`. For your convenience, there's a demo module included in this repo. Just clone the repo, open the project in Android Studio, then hit `Run`. The demo will play on whatever attached devices or emulators are running. There are several examples of uses for both `ScrollView` and related classes.
30 |
31 | ## API
32 |
33 | The API will seem almost entirely identical; the soure is taken largely from existing sources, and modified where needed. Both `android.widget.ScrollView` and `android.widget.HorizontalScrollView` were used to come up with the base; additionally ideas were taken from my own previous work on http://github.com/moagrius/TileView, and:
34 |
35 | 1. https://android.googlesource.com/platform/frameworks/support/+/master/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
36 | 1. https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/GestureDetector.java
37 |
38 | Additionally, there's a `ScalingScrollView` class, that reacts to pinch and double-tap gestures to handle "zooming". It will scale it's content visually if you call `setShouldVisuallyScaleContents` and pass a parameter of `true`. Otherwise, it will update a scale variable, but it's up to you what to do with those chnages (except for scroll boundaries - those are updated in all cases). For example, you might want a grid of icons that reflows when scaled, but the icons themselves do not actually scale.
39 |
40 | Of note, `ScalingScrollView` is the base class for `com.moagrius.TileView` http://github.com/moagrius/TileView, my image tiling libraray.
41 |
42 | ## Changes
43 |
44 | There were a few pieces that did make the port a bit rockier than you might imagine. Diagonal, for example, is an entirely new concept and foreign to both the existing widgets, although scrolling along an irregular axis is present in other components, like Google Maps. Another exmaple would be "fading edges" - that graphic hint you've reached the end of a scroll. While you could argue it's less likely to be useful in a omniaxis `ScrollView`, the widget might be used a drop-in replacement for the existing single-axis widgets, as originally stated. In those cases, fading edges might certainly be appropriate, but the amount of code required to support that was signficant, and the amount of new code that would have been required to make sure that those edges didn't overlap, and played nicely with another, was even greater. So the decison was made and the feature was sacrified, for better or worse. Similarly, I've modified the final output for other reasons:
45 |
46 | I've modified from the source for a few reasons:
47 |
48 | 1. Anything that was required for functionality on both axes.
49 | 2. Inaccessibility (package-private, internal, etc).
50 | 3. There's very little left around child focus, as a 2D scroll view is likely to be a form container.
51 | 5. Certain accessibility functions have been removed (e.g., does "scroll forward" mean down, or right?)
52 | 6. Using a Scroller rather than an OverScroller; over-scroll seems less helpful for "panning" views than a list-type view.
53 |
54 | You may not agree with all the decisions made, but I think if you check out the demo (built into the repo and super easy to use), you'll find the `com.moagrius.widgets.ScrollView` is not only a worthy replacement that does the job required of multiple framework-provided widgets, but also that you'll find uses that exceed the mandate of those other widgets. For example, if you just want to pan and zoom an image or component, these classes can handle almost all of that functionality out of the box, with a familiar API and familiar behavior. Remember that we're usign the same time ranges, pixel slops, interpolation, thresholds and qualification branching that are used currently, so you'll find that your new `ScrollView` looks, acts, and is programmed almost exactly like the others in your app, or others.
55 |
56 | ## Feedback & Contributing
57 |
58 | As always, feedback is welcome, **as are pull requests**. We'd love to have you spot a bug and report it properly with tons of detail and STR, but we'd love it even more if that issue came along with a thoughtful PR that fixed the problem.
59 |
60 | If you like the widget, we're not asing for donation or credit (license is MIT), but do please pop a star on the repo so we know we're working for real people with real apps and real problems to solve.
61 |
62 | Thanks for reading, and we hope you love ScrollView.
63 |
--------------------------------------------------------------------------------
/demo/src/main/asset-files/min/totri_scripts.js:
--------------------------------------------------------------------------------
1 | window.requestAnimationFrame||(window.requestAnimationFrame=function(a){return window.setTimeout(a,16)},window.cancelAnimationFrame=function(a){window.clearTimeout(a)});
2 | var Oreilly={Mode:{SCROLLING:1,PAGING:-1},configuration:{frames:{},isLoaded:!1,wait:0,CONTENT_LOADED:1,PAGE_COUNT_DETERMINED:2,CONTENT_SIZE_DETERMINED:4,blacklist:[".annotator-highlight",".table-wrapper",".search-result"]},onActualLoad:function(){Oreilly.configuration.isLoaded=!0;AndroidBridge.onLoad()},attemptTentativeLoad:function(){console.log("wait: "+Oreilly.configuration.wait);if(!Oreilly.configuration.isLoaded&&0==Oreilly.configuration.wait)AndroidBridge.onTentativeLoad()},addRule:function(a,
3 | c){if(c){var b=document.getElementById(c);null!=b&&b.parentNode.removeChild(b)}b=document.createElement("style");b.id=c;b.type="text/css";b.innerHTML=a;document.head.appendChild(b);window.requestAnimationFrame(function(){if(Oreilly.configuration.mode==Oreilly.Mode.PAGING)Oreilly.calculatePageCount(!0);else AndroidBridge.onContentSizeDetermined(document.body.scrollHeight)})},getTop:function(a){return window.pageYOffset+a.getBoundingClientRect().top},getLeft:function(a){return window.pageXOffset+a.getBoundingClientRect().left},
4 | scrollToPosition:function(a){console.log("747: Oreilly.scrollToPosition, position: "+a);AndroidBridge.onElementPositionFound(a/(Oreilly.configuration.mode==Oreilly.Mode.PAGING?document.body.scrollWidth:document.body.scrollHeight))},scrollToElement:function(a){console.log("747: Oreilly.scrollToElement, "+a);a&&(console.log("747: element is not null, scroll to it"),a=Oreilly.configuration.mode==Oreilly.Mode.PAGING?Oreilly.getLeft(a):Oreilly.getTop(a)-Oreilly.configuration.computedHeight/3,console.log("747: scrollToElement, position: "+
5 | a),Oreilly.scrollToPosition(a))},scrollToId:function(a){a&&(a=document.getElementById(a),Oreilly.scrollToElement(a))},scrollToXpath:function(a,c){console.log("747: scrollToXpath, "+a);if(a){0==a.indexOf("/")&&(a=a.substring(1));var b=document.getElementById("sbo-rt-content");console.log("747: find element for "+a);b=(new XPath(b)).read(a);null!=b&&(console.log("747: element found "+b.tagName),Oreilly.scrollToElementAndOffset(b,c))}},scrollToElementAndOffset:function(a,c){console.log("747: scrollToElementAndOffset");
6 | if(null==a)console.log("747: no element found to scroll to");else{var b=Dom.getNodeAndOffset(a,c),d=document.createRange();d.setStart(b.node,b.offset);b=d.getBoundingClientRect();b=Oreilly.configuration.mode==Oreilly.Mode.PAGING?window.pageXOffset+b.left:window.pageYOffset+b.top-Oreilly.configuration.computedHeight/3;console.log("747: scrollToElementAndOffset, position\x3d"+b);Oreilly.scrollToPosition(b);d.collapse()}},highlightAndScrollToRange:function(a){console.log("747: highlightAndScrollToRange");
7 | try{console.log("747: highlighting "+JSON.stringify(a));var c={start:a.start,startOffset:a.start_offset,end:a.end,endOffset:a.end_offset},b=new Highlighter({rootNode:document.getElementById("sbo-rt-content"),tagName:"span",className:"search-result",blacklist:Oreilly.configuration.blacklist});b.clearAll();var d=b.highlightRange(c);console.log("747: highlighted.length\x3d"+d.length);var e=d[0];console.log("747: first element in range: "+e);e&&(console.log("747: scrolling to element: "+e),Oreilly.scrollToElement(e))}catch(f){AndroidBridge.onCaughtException(f.message)}},
8 | getContainer:function(a,c){return null==a||a==document.body?null:-1= mMaxScale ? mMinScale : destination;
280 | destination = getConstrainedDestinationScale(effectiveDestination);
281 | smoothScaleFromFocalPoint((int) event.getX(), (int) event.getY(), destination);
282 | }
283 |
284 | @Override
285 | public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
286 | float currentScale = mScale * mScaleGestureDetector.getScaleFactor();
287 | setScaleFromPosition(
288 | (int) scaleGestureDetector.getFocusX(),
289 | (int) scaleGestureDetector.getFocusY(),
290 | currentScale);
291 | return true;
292 | }
293 |
294 | @Override
295 | public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
296 | mIsScaling = true;
297 | return true;
298 | }
299 |
300 | @Override
301 | public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
302 | mIsScaling = false;
303 | }
304 |
305 | @Override
306 | public void onPointerCounterChange(int pointerCount) {
307 | mHasSingleFingerDown = (pointerCount == SCROLL_GESTURE_MAX_POINTER_COUNT);
308 | }
309 |
310 | @Override
311 | public void onDraw( Canvas canvas ) {
312 | super.onDraw( canvas );
313 | if (mShouldVisuallyScaleContents) {
314 | canvas.scale(mScale, mScale);
315 | }
316 | }
317 |
318 | public interface ScaleChangedListener {
319 | void onScaleChanged(ScalingScrollView scalingScrollView, float currentScale, float previousScale);
320 | }
321 |
322 | protected static class ScrollScaleState extends SavedState {
323 | public float scale = 1f;
324 |
325 | public ScrollScaleState(Parcelable superState) {
326 | super(superState);
327 | }
328 |
329 | public ScrollScaleState(Parcel source) {
330 | super(source);
331 | scale = source.readFloat();
332 | }
333 |
334 | @Override
335 | public void writeToParcel(Parcel dest, int flags) {
336 | super.writeToParcel(dest, flags);
337 | dest.writeFloat(scale);
338 | }
339 |
340 | @Override
341 | public String toString() {
342 | return "ScalingScrollView.ScrollScaleState{" + Integer.toHexString(System.identityHashCode(this)) + " scrollPositionY=" + scrollPositionY + ", scrollPositionX=" + scrollPositionX + ", scale=" + scale + "}";
343 | }
344 |
345 | public static final Creator CREATOR = new Creator() {
346 | public ScrollScaleState createFromParcel(Parcel in) {
347 | return new ScrollScaleState(in);
348 | }
349 |
350 | public ScrollScaleState[] newArray(int size) {
351 | return new ScrollScaleState[size];
352 | }
353 | };
354 | }
355 |
356 | /**
357 | * @author Mike Dunn, 2/2/18.
358 | */
359 |
360 | private static class ZoomScrollAnimator extends ValueAnimator implements
361 | ValueAnimator.AnimatorUpdateListener,
362 | ValueAnimator.AnimatorListener {
363 |
364 | private WeakReference mScalingScrollViewWeakReference;
365 | private ScaleAndScrollState mStartState = new ScaleAndScrollState();
366 | private ScaleAndScrollState mEndState = new ScaleAndScrollState();
367 | private boolean mHasPendingZoomUpdates;
368 | private boolean mHasPendingScrollUpdates;
369 |
370 | public ZoomScrollAnimator(ScalingScrollView scalingScrollView) {
371 | super();
372 | addUpdateListener(this);
373 | addListener(this);
374 | setFloatValues(0f, 1f);
375 | setInterpolator(new QuinticInterpolator());
376 | mScalingScrollViewWeakReference = new WeakReference<>(scalingScrollView);
377 | }
378 |
379 | private boolean setupScrollAnimation(int x, int y) {
380 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
381 | if (scalingScrollView != null) {
382 | mStartState.x = scalingScrollView.getScrollX();
383 | mStartState.y = scalingScrollView.getScrollY();
384 | mEndState.x = x;
385 | mEndState.y = y;
386 | return mStartState.x != mEndState.x || mStartState.y != mEndState.y;
387 | }
388 | return false;
389 | }
390 |
391 | private boolean setupZoomAnimation(float scale) {
392 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
393 | if (scalingScrollView != null) {
394 | mStartState.scale = scalingScrollView.getScale();
395 | mEndState.scale = scale;
396 | return mStartState.scale != mEndState.scale;
397 | }
398 | return false;
399 | }
400 |
401 | public void animate(int x, int y, float scale) {
402 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
403 | if (scalingScrollView != null) {
404 | mHasPendingZoomUpdates = setupZoomAnimation(scale);
405 | mHasPendingScrollUpdates = setupScrollAnimation(x, y);
406 | if (mHasPendingScrollUpdates || mHasPendingZoomUpdates) {
407 | start();
408 | }
409 | }
410 | }
411 |
412 | public void animateZoom(float scale) {
413 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
414 | if (scalingScrollView != null) {
415 | mHasPendingZoomUpdates = setupZoomAnimation(scale);
416 | if (mHasPendingZoomUpdates) {
417 | start();
418 | }
419 | }
420 | }
421 |
422 | public void animateScroll(int x, int y) {
423 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
424 | if (scalingScrollView != null) {
425 | mHasPendingScrollUpdates = setupScrollAnimation(x, y);
426 | if (mHasPendingScrollUpdates) {
427 | start();
428 | }
429 | }
430 | }
431 |
432 | @Override
433 | public void onAnimationUpdate(ValueAnimator animation) {
434 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
435 | if (scalingScrollView != null) {
436 | float progress = (float) animation.getAnimatedValue();
437 | if (mHasPendingZoomUpdates) {
438 | float scale = mStartState.scale + (mEndState.scale - mStartState.scale) * progress;
439 | scalingScrollView.setScale(scale);
440 | scalingScrollView.setIsScaling(true);
441 | }
442 | if (mHasPendingScrollUpdates) {
443 | int x = (int) (mStartState.x + (mEndState.x - mStartState.x) * progress);
444 | int y = (int) (mStartState.y + (mEndState.y - mStartState.y) * progress);
445 | scalingScrollView.scrollTo(x, y);
446 | }
447 | }
448 | }
449 |
450 | @Override
451 | public void onAnimationStart(Animator animator) {
452 |
453 | }
454 |
455 | @Override
456 | public void onAnimationEnd(Animator animator) {
457 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
458 | if (scalingScrollView != null) {
459 | scalingScrollView.setIsScaling(false);
460 | }
461 | }
462 |
463 | @Override
464 | public void onAnimationCancel(Animator animator) {
465 | ScalingScrollView scalingScrollView = mScalingScrollViewWeakReference.get();
466 | if (scalingScrollView != null) {
467 | scalingScrollView.setIsScaling(false);
468 | }
469 | }
470 |
471 | @Override
472 | public void onAnimationRepeat(Animator animator) {
473 |
474 | }
475 |
476 | private static class ScaleAndScrollState {
477 | public int x;
478 | public int y;
479 | public float scale;
480 | }
481 |
482 | // https://android.googlesource.com/platform/frameworks/support/+/master/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java#514
483 | private static class QuinticInterpolator implements Interpolator {
484 | @Override
485 | public float getInterpolation(float t) {
486 | t -= 1.0f;
487 | return t * t * t * t * t + 1.0f;
488 | }
489 | }
490 | }
491 |
492 |
493 |
494 | }
495 |
--------------------------------------------------------------------------------
/scrollview/src/main/java/com/moagrius/widget/ScrollView.java:
--------------------------------------------------------------------------------
1 | package com.moagrius.widget;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Rect;
6 | import android.os.Parcel;
7 | import android.os.Parcelable;
8 | import android.util.AttributeSet;
9 | import android.view.FocusFinder;
10 | import android.view.InputDevice;
11 | import android.view.KeyEvent;
12 | import android.view.MotionEvent;
13 | import android.view.VelocityTracker;
14 | import android.view.View;
15 | import android.view.ViewConfiguration;
16 | import android.view.ViewGroup;
17 | import android.view.ViewParent;
18 | import android.view.accessibility.AccessibilityEvent;
19 | import android.view.animation.AnimationUtils;
20 | import android.widget.FrameLayout;
21 | import android.widget.Scroller;
22 |
23 | import com.moagrius.scrollview.R;
24 |
25 |
26 | /**
27 | * This is a 2D scroller modified from ScrollView and HorizontalScrollView,
28 | * taken from the KitKat release (API 16)
29 | * At the time of this writing, KitKat and later accounted for more than 95% of devices according to https://developer.android.com/about/dashboards/
30 | * https://android.googlesource.com/platform/frameworks/base/+/kitkat-release/core/java/android/widget/ScrollView.java
31 | * https://android.googlesource.com/platform/frameworks/base/+/kitkat-release/core/java/android/widget/HorizontalScrollView.java
32 | *
33 | * Some minor changes were also made, informed by the most modern version at the time of this writing:
34 | * https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/ScrollView.java
35 | * https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/HorizontalScrollView.java
36 | * and similar classes, like:
37 | * https://android.googlesource.com/platform/frameworks/support/+/master/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
38 | * https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/GestureDetector.java
39 | *
40 | * I've modified from the source for a few reasons:
41 | * 1. Anything that was required for functionality on both axes.
42 | * 2. Inaccessibility (package-private, internal, etc).
43 | * 3. There's very little left around child focus, as a 2D scroll view is likely to be a form container.
44 | * 4. Fading edge logic has been removed.
45 | * 5. Certain accessibility functions have been removed (e.g., does "scroll forward" mean down, or right?)
46 | * 6. Using a Scroller rather than an OverScroller; over-scroll seems less helpful for "panning" views than a list-type view.
47 | *
48 | * Mike Dunn
49 | * June 2018
50 | */
51 |
52 | public class ScrollView extends FrameLayout {
53 |
54 | private static final int ANIMATED_SCROLL_GAP = 250;
55 | private static final int INVALID_POINTER = -1;
56 |
57 | private static final int DIRECTION_BACKWARD = -1;
58 | private static final int DIRECTION_FORWARD = 1;
59 |
60 | private static final String ADD_VIEW_ERROR_MESSAGE = "ScrollView can host only one direct child";
61 |
62 | private long mLastScroll;
63 | private final Rect mTempRect = new Rect();
64 | private Scroller mScroller;
65 | private int mLastMotionY;
66 | private int mLastMotionX;
67 | private boolean mIsLayoutDirty = true;
68 | private View mChildToScrollTo = null;
69 | private boolean mIsBeingDragged = false;
70 | private VelocityTracker mVelocityTracker;
71 | private boolean mFillViewport;
72 | private boolean mSmoothScrollingEnabled = true;
73 | private int mTouchSlop;
74 | private int mMinimumVelocity;
75 | private int mMaximumVelocity;
76 | private int mActivePointerId = INVALID_POINTER;
77 | private SavedState mSavedState;
78 |
79 | public ScrollView(Context context) {
80 | this(context, null);
81 | }
82 |
83 | public ScrollView(Context context, AttributeSet attrs) {
84 | this(context, attrs, 0);
85 | }
86 |
87 | public ScrollView(Context context, AttributeSet attrs, int defStyle) {
88 | super(context, attrs, defStyle);
89 | initScrollView();
90 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScrollView, defStyle, 0);
91 | setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
92 | a.recycle();
93 | }
94 |
95 | @Override
96 | public boolean shouldDelayChildPressedState() {
97 | return true;
98 | }
99 |
100 | private void initScrollView() {
101 | setFocusable(true);
102 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
103 | setWillNotDraw(false);
104 | final ViewConfiguration configuration = ViewConfiguration.get(getContext());
105 | mTouchSlop = configuration.getScaledTouchSlop();
106 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
107 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
108 | mScroller = new Scroller(getContext());
109 | }
110 |
111 | public boolean isFillViewport() {
112 | return mFillViewport;
113 | }
114 |
115 | public void setFillViewport(boolean fillViewport) {
116 | if (fillViewport != mFillViewport) {
117 | mFillViewport = fillViewport;
118 | requestLayout();
119 | }
120 | }
121 |
122 | protected boolean hasContent() {
123 | return getChildCount() > 0;
124 | }
125 |
126 | protected View getChild() {
127 | if (hasContent()) {
128 | return getChildAt(0);
129 | }
130 | return null;
131 | }
132 |
133 | // SCROLLING
134 |
135 | public int getContentWidth() {
136 | if (hasContent()) {
137 | return getChild().getMeasuredWidth();
138 | }
139 | return 0;
140 | }
141 |
142 | public int getContentHeight() {
143 | if (hasContent()) {
144 | return getChild().getMeasuredHeight();
145 | }
146 | return 0;
147 | }
148 |
149 | protected int getConstrainedScrollX(int x) {
150 | return Math.max(getScrollMinX(), Math.min(x, getHorizontalScrollRange()));
151 | }
152 |
153 | protected int getConstrainedScrollY(int y) {
154 | return Math.max(getScrollMinY(), Math.min(y, getVerticalScrollRange()));
155 | }
156 |
157 | protected int getVerticalScrollRange() {
158 | if (!hasContent()) {
159 | return 0;
160 | }
161 | return Math.max(0, getContentHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
162 | }
163 |
164 | protected int getHorizontalScrollRange() {
165 | if (!hasContent()) {
166 | return 0;
167 | }
168 | return Math.max(0, getContentWidth() - (getWidth() - getPaddingLeft() - getPaddingRight()));
169 | }
170 |
171 | protected int getContentRight() {
172 | if (hasContent()) {
173 | return getChild().getLeft() + getContentWidth();
174 | }
175 | return 0;
176 | }
177 |
178 | protected int getContentBottom() {
179 | if (hasContent()) {
180 | return getChild().getTop() + getContentHeight();
181 | }
182 | return 0;
183 | }
184 |
185 | @Override
186 | protected int computeHorizontalScrollRange() {
187 | if (!hasContent()) {
188 | return getWidth() - getPaddingLeft() - getPaddingRight();
189 | }
190 | return getContentRight();
191 | }
192 |
193 | @Override
194 | protected int computeVerticalScrollRange() {
195 | if (!hasContent()) {
196 | return getHeight() - getPaddingBottom() - getPaddingTop();
197 | }
198 | return getContentBottom();
199 | }
200 |
201 | @Override
202 | protected int computeHorizontalScrollOffset() {
203 | return Math.max(0, super.computeHorizontalScrollOffset());
204 | }
205 |
206 | @Override
207 | protected int computeVerticalScrollOffset() {
208 | return Math.max(0, super.computeVerticalScrollOffset());
209 | }
210 |
211 | protected int getScrollMinX() {
212 | return 0;
213 | }
214 |
215 | protected int getScrollMinY() {
216 | return 0;
217 | }
218 |
219 | @Override
220 | public boolean canScrollHorizontally(int direction) {
221 | int position = getScrollX();
222 | return direction > 0 ? position < getHorizontalScrollRange() : direction < 0 && position > 0;
223 | }
224 |
225 | @Override
226 | public boolean canScrollVertically(int direction) {
227 | int position = getScrollY();
228 | return direction > 0 ? position < getVerticalScrollRange() : direction < 0 && position > 0;
229 | }
230 |
231 | public boolean canScroll(int direction) {
232 | return canScrollVertically(direction) || canScrollHorizontally(direction);
233 | }
234 |
235 | public boolean canScroll() {
236 | return canScroll(DIRECTION_FORWARD) || canScroll(DIRECTION_BACKWARD);
237 | }
238 |
239 | public boolean isSmoothScrollingEnabled() {
240 | return mSmoothScrollingEnabled;
241 | }
242 |
243 | public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
244 | mSmoothScrollingEnabled = smoothScrollingEnabled;
245 | }
246 |
247 | public final void smoothScrollBy(int dx, int dy) {
248 | if (!hasContent()) {
249 | return;
250 | }
251 | long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
252 | if (duration > ANIMATED_SCROLL_GAP) {
253 | final int width = getWidth() - getPaddingRight() - getPaddingLeft();
254 | final int right = getChildAt(0).getWidth();
255 | final int maxX = Math.max(0, right - width);
256 | final int scrollX = getScrollX();
257 | dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
258 | final int height = getHeight() - getPaddingBottom() - getPaddingTop();
259 | final int bottom = getChild().getHeight();
260 | final int maxY = Math.max(0, bottom - height);
261 | final int scrollY = getScrollY();
262 | dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
263 | mScroller.startScroll(scrollX, scrollY, dx, dy);
264 | postInvalidateOnAnimation();
265 | } else {
266 | if (!mScroller.isFinished()) {
267 | mScroller.abortAnimation();
268 | }
269 | scrollBy(dx, dy);
270 | }
271 | mLastScroll = AnimationUtils.currentAnimationTimeMillis();
272 | }
273 |
274 | public final void smoothScrollTo(int x, int y) {
275 | smoothScrollBy(x - getScrollX(), y - getScrollY());
276 | }
277 |
278 | private void performScrollBy(int x, int y) {
279 | if (mSmoothScrollingEnabled) {
280 | smoothScrollBy(x, y);
281 | } else {
282 | scrollBy(x, y);
283 | }
284 | }
285 |
286 | @Override
287 | public void scrollTo(int x, int y) {
288 | if (hasContent()) {
289 | x = getConstrainedScrollX(x);
290 | y = getConstrainedScrollY(y);
291 | if (x != getScrollX() || y != getScrollY()) {
292 | super.scrollTo(x, y);
293 | }
294 | }
295 | }
296 |
297 | @Override
298 | public void scrollBy(int x, int y) {
299 | scrollTo(getScrollX() + x, getScrollY() + y);
300 | }
301 |
302 | @Override
303 | public void computeScroll() {
304 | if (mScroller.computeScrollOffset()) {
305 | int x = mScroller.getCurrX();
306 | int y = mScroller.getCurrY();
307 | scrollTo(x, y);
308 | if (!awakenScrollBars()) {
309 | postInvalidateOnAnimation();
310 | }
311 | }
312 | }
313 |
314 | private void initOrResetVelocityTracker() {
315 | if (mVelocityTracker == null) {
316 | mVelocityTracker = VelocityTracker.obtain();
317 | } else {
318 | mVelocityTracker.clear();
319 | }
320 | }
321 |
322 | private void initVelocityTrackerIfNotExists() {
323 | if (mVelocityTracker == null) {
324 | mVelocityTracker = VelocityTracker.obtain();
325 | }
326 | }
327 |
328 | private void recycleVelocityTracker() {
329 | if (mVelocityTracker != null) {
330 | mVelocityTracker.recycle();
331 | mVelocityTracker = null;
332 | }
333 | }
334 |
335 | @Override
336 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
337 | if (disallowIntercept) {
338 | recycleVelocityTracker();
339 | }
340 | super.requestDisallowInterceptTouchEvent(disallowIntercept);
341 | }
342 |
343 | @Override
344 | public boolean onInterceptTouchEvent(MotionEvent event) {
345 | final int action = event.getAction();
346 | if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
347 | return true;
348 | }
349 | if (!canScroll()) {
350 | return false;
351 | }
352 | switch (action & MotionEvent.ACTION_MASK) {
353 | case MotionEvent.ACTION_MOVE: {
354 | final int activePointerId = mActivePointerId;
355 | if (activePointerId == INVALID_POINTER) {
356 | break;
357 | }
358 | final int pointerIndex = event.findPointerIndex(activePointerId);
359 | if (pointerIndex == -1) {
360 | break;
361 | }
362 | final int x = (int) event.getX(pointerIndex);
363 | final int y = (int) event.getY(pointerIndex);
364 | final int xDiff = Math.abs(x - mLastMotionX);
365 | final int yDiff = Math.abs(y - mLastMotionY);
366 | if (yDiff > mTouchSlop || xDiff > mTouchSlop) {
367 | mIsBeingDragged = true;
368 | mLastMotionY = y;
369 | mLastMotionX = x;
370 | initVelocityTrackerIfNotExists();
371 | mVelocityTracker.addMovement(event);
372 | final ViewParent parent = getParent();
373 | if (parent != null) {
374 | parent.requestDisallowInterceptTouchEvent(true);
375 | }
376 | }
377 | break;
378 | }
379 | case MotionEvent.ACTION_DOWN: {
380 | final int y = (int) event.getY();
381 | final int x = (int) event.getX();
382 | if (!inChild(x, y)) {
383 | mIsBeingDragged = false;
384 | recycleVelocityTracker();
385 | break;
386 | }
387 | mLastMotionY = y;
388 | mLastMotionX = x;
389 | mActivePointerId = event.getPointerId(0);
390 | initOrResetVelocityTracker();
391 | mVelocityTracker.addMovement(event);
392 | mIsBeingDragged = !mScroller.isFinished();
393 | break;
394 | }
395 | case MotionEvent.ACTION_CANCEL:
396 | case MotionEvent.ACTION_UP:
397 | mIsBeingDragged = false;
398 | mActivePointerId = INVALID_POINTER;
399 | recycleVelocityTracker();
400 | break;
401 | case MotionEvent.ACTION_POINTER_UP:
402 | onSecondaryPointerUp(event);
403 | break;
404 | }
405 | return mIsBeingDragged;
406 | }
407 |
408 | @Override
409 | public boolean onTouchEvent(MotionEvent event) {
410 | initVelocityTrackerIfNotExists();
411 | mVelocityTracker.addMovement(event);
412 | final int action = event.getAction();
413 | switch (action & MotionEvent.ACTION_MASK) {
414 | case MotionEvent.ACTION_DOWN: {
415 | if (!hasContent()) {
416 | return false;
417 | }
418 | if (mIsBeingDragged = !mScroller.isFinished()) {
419 | final ViewParent parent = getParent();
420 | if (parent != null) {
421 | parent.requestDisallowInterceptTouchEvent(true);
422 | }
423 | }
424 | if (!mScroller.isFinished()) {
425 | mScroller.abortAnimation();
426 | }
427 | mLastMotionY = (int) event.getY();
428 | mLastMotionX = (int) event.getX();
429 | mActivePointerId = event.getPointerId(0);
430 | break;
431 | }
432 | case MotionEvent.ACTION_MOVE:
433 | final int activePointerIndex = event.findPointerIndex(mActivePointerId);
434 | if (activePointerIndex == -1) {
435 | break;
436 | }
437 | final int y = (int) event.getY(activePointerIndex);
438 | final int x = (int) event.getX(activePointerIndex);
439 | int deltaY = mLastMotionY - y;
440 | int deltaX = mLastMotionX - x;
441 | if (!mIsBeingDragged && (Math.abs(deltaY) > mTouchSlop || Math.abs(deltaX) > mTouchSlop)) {
442 | final ViewParent parent = getParent();
443 | if (parent != null) {
444 | parent.requestDisallowInterceptTouchEvent(true);
445 | }
446 | mIsBeingDragged = true;
447 | deltaX += mTouchSlop * (deltaX < 0 ? -1 : 1);
448 | deltaY += mTouchSlop * (deltaY < 0 ? -1 : 1);
449 | }
450 | if (mIsBeingDragged) {
451 | mLastMotionY = y;
452 | mLastMotionX = x;
453 | scrollBy(deltaX, deltaY);
454 | }
455 | break;
456 | case MotionEvent.ACTION_UP:
457 | if (mIsBeingDragged) {
458 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
459 | int velocityX = (int) mVelocityTracker.getXVelocity(mActivePointerId);
460 | int velocityY = (int) mVelocityTracker.getYVelocity(mActivePointerId);
461 | if (hasContent()) {
462 | if (Math.abs(velocityX) > mMinimumVelocity || Math.abs(velocityY) > mMinimumVelocity) {
463 | mScroller.fling(getScrollX(), getScrollY(), -velocityX, -velocityY, 0, getHorizontalScrollRange(), 0, getVerticalScrollRange());
464 | postInvalidateOnAnimation();
465 | }
466 | }
467 | mActivePointerId = INVALID_POINTER;
468 | endDrag();
469 | }
470 | break;
471 | case MotionEvent.ACTION_CANCEL:
472 | if (mIsBeingDragged && hasContent()) {
473 | mActivePointerId = INVALID_POINTER;
474 | endDrag();
475 | }
476 | break;
477 | case MotionEvent.ACTION_POINTER_DOWN: {
478 | final int index = event.getActionIndex();
479 | mLastMotionY = (int) event.getY(index);
480 | mLastMotionX = (int) event.getX(index);
481 | mActivePointerId = event.getPointerId(index);
482 | break;
483 | }
484 | case MotionEvent.ACTION_POINTER_UP:
485 | onSecondaryPointerUp(event);
486 | mLastMotionY = (int) event.getY(event.findPointerIndex(mActivePointerId));
487 | mLastMotionX = (int) event.getX(event.findPointerIndex(mActivePointerId));
488 | break;
489 | }
490 | return true;
491 | }
492 |
493 | private void onSecondaryPointerUp(MotionEvent ev) {
494 | final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
495 | final int pointerId = ev.getPointerId(pointerIndex);
496 | if (pointerId == mActivePointerId) {
497 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
498 | mLastMotionY = (int) ev.getY(newPointerIndex);
499 | mActivePointerId = ev.getPointerId(newPointerIndex);
500 | if (mVelocityTracker != null) {
501 | mVelocityTracker.clear();
502 | }
503 | }
504 | }
505 |
506 | private void endDrag() {
507 | mIsBeingDragged = false;
508 | recycleVelocityTracker();
509 | }
510 |
511 | private void scrollToChild(View child) {
512 | child.getDrawingRect(mTempRect);
513 | offsetDescendantRectToMyCoords(child, mTempRect);
514 | int deltaX = computeScrollXDeltaToGetChildRectOnScreen(mTempRect);
515 | int deltaY = computeScrollYDeltaToGetChildRectOnScreen(mTempRect);
516 | if (deltaY != 0 || deltaX != 0) {
517 | scrollBy(deltaX, deltaY);
518 | }
519 | }
520 |
521 | private boolean scrollToChildRect(Rect rect, boolean immediate) {
522 | final int deltaX = computeScrollXDeltaToGetChildRectOnScreen(rect);
523 | final int deltaY = computeScrollYDeltaToGetChildRectOnScreen(rect);
524 | final boolean scroll = deltaY != 0 || deltaX != 0;
525 | if (scroll) {
526 | if (immediate) {
527 | scrollBy(deltaX, deltaY);
528 | } else {
529 | smoothScrollBy(deltaX, deltaY);
530 | }
531 | }
532 | return scroll;
533 | }
534 |
535 | protected int computeScrollYDeltaToGetChildRectOnScreen(Rect rect) {
536 | if (!hasContent()) {
537 | return 0;
538 | }
539 | int height = getHeight();
540 | int screenTop = getScrollY();
541 | int screenBottom = screenTop + height;
542 | int scrollYDelta = 0;
543 | if (rect.bottom > screenBottom && rect.top > screenTop) {
544 | if (rect.height() > height) {
545 | scrollYDelta += (rect.top - screenTop);
546 | } else {
547 | scrollYDelta += (rect.bottom - screenBottom);
548 | }
549 | int bottom = getChild().getBottom();
550 | int distanceToBottom = bottom - screenBottom;
551 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
552 | } else if (rect.top < screenTop && rect.bottom < screenBottom) {
553 | if (rect.height() > height) {
554 | scrollYDelta -= (screenBottom - rect.bottom);
555 | } else {
556 | scrollYDelta -= (screenTop - rect.top);
557 | }
558 | scrollYDelta = Math.max(scrollYDelta, -getScrollY());
559 | }
560 | return scrollYDelta;
561 | }
562 |
563 | protected int computeScrollXDeltaToGetChildRectOnScreen(Rect rect) {
564 | if (!hasContent()) {
565 | return 0;
566 | }
567 | int width = getWidth();
568 | int screenLeft = getScrollX();
569 | int screenRight = screenLeft + width;
570 | int scrollXDelta = 0;
571 | if (rect.right > screenRight && rect.left > screenLeft) {
572 | if (rect.width() > width) {
573 | scrollXDelta += (rect.left - screenLeft);
574 | } else {
575 | scrollXDelta += (rect.right - screenRight);
576 | }
577 | int right = getChild().getRight();
578 | int distanceToRight = right - screenRight;
579 | scrollXDelta = Math.min(scrollXDelta, distanceToRight);
580 | } else if (rect.left < screenLeft && rect.right < screenRight) {
581 | if (rect.width() > width) {
582 | scrollXDelta -= (screenRight - rect.right);
583 | } else {
584 | scrollXDelta -= (screenLeft - rect.left);
585 | }
586 | scrollXDelta = Math.max(scrollXDelta, -getScrollX());
587 | }
588 | return scrollXDelta;
589 | }
590 |
591 | // VIEW HIERARCHY, LAYOUT & MEASURE
592 |
593 | private void assertSingleChild() {
594 | if (getChildCount() > 0) {
595 | throw new IllegalStateException(ADD_VIEW_ERROR_MESSAGE);
596 | }
597 | }
598 |
599 | @Override
600 | public void addView(View child) {
601 | assertSingleChild();
602 | super.addView(child);
603 | }
604 |
605 | @Override
606 | public void addView(View child, int index) {
607 | assertSingleChild();
608 | super.addView(child, index);
609 | }
610 |
611 | @Override
612 | public void addView(View child, ViewGroup.LayoutParams params) {
613 | assertSingleChild();
614 | super.addView(child, params);
615 | }
616 |
617 | @Override
618 | public void addView(View child, int index, ViewGroup.LayoutParams params) {
619 | assertSingleChild();
620 | super.addView(child, index, params);
621 | }
622 |
623 | @Override
624 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
625 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
626 | if (!mFillViewport) {
627 | return;
628 | }
629 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
630 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
631 | if (heightMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.UNSPECIFIED) {
632 | return;
633 | }
634 | if (hasContent()) {
635 | final View child = getChild();
636 | int height = getMeasuredHeight();
637 | int width = getMeasuredWidth();
638 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
639 | if (child.getMeasuredHeight() < height || child.getMeasuredWidth() < width) {
640 | int childWidthMeasureSpec;
641 | int childHeightMeasureSpec;
642 | if (child.getMeasuredHeight() < height) {
643 | height -= getPaddingTop();
644 | height -= getPaddingBottom();
645 | childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
646 | } else {
647 | childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height);
648 | }
649 | if (child.getMeasuredWidth() < width) {
650 | width -= getPaddingLeft();
651 | width -= getPaddingRight();
652 | childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
653 | } else {
654 | childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
655 | }
656 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
657 | }
658 | }
659 | }
660 |
661 | @Override
662 | protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
663 | ViewGroup.LayoutParams lp = child.getLayoutParams();
664 | final int childWidthMeasureSpec = getScrollViewChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
665 | final int childHeightMeasureSpec = getScrollViewChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height);
666 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
667 | }
668 |
669 | @Override
670 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
671 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
672 | final int childWidthMeasureSpec = getScrollViewChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
673 | final int childHeightMeasureSpec = getScrollViewChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin + widthUsed, lp.height);
674 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
675 | }
676 |
677 | public static int getScrollViewChildMeasureSpec(int spec, int padding, int childDimension) {
678 | int specMode = MeasureSpec.getMode(spec);
679 | int specSize = MeasureSpec.getSize(spec);
680 |
681 | int size = Math.max(0, specSize - padding);
682 |
683 | int resultSize = 0;
684 | int resultMode = 0;
685 |
686 | switch (specMode) {
687 | case MeasureSpec.EXACTLY:
688 | if (childDimension >= 0) {
689 | resultSize = childDimension;
690 | resultMode = MeasureSpec.EXACTLY;
691 | } else if (childDimension == LayoutParams.MATCH_PARENT) {
692 | resultSize = size;
693 | resultMode = MeasureSpec.EXACTLY;
694 | } else if (childDimension == LayoutParams.WRAP_CONTENT) {
695 | resultSize = size;
696 | resultMode = MeasureSpec.UNSPECIFIED;
697 | }
698 | break;
699 |
700 | case MeasureSpec.AT_MOST:
701 | if (childDimension >= 0) {
702 | resultSize = childDimension;
703 | resultMode = MeasureSpec.EXACTLY;
704 | } else if (childDimension == LayoutParams.MATCH_PARENT) {
705 | resultSize = size;
706 | resultMode = MeasureSpec.AT_MOST;
707 | } else if (childDimension == LayoutParams.WRAP_CONTENT) {
708 | resultSize = size;
709 | resultMode = MeasureSpec.UNSPECIFIED;
710 | }
711 | break;
712 |
713 | case MeasureSpec.UNSPECIFIED:
714 | if (childDimension >= 0) {
715 | resultSize = childDimension;
716 | resultMode = MeasureSpec.EXACTLY;
717 | } else if (childDimension == LayoutParams.MATCH_PARENT) {
718 | resultSize = size;
719 | resultMode = MeasureSpec.UNSPECIFIED;
720 | } else if (childDimension == LayoutParams.WRAP_CONTENT) {
721 | resultSize = size;
722 | resultMode = MeasureSpec.UNSPECIFIED;
723 | }
724 | break;
725 | }
726 | return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
727 | }
728 |
729 | @Override
730 | public void requestLayout() {
731 | mIsLayoutDirty = true;
732 | super.requestLayout();
733 | }
734 |
735 | @Override
736 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
737 | super.onLayout(changed, l, t, r, b);
738 | mIsLayoutDirty = false;
739 | if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
740 | scrollToChild(mChildToScrollTo);
741 | }
742 | mChildToScrollTo = null;
743 | if (!isLaidOut()) {
744 | if (mSavedState != null) {
745 | setScrollX(mSavedState.scrollPositionX);
746 | setScrollY(mSavedState.scrollPositionY);
747 | mSavedState = null;
748 | }
749 | }
750 | scrollTo(getScrollX(), getScrollY());
751 | }
752 |
753 | @Override
754 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
755 | super.onSizeChanged(w, h, oldw, oldh);
756 | View currentFocused = findFocus();
757 | if (null == currentFocused || this == currentFocused) {
758 | return;
759 | }
760 | if (isWithinDeltaOfScreen(currentFocused, 0, oldw, oldh)) {
761 | currentFocused.getDrawingRect(mTempRect);
762 | offsetDescendantRectToMyCoords(currentFocused, mTempRect);
763 | int deltaX = computeScrollXDeltaToGetChildRectOnScreen(mTempRect);
764 | int deltaY = computeScrollYDeltaToGetChildRectOnScreen(mTempRect);
765 | performScrollBy(deltaX, deltaY);
766 | }
767 | }
768 |
769 | // UTILITY
770 |
771 | private boolean inChild(int x, int y) {
772 | if (hasContent()) {
773 | final int scrollY = getScrollY();
774 | final int scrollX = getScrollX();
775 | final View child = getChild();
776 | return !(y < child.getTop() - scrollY
777 | || y >= child.getBottom() - scrollY
778 | || x < child.getLeft() - scrollX
779 | || x >= child.getRight() - scrollX);
780 | }
781 | return false;
782 | }
783 |
784 | private boolean isOffScreen(View descendant) {
785 | return !isWithinDeltaOfScreen(descendant, 0, getWidth(), getHeight());
786 | }
787 |
788 | private boolean isWithinDeltaOfScreen(View descendant, int delta, int width, int height) {
789 | descendant.getDrawingRect(mTempRect);
790 | offsetDescendantRectToMyCoords(descendant, mTempRect);
791 | return ((mTempRect.bottom + delta) >= getScrollY() && (mTempRect.top - delta) <= (getScrollY() + height))
792 | && ((mTempRect.right + delta) >= getScrollX() && (mTempRect.left - delta) <= (getScrollX() + width));
793 | }
794 |
795 | @Override
796 | public void requestChildFocus(View child, View focused) {
797 | if (!mIsLayoutDirty) {
798 | scrollToChild(focused);
799 | } else {
800 | mChildToScrollTo = focused;
801 | }
802 | super.requestChildFocus(child, focused);
803 | }
804 |
805 | @Override
806 | protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
807 | if (direction == View.FOCUS_FORWARD) {
808 | direction = View.FOCUS_DOWN;
809 | } else if (direction == View.FOCUS_BACKWARD) {
810 | direction = View.FOCUS_UP;
811 | }
812 | final View nextFocus = previouslyFocusedRect == null ? FocusFinder.getInstance().findNextFocus(this, null, direction) : FocusFinder.getInstance().findNextFocusFromRect(this, previouslyFocusedRect, direction);
813 | if (nextFocus == null) {
814 | return false;
815 | }
816 | if (isOffScreen(nextFocus)) {
817 | return false;
818 | }
819 | return nextFocus.requestFocus(direction, previouslyFocusedRect);
820 | }
821 |
822 | @Override
823 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
824 | rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
825 | return scrollToChildRect(rectangle, immediate);
826 | }
827 |
828 | private static boolean isViewDescendantOf(View child, View parent) {
829 | if (child == parent) {
830 | return true;
831 | }
832 | final ViewParent parentOfChild = child.getParent();
833 | return (parentOfChild instanceof ViewGroup) && isViewDescendantOf((View) parentOfChild, parent);
834 | }
835 |
836 |
837 | // INPUT & ACCESSIBILITY
838 |
839 | @Override
840 | public boolean onGenericMotionEvent(MotionEvent event) {
841 | if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
842 | switch (event.getAction()) {
843 | case MotionEvent.ACTION_SCROLL: {
844 | if (!mIsBeingDragged) {
845 | final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
846 | final float hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
847 | if (vscroll != 0) {
848 | final int verticalScrollRange = getVerticalScrollRange();
849 | final int horizontalScrollRange = getHorizontalScrollRange();
850 | int oldScrollY = getScrollY();
851 | int oldScrollX = getScrollX();
852 | int newScrollY = (int) (oldScrollY - vscroll);
853 | int newScrollX = (int) (oldScrollX - hscroll);
854 | if (newScrollY < 0) {
855 | newScrollY = 0;
856 | } else if (newScrollY > verticalScrollRange) {
857 | newScrollY = verticalScrollRange;
858 | }
859 | if (newScrollX < 0) {
860 | newScrollX = 0;
861 | } else if (newScrollX > horizontalScrollRange) {
862 | newScrollX = horizontalScrollRange;
863 | }
864 | if (newScrollY != oldScrollY || newScrollX != oldScrollX) {
865 | super.scrollTo(newScrollX, newScrollY);
866 | return true;
867 | }
868 | }
869 | }
870 | }
871 | }
872 | }
873 | return super.onGenericMotionEvent(event);
874 | }
875 |
876 | @Override
877 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
878 | super.onInitializeAccessibilityEvent(event);
879 | event.setClassName(ScrollView.class.getName());
880 | event.setScrollable(canScroll());
881 | event.setScrollX(getScrollX());
882 | event.setScrollY(getScrollY());
883 | event.setMaxScrollX(getHorizontalScrollRange());
884 | event.setMaxScrollY(getVerticalScrollRange());
885 | }
886 |
887 | @Override
888 | public boolean dispatchKeyEvent(KeyEvent event) {
889 | return super.dispatchKeyEvent(event) || executeKeyEvent(event);
890 | }
891 |
892 | public boolean executeKeyEvent(KeyEvent event) {
893 | if (!canScroll()) {
894 | if (isFocused()) {
895 | View currentFocused = findFocus();
896 | if (currentFocused == this) {
897 | currentFocused = null;
898 | }
899 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN);
900 | return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN);
901 | }
902 | return false;
903 | }
904 | boolean alt = event.isAltPressed();
905 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
906 | switch (event.getKeyCode()) {
907 | case KeyEvent.KEYCODE_DPAD_UP:
908 | if (canScrollVertically(DIRECTION_BACKWARD)) {
909 | if (alt) {
910 | performScrollBy(0, -getScrollY());
911 | } else {
912 | performScrollBy(0, -getHeight());
913 | }
914 | return true;
915 | }
916 | break;
917 | case KeyEvent.KEYCODE_DPAD_DOWN:
918 | // if we can scroll down
919 | if (canScrollVertically(DIRECTION_FORWARD)) {
920 | // if alt is down, scroll all the way to the end of content
921 | if (alt) {
922 | performScrollBy(0, getChild().getMeasuredHeight() - getScrollY());
923 | } else { // otherwise scroll down one "page" (height)
924 | performScrollBy(0, getHeight());
925 | }
926 | return true;
927 | }
928 | break;
929 | case KeyEvent.KEYCODE_DPAD_LEFT:
930 | // if we can scroll left
931 | if (canScrollHorizontally(DIRECTION_BACKWARD)) {
932 | // if alt is down, scroll all the way home
933 | if (alt) {
934 | performScrollBy(0, -getScrollX());
935 | } else { // otherwise scroll left one "page" (width)
936 | performScrollBy(0, -getWidth());
937 | }
938 | return true;
939 | }
940 | break;
941 | case KeyEvent.KEYCODE_DPAD_RIGHT:
942 | // if we can scroll right
943 | if (canScrollHorizontally(DIRECTION_FORWARD)) {
944 | // if alt is down, scroll all the way to the end of content
945 | if (alt) {
946 | performScrollBy(getChild().getMeasuredWidth() - getScrollX(), 0);
947 | } else { // otherwise scroll right one "page" (width)
948 | performScrollBy(getWidth(), 0);
949 | }
950 | return true;
951 | }
952 | break;
953 | }
954 | }
955 | return false;
956 | }
957 |
958 | // STATE
959 |
960 | @Override
961 | protected void onRestoreInstanceState(Parcelable state) {
962 | BaseSavedState baseSavedState = (BaseSavedState) state;
963 | super.onRestoreInstanceState(baseSavedState.getSuperState());
964 | restoreInstanceState(state);
965 | }
966 |
967 | // so we don't have to get super's behavior
968 | protected void restoreInstanceState(Parcelable state) {
969 | mSavedState = (SavedState) state;
970 | requestLayout();
971 | }
972 |
973 | @Override
974 | protected Parcelable onSaveInstanceState() {
975 | Parcelable superState = super.onSaveInstanceState();
976 | SavedState ss = new SavedState(superState);
977 | ss.scrollPositionY = getScrollY();
978 | ss.scrollPositionX = getScrollX();
979 | return ss;
980 | }
981 |
982 | protected static class SavedState extends BaseSavedState {
983 | public int scrollPositionY;
984 | public int scrollPositionX;
985 |
986 | public SavedState(Parcelable superState) {
987 | super(superState);
988 | }
989 |
990 | public SavedState(Parcel source) {
991 | super(source);
992 | scrollPositionX = source.readInt();
993 | scrollPositionY = source.readInt();
994 | }
995 |
996 | @Override
997 | public void writeToParcel(Parcel dest, int flags) {
998 | super.writeToParcel(dest, flags);
999 | dest.writeInt(scrollPositionX);
1000 | dest.writeInt(scrollPositionY);
1001 | }
1002 |
1003 | @Override
1004 | public String toString() {
1005 | return "ScrollView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " scrollPositionY=" + scrollPositionY + ", scrollPositionX=" + scrollPositionX + "}";
1006 | }
1007 |
1008 | public static final Creator CREATOR = new Creator() {
1009 | public SavedState createFromParcel(Parcel in) {
1010 | return new SavedState(in);
1011 | }
1012 |
1013 | public SavedState[] newArray(int size) {
1014 | return new SavedState[size];
1015 | }
1016 | };
1017 | }
1018 | }
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-v24/tiger.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
33 |
36 |
39 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
76 |
77 |
80 |
83 |
86 |
89 |
92 |
95 |
98 |
101 |
104 |
107 |
110 |
113 |
114 |
117 |
120 |
123 |
124 |
127 |
130 |
133 |
136 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
188 |
191 |
194 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
248 |
251 |
254 |
257 |
260 |
263 |
266 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
283 |
284 |
287 |
290 |
293 |
296 |
299 |
302 |
305 |
308 |
311 |
314 |
317 |
320 |
323 |
326 |
329 |
332 |
335 |
338 |
341 |
344 |
347 |
350 |
353 |
356 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
390 |
393 |
396 |
399 |
400 |
--------------------------------------------------------------------------------