├── 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 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 | ![Release Badge](https://img.shields.io/github/release/moagrius/ScrollView.svg) 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 | --------------------------------------------------------------------------------