├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── drawable-xhdpi │ │ │ │ ├── icon_food.png │ │ │ │ ├── icon_title.png │ │ │ │ ├── meituan1.png │ │ │ │ ├── meituan2.png │ │ │ │ ├── dp_ad_icon_half.png │ │ │ │ ├── gc_deal_second.png │ │ │ │ ├── icon_descript.png │ │ │ │ ├── takeout_ic_new.png │ │ │ │ ├── dp_ad_icon_star_small.png │ │ │ │ ├── dp_ad_icon_star_small_gary.png │ │ │ │ └── trip_oversea_icon_list_group.png │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── color │ │ │ │ └── bg_scroll_select.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ ├── layout │ │ │ │ ├── fragment_test1.xml │ │ │ │ ├── fragment_test2.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── activity_main_top.xml │ │ │ │ ├── activity_main_include.xml │ │ │ │ └── activity_main_scroll.xml │ │ │ └── drawable │ │ │ │ └── bg_scroll_select.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── xiaochen │ │ │ └── you │ │ │ └── meituan │ │ │ ├── TestFragment.java │ │ │ ├── ConfigUtils.java │ │ │ ├── MainActivity.java │ │ │ └── widget │ │ │ └── NestedScrollView.java │ ├── test │ │ └── java │ │ │ └── xiaochen │ │ │ └── you │ │ │ └── meituan │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── xiaochen │ │ └── you │ │ └── meituan │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── imgs ├── 1.gif └── 2.gif ├── .gitignore ├── README.md └── gradle.properties /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /imgs/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/imgs/1.gif -------------------------------------------------------------------------------- /imgs/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/imgs/2.gif -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | meituan 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/icon_food.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/icon_title.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/meituan1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/meituan1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/meituan2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/meituan2.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/dp_ad_icon_half.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/dp_ad_icon_half.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/gc_deal_second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/gc_deal_second.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_descript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/icon_descript.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/takeout_ic_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/takeout_ic_new.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.DS_Store 3 | /.gradle 4 | /.idea 5 | /build 6 | /gradle 7 | /gradlew 8 | /gradlew.bat 9 | /local.properties 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/dp_ad_icon_star_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/dp_ad_icon_star_small.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/dp_ad_icon_star_small_gary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/dp_ad_icon_star_small_gary.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/trip_oversea_icon_list_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youxiaochen/MeituanDetailScrollDemo/HEAD/app/src/main/res/drawable-xhdpi/trip_oversea_icon_list_group.png -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/bg_scroll_select.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #f1f1f1 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MeituanDetailScrollDemo 2 | 仿美团详情滑动界面,并兼容NestedScroll嵌套 3 | 4 | ### 项目博客地址 http://blog.csdn.net/u012216274/article/details/71024304 5 | 6 | ![效果图1](https://github.com/youxiaochen/MeituanDetailScrollDemo/blob/master/imgs/1.gif) 7 | 8 | 效果图2 9 | 10 | ![效果图2](https://github.com/youxiaochen/MeituanDetailScrollDemo/blob/master/imgs/2.gif) 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/test/java/xiaochen/you/meituan/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package xiaochen.you.meituan; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_test2.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_scroll_select.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in G:\studio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/xiaochen/you/meituan/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package xiaochen.you.meituan; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("xiaochen.you.meituan", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "24.0.3" 6 | 7 | aaptOptions.cruncherEnabled = false 8 | 9 | aaptOptions.useNewCruncher = false 10 | 11 | defaultConfig { 12 | applicationId "xiaochen.you.meituan" 13 | minSdkVersion 15 14 | targetSdkVersion 24 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(include: ['*.jar'], dir: 'libs') 29 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 30 | exclude group: 'com.android.support', module: 'support-annotations' 31 | }) 32 | compile 'com.android.support:appcompat-v7:24.2.1' 33 | compile 'com.android.support:recyclerview-v7:24.2.0' 34 | compile 'io.reactivex:rxandroid:1.2.1' 35 | testCompile 'junit:junit:4.12' 36 | compile 'com.android.support:design:24.2.1' 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/xiaochen/you/meituan/TestFragment.java: -------------------------------------------------------------------------------- 1 | package xiaochen.you.meituan; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.widget.NestedScrollView; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | /** 12 | * Created by dgaz on 2017/4/24. 13 | */ 14 | 15 | public class TestFragment extends Fragment { 16 | 17 | public static TestFragment newInstance(int tag) { 18 | TestFragment f = new TestFragment(); 19 | Bundle bundle = new Bundle(); 20 | bundle.putInt("tag", tag); 21 | f.setArguments(bundle); 22 | return f; 23 | } 24 | 25 | private int tag; 26 | 27 | @Override 28 | public void onCreate(@Nullable Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | tag = getArguments().getInt("tag"); 31 | } 32 | 33 | @Override 34 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 35 | if (tag == 0) { 36 | return inflater.inflate(R.layout.fragment_test1, container, false); 37 | } else { 38 | return inflater.inflate(R.layout.fragment_test2, container, false); 39 | } 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 21 | 22 | 28 | 29 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_include.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | 27 | 33 | 34 | 42 | 43 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/xiaochen/you/meituan/ConfigUtils.java: -------------------------------------------------------------------------------- 1 | package xiaochen.you.meituan; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.support.v4.view.ViewCompat; 7 | import android.view.Gravity; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.Window; 11 | import android.view.WindowManager; 12 | import android.widget.FrameLayout.LayoutParams; 13 | 14 | /** 15 | * Created by you on 16/8/31. 16 | * 配置辅助类 Resources.getSystem() 17 | */ 18 | public class ConfigUtils { 19 | 20 | private ConfigUtils() { 21 | } 22 | 23 | /** 24 | * 4.4以上版本 25 | */ 26 | public static boolean hasKitKat() { 27 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 28 | } 29 | 30 | /** 31 | * 5.0版本 32 | */ 33 | public static boolean hasLollipop() { 34 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 35 | } 36 | 37 | /** 38 | * 获取状态栏高度 39 | */ 40 | public static final int getStatusHeight(Context context) { 41 | int result = 0; 42 | int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 43 | if (resId > 0) { 44 | result = context.getResources().getDimensionPixelOffset(resId); 45 | } 46 | return result; 47 | } 48 | 49 | /** 50 | * 设置activity statusbar颜色, 此方法必须在activitysetContentView之后才可以见效果 51 | * 52 | * @param activity 53 | * @param statusColor 54 | */ 55 | public static void setStatusBarColor(Activity activity, int statusColor) { 56 | Window window = activity.getWindow(); 57 | ViewGroup root = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT); 58 | if (hasKitKat()) {//4.4以上才支持 59 | window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 60 | ViewGroup decorView = (ViewGroup) window.getDecorView(); 61 | Object tag = decorView.getTag(); 62 | if (tag instanceof Boolean && (Boolean) tag) { 63 | View mStatusBarView = decorView.getChildAt(0); 64 | if (mStatusBarView != null) { 65 | mStatusBarView.setBackgroundColor(statusColor); 66 | } 67 | } else { 68 | int statusBarHeight = getStatusHeight(activity); 69 | View contentChild = root.getChildAt(0); 70 | if (contentChild != null) { 71 | ViewCompat.setFitsSystemWindows(contentChild, false); 72 | LayoutParams lp = (LayoutParams) contentChild.getLayoutParams(); 73 | lp.topMargin += statusBarHeight; 74 | contentChild.setLayoutParams(lp); 75 | } 76 | View mStatusBarView = new View(activity); 77 | LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, statusBarHeight); 78 | layoutParams.gravity = Gravity.TOP; 79 | mStatusBarView.setLayoutParams(layoutParams); 80 | mStatusBarView.setBackgroundColor(statusColor); 81 | decorView.addView(mStatusBarView, 0); 82 | decorView.setTag(true); 83 | } 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/xiaochen/you/meituan/MainActivity.java: -------------------------------------------------------------------------------- 1 | package xiaochen.you.meituan; 2 | 3 | import android.graphics.Color; 4 | import android.os.Bundle; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.app.FragmentManager; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.LinearLayout; 12 | import android.widget.RadioGroup; 13 | 14 | import xiaochen.you.meituan.widget.NestedScrollView; 15 | 16 | public class MainActivity extends AppCompatActivity { 17 | /** 18 | * 移除并放大的模块, 悬浮模块(标题), 内容中的标题 19 | */ 20 | private View rl_top, ll_float, ll_title; 21 | /** 22 | * 有反弹的滑动控件 23 | */ 24 | private NestedScrollView sv_root; 25 | /** 26 | * 滑动组中的内容 27 | */ 28 | private LinearLayout ll_content; 29 | /** 30 | * fragment 模块单选组 31 | */ 32 | private RadioGroup rg_group; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | ConfigUtils.setStatusBarColor(this, getResources().getColor(R.color.colorPrimary)); 39 | Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar); 40 | mToolbar.setTitle("游小陈"); 41 | mToolbar.setTitleTextColor(Color.WHITE); 42 | setSupportActionBar(mToolbar); 43 | 44 | rl_top = findViewById(R.id.rl_top); 45 | ll_float = findViewById(R.id.ll_float); 46 | ll_title = findViewById(R.id.ll_title); 47 | sv_root = (NestedScrollView) findViewById(R.id.sv_root); 48 | ll_content = (LinearLayout) findViewById(R.id.ll_content); 49 | rg_group = (RadioGroup) findViewById(R.id.rg_group); 50 | initView(); 51 | initFragmentGroup(); 52 | } 53 | 54 | private void initView() { 55 | sv_root.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() { 56 | @Override 57 | public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 58 | if (scrollY >= 0) {//往上滑动 59 | int height = rl_top.getHeight(); 60 | if (height != ll_content.getPaddingTop()) {//如果滑动时高度有误先矫正高度 61 | ViewGroup.LayoutParams layoutParams = rl_top.getLayoutParams(); 62 | layoutParams.height = ll_content.getPaddingTop(); 63 | rl_top.setLayoutParams(layoutParams); 64 | } 65 | boolean overTitle = scrollY >= height; 66 | ll_float.setVisibility(overTitle ? View.VISIBLE : View.GONE); 67 | ll_title.setVisibility(overTitle ? View.INVISIBLE : View.VISIBLE); 68 | rl_top.setVisibility(overTitle ? View.GONE : View.VISIBLE); 69 | rl_top.scrollTo(0, scrollY / 3); 70 | } else {//下拉滑动 71 | rl_top.scrollTo(0, 0);//不能有滑动偏移 72 | ViewGroup.LayoutParams layoutParams = rl_top.getLayoutParams(); 73 | layoutParams.height = ll_content.getPaddingTop() - scrollY; 74 | rl_top.setLayoutParams(layoutParams); 75 | } 76 | } 77 | }); 78 | } 79 | 80 | private void initFragmentGroup() { 81 | rg_group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { 82 | @Override 83 | public void onCheckedChanged(RadioGroup group, int checkedId) { 84 | switch (checkedId) { 85 | case R.id.f1: 86 | replaceFragment(TestFragment.newInstance(0)); 87 | break; 88 | case R.id.f2: 89 | replaceFragment(TestFragment.newInstance(1)); 90 | break; 91 | } 92 | } 93 | }); 94 | replaceFragment(TestFragment.newInstance(0)); 95 | } 96 | 97 | private void replaceFragment(Fragment f) { 98 | FragmentManager fm = getSupportFragmentManager(); 99 | fm.beginTransaction().replace(R.id.fl_fragment, f).commit(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_scroll.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 19 | 20 | 28 | 29 | 35 | 36 | 44 | 45 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 78 | 79 | 85 | 86 | 98 | 99 | 110 | 111 | 112 | 113 | 117 | 118 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /app/src/main/java/xiaochen/you/meituan/widget/NestedScrollView.java: -------------------------------------------------------------------------------- 1 | package xiaochen.you.meituan.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Rect; 7 | import android.os.Bundle; 8 | import android.os.Parcel; 9 | import android.os.Parcelable; 10 | import android.support.v4.view.AccessibilityDelegateCompat; 11 | import android.support.v4.view.InputDeviceCompat; 12 | import android.support.v4.view.MotionEventCompat; 13 | import android.support.v4.view.NestedScrollingChild; 14 | import android.support.v4.view.NestedScrollingChildHelper; 15 | import android.support.v4.view.NestedScrollingParent; 16 | import android.support.v4.view.NestedScrollingParentHelper; 17 | import android.support.v4.view.ScrollingView; 18 | import android.support.v4.view.VelocityTrackerCompat; 19 | import android.support.v4.view.ViewCompat; 20 | import android.support.v4.view.accessibility.AccessibilityEventCompat; 21 | import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 22 | import android.support.v4.view.accessibility.AccessibilityRecordCompat; 23 | import android.support.v4.widget.EdgeEffectCompat; 24 | import android.support.v4.widget.ScrollerCompat; 25 | import android.util.AttributeSet; 26 | import android.util.Log; 27 | import android.util.TypedValue; 28 | import android.view.FocusFinder; 29 | import android.view.KeyEvent; 30 | import android.view.MotionEvent; 31 | import android.view.VelocityTracker; 32 | import android.view.View; 33 | import android.view.ViewConfiguration; 34 | import android.view.ViewGroup; 35 | import android.view.ViewParent; 36 | import android.view.accessibility.AccessibilityEvent; 37 | import android.view.animation.AnimationUtils; 38 | import android.widget.FrameLayout; 39 | import android.widget.ScrollView; 40 | 41 | import java.util.List; 42 | 43 | /** 44 | * Created by you on 2017/3/29. 45 | */ 46 | 47 | public class NestedScrollView extends FrameLayout implements NestedScrollingParent, 48 | NestedScrollingChild, ScrollingView { 49 | /** 50 | * 支持向下滑动偏移 51 | */ 52 | static final int PULLDOWN_SCALE = 2; 53 | /** 54 | * 向下越界滑动时的滑动比例 55 | */ 56 | static final int PULLDOWN_SCROLL = 3; 57 | 58 | static final int ANIMATED_SCROLL_GAP = 250; 59 | 60 | static final float MAX_SCROLL_FACTOR = 0.5f; 61 | 62 | private static final String TAG = "NestedScrollView"; 63 | 64 | /** 65 | * Interface definition for a callback to be invoked when the scroll 66 | * X or Y positions of a view change. 67 | * 68 | *

This version of the interface works on all versions of Android, back to API v4.

69 | * 70 | * @see #setOnScrollChangeListener(OnScrollChangeListener) 71 | */ 72 | public interface OnScrollChangeListener { 73 | /** 74 | * Called when the scroll position of a view changes. 75 | * 76 | * @param v The view whose scroll position has changed. 77 | * @param scrollX Current horizontal scroll origin. 78 | * @param scrollY Current vertical scroll origin. 79 | * @param oldScrollX Previous horizontal scroll origin. 80 | * @param oldScrollY Previous vertical scroll origin. 81 | */ 82 | void onScrollChange(NestedScrollView v, int scrollX, int scrollY, 83 | int oldScrollX, int oldScrollY); 84 | } 85 | 86 | private long mLastScroll; 87 | 88 | private final Rect mTempRect = new Rect(); 89 | private ScrollerCompat mScroller; 90 | private EdgeEffectCompat mEdgeGlowTop; 91 | private EdgeEffectCompat mEdgeGlowBottom; 92 | 93 | /** 94 | * Position of the last motion event. 95 | */ 96 | private int mLastMotionY; 97 | 98 | /** 99 | * True when the layout has changed but the traversal has not come through yet. 100 | * Ideally the view hierarchy would keep track of this for us. 101 | */ 102 | private boolean mIsLayoutDirty = true; 103 | private boolean mIsLaidOut = false; 104 | 105 | /** 106 | * The child to give focus to in the event that a child has requested focus while the 107 | * layout is dirty. This prevents the scroll from being wrong if the child has not been 108 | * laid out before requesting focus. 109 | */ 110 | private View mChildToScrollTo = null; 111 | 112 | /** 113 | * True if the user is currently dragging this ScrollView around. This is 114 | * not the same as 'is being flinged', which can be checked by 115 | * mScroller.isFinished() (flinging begins when the user lifts his finger). 116 | */ 117 | private boolean mIsBeingDragged = false; 118 | 119 | /** 120 | * Determines speed during touch scrolling 121 | */ 122 | private VelocityTracker mVelocityTracker; 123 | 124 | /** 125 | * When set to true, the scroll view measure its child to make it fill the currently 126 | * visible area. 127 | */ 128 | private boolean mFillViewport; 129 | 130 | /** 131 | * Whether arrow scrolling is animated. 132 | */ 133 | private boolean mSmoothScrollingEnabled = true; 134 | 135 | private int mTouchSlop; 136 | private int mMinimumVelocity; 137 | private int mMaximumVelocity; 138 | 139 | /** 140 | * ID of the active pointer. This is used to retain consistency during 141 | * drags/flings if multiple pointers are used. 142 | */ 143 | private int mActivePointerId = INVALID_POINTER; 144 | 145 | /** 146 | * Used during scrolling to retrieve the new offset within the window. 147 | */ 148 | private final int[] mScrollOffset = new int[2]; 149 | private final int[] mScrollConsumed = new int[2]; 150 | private int mNestedYOffset; 151 | 152 | /** 153 | * Sentinel value for no current active pointer. 154 | * Used by {@link #mActivePointerId}. 155 | */ 156 | private static final int INVALID_POINTER = -1; 157 | 158 | private SavedState mSavedState; 159 | 160 | private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); 161 | 162 | private static final int[] SCROLLVIEW_STYLEABLE = new int[] { 163 | android.R.attr.fillViewport 164 | }; 165 | 166 | private final NestedScrollingParentHelper mParentHelper; 167 | private final NestedScrollingChildHelper mChildHelper; 168 | 169 | private float mVerticalScrollFactor; 170 | 171 | private OnScrollChangeListener mOnScrollChangeListener; 172 | 173 | public NestedScrollView(Context context) { 174 | this(context, null); 175 | } 176 | 177 | public NestedScrollView(Context context, AttributeSet attrs) { 178 | this(context, attrs, 0); 179 | } 180 | 181 | public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 182 | super(context, attrs, defStyleAttr); 183 | initScrollView(); 184 | 185 | final TypedArray a = context.obtainStyledAttributes( 186 | attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); 187 | 188 | setFillViewport(a.getBoolean(0, false)); 189 | 190 | a.recycle(); 191 | 192 | mParentHelper = new NestedScrollingParentHelper(this); 193 | mChildHelper = new NestedScrollingChildHelper(this); 194 | 195 | // ...because why else would you be using this widget? 196 | setNestedScrollingEnabled(true); 197 | 198 | ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); 199 | } 200 | 201 | // NestedScrollingChild 202 | 203 | @Override 204 | public void setNestedScrollingEnabled(boolean enabled) { 205 | mChildHelper.setNestedScrollingEnabled(enabled); 206 | } 207 | 208 | @Override 209 | public boolean isNestedScrollingEnabled() { 210 | return mChildHelper.isNestedScrollingEnabled(); 211 | } 212 | 213 | @Override 214 | public boolean startNestedScroll(int axes) { 215 | return mChildHelper.startNestedScroll(axes); 216 | } 217 | 218 | @Override 219 | public void stopNestedScroll() { 220 | mChildHelper.stopNestedScroll(); 221 | } 222 | 223 | @Override 224 | public boolean hasNestedScrollingParent() { 225 | return mChildHelper.hasNestedScrollingParent(); 226 | } 227 | 228 | @Override 229 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 230 | int dyUnconsumed, int[] offsetInWindow) { 231 | return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 232 | offsetInWindow); 233 | } 234 | 235 | @Override 236 | public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { 237 | return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); 238 | } 239 | 240 | @Override 241 | public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 242 | return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 243 | } 244 | 245 | @Override 246 | public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 247 | return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 248 | } 249 | 250 | // NestedScrollingParent 251 | 252 | @Override 253 | public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 254 | return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 255 | } 256 | 257 | @Override 258 | public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 259 | mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); 260 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 261 | } 262 | 263 | @Override 264 | public void onStopNestedScroll(View target) { 265 | mParentHelper.onStopNestedScroll(target); 266 | stopNestedScroll(); 267 | } 268 | 269 | @Override 270 | public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, 271 | int dyUnconsumed) { 272 | final int oldScrollY = getScrollY(); 273 | scrollBy(0, dyUnconsumed); 274 | final int myConsumed = getScrollY() - oldScrollY; 275 | final int myUnconsumed = dyUnconsumed - myConsumed; 276 | dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); 277 | } 278 | 279 | @Override 280 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 281 | dispatchNestedPreScroll(dx, dy, consumed, null); 282 | } 283 | 284 | @Override 285 | public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 286 | if (!consumed) { 287 | flingWithNestedDispatch((int) velocityY); 288 | return true; 289 | } 290 | return false; 291 | } 292 | 293 | @Override 294 | public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 295 | return dispatchNestedPreFling(velocityX, velocityY); 296 | } 297 | 298 | @Override 299 | public int getNestedScrollAxes() { 300 | return mParentHelper.getNestedScrollAxes(); 301 | } 302 | 303 | // ScrollView import 304 | 305 | public boolean shouldDelayChildPressedState() { 306 | return true; 307 | } 308 | 309 | @Override 310 | protected float getTopFadingEdgeStrength() { 311 | if (getChildCount() == 0) { 312 | return 0.0f; 313 | } 314 | 315 | final int length = getVerticalFadingEdgeLength(); 316 | final int scrollY = getScrollY(); 317 | if (scrollY < length) { 318 | return scrollY / (float) length; 319 | } 320 | 321 | return 1.0f; 322 | } 323 | 324 | @Override 325 | protected float getBottomFadingEdgeStrength() { 326 | if (getChildCount() == 0) { 327 | return 0.0f; 328 | } 329 | 330 | final int length = getVerticalFadingEdgeLength(); 331 | final int bottomEdge = getHeight() - getPaddingBottom(); 332 | final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; 333 | if (span < length) { 334 | return span / (float) length; 335 | } 336 | 337 | return 1.0f; 338 | } 339 | 340 | /** 341 | * @return The maximum amount this scroll view will scroll in response to 342 | * an arrow event. 343 | */ 344 | public int getMaxScrollAmount() { 345 | return (int) (MAX_SCROLL_FACTOR * getHeight()); 346 | } 347 | 348 | private void initScrollView() { 349 | mScroller = ScrollerCompat.create(getContext(), null); 350 | setFocusable(true); 351 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 352 | setWillNotDraw(false); 353 | final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 354 | mTouchSlop = configuration.getScaledTouchSlop(); 355 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 356 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 357 | } 358 | 359 | @Override 360 | public void addView(View child) { 361 | if (getChildCount() > 0) { 362 | throw new IllegalStateException("ScrollView can host only one direct child"); 363 | } 364 | 365 | super.addView(child); 366 | } 367 | 368 | @Override 369 | public void addView(View child, int index) { 370 | if (getChildCount() > 0) { 371 | throw new IllegalStateException("ScrollView can host only one direct child"); 372 | } 373 | 374 | super.addView(child, index); 375 | } 376 | 377 | @Override 378 | public void addView(View child, ViewGroup.LayoutParams params) { 379 | if (getChildCount() > 0) { 380 | throw new IllegalStateException("ScrollView can host only one direct child"); 381 | } 382 | 383 | super.addView(child, params); 384 | } 385 | 386 | @Override 387 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 388 | if (getChildCount() > 0) { 389 | throw new IllegalStateException("ScrollView can host only one direct child"); 390 | } 391 | 392 | super.addView(child, index, params); 393 | } 394 | 395 | /** 396 | * Register a callback to be invoked when the scroll X or Y positions of 397 | * this view change. 398 | *

This version of the method works on all versions of Android, back to API v4.

399 | * 400 | * @param l The listener to notify when the scroll X or Y position changes. 401 | * @see android.view.View#getScrollX() 402 | * @see android.view.View#getScrollY() 403 | */ 404 | public void setOnScrollChangeListener(OnScrollChangeListener l) { 405 | mOnScrollChangeListener = l; 406 | } 407 | 408 | /** 409 | * @return Returns true this ScrollView can be scrolled 410 | */ 411 | private boolean canScroll() { 412 | View child = getChildAt(0); 413 | if (child != null) { 414 | int childHeight = child.getHeight(); 415 | return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); 416 | } 417 | return false; 418 | } 419 | 420 | /** 421 | * Indicates whether this ScrollView's content is stretched to fill the viewport. 422 | * 423 | * @return True if the content fills the viewport, false otherwise. 424 | * 425 | * @attr name android:fillViewport 426 | */ 427 | public boolean isFillViewport() { 428 | return mFillViewport; 429 | } 430 | 431 | /** 432 | * Set whether this ScrollView should stretch its content height to fill the viewport or not. 433 | * 434 | * @param fillViewport True to stretch the content's height to the viewport's 435 | * boundaries, false otherwise. 436 | * 437 | * @attr name android:fillViewport 438 | */ 439 | public void setFillViewport(boolean fillViewport) { 440 | if (fillViewport != mFillViewport) { 441 | mFillViewport = fillViewport; 442 | requestLayout(); 443 | } 444 | } 445 | 446 | /** 447 | * @return Whether arrow scrolling will animate its transition. 448 | */ 449 | public boolean isSmoothScrollingEnabled() { 450 | return mSmoothScrollingEnabled; 451 | } 452 | 453 | /** 454 | * Set whether arrow scrolling will animate its transition. 455 | * @param smoothScrollingEnabled whether arrow scrolling will animate its transition 456 | */ 457 | public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 458 | mSmoothScrollingEnabled = smoothScrollingEnabled; 459 | } 460 | 461 | @Override 462 | protected void onScrollChanged(int l, int t, int oldl, int oldt) { 463 | super.onScrollChanged(l, t, oldl, oldt); 464 | 465 | if (mOnScrollChangeListener != null) { 466 | mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); 467 | } 468 | } 469 | 470 | @Override 471 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 472 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 473 | 474 | if (!mFillViewport) { 475 | return; 476 | } 477 | 478 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 479 | if (heightMode == MeasureSpec.UNSPECIFIED) { 480 | return; 481 | } 482 | 483 | if (getChildCount() > 0) { 484 | final View child = getChildAt(0); 485 | int height = getMeasuredHeight(); 486 | if (child.getMeasuredHeight() < height) { 487 | final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 488 | 489 | int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 490 | getPaddingLeft() + getPaddingRight(), lp.width); 491 | height -= getPaddingTop(); 492 | height -= getPaddingBottom(); 493 | int childHeightMeasureSpec = 494 | MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 495 | 496 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 497 | } 498 | } 499 | } 500 | 501 | @Override 502 | public boolean dispatchKeyEvent(KeyEvent event) { 503 | // Let the focused view and/or our descendants get the key first 504 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 505 | } 506 | 507 | /** 508 | * You can call this function yourself to have the scroll view perform 509 | * scrolling from a key event, just as if the event had been dispatched to 510 | * it by the view hierarchy. 511 | * 512 | * @param event The key event to execute. 513 | * @return Return true if the event was handled, else false. 514 | */ 515 | public boolean executeKeyEvent(KeyEvent event) { 516 | mTempRect.setEmpty(); 517 | 518 | if (!canScroll()) { 519 | if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { 520 | View currentFocused = findFocus(); 521 | if (currentFocused == this) currentFocused = null; 522 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, 523 | currentFocused, View.FOCUS_DOWN); 524 | return nextFocused != null 525 | && nextFocused != this 526 | && nextFocused.requestFocus(View.FOCUS_DOWN); 527 | } 528 | return false; 529 | } 530 | 531 | boolean handled = false; 532 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 533 | switch (event.getKeyCode()) { 534 | case KeyEvent.KEYCODE_DPAD_UP: 535 | if (!event.isAltPressed()) { 536 | handled = arrowScroll(View.FOCUS_UP); 537 | } else { 538 | handled = fullScroll(View.FOCUS_UP); 539 | } 540 | break; 541 | case KeyEvent.KEYCODE_DPAD_DOWN: 542 | if (!event.isAltPressed()) { 543 | handled = arrowScroll(View.FOCUS_DOWN); 544 | } else { 545 | handled = fullScroll(View.FOCUS_DOWN); 546 | } 547 | break; 548 | case KeyEvent.KEYCODE_SPACE: 549 | pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); 550 | break; 551 | } 552 | } 553 | 554 | return handled; 555 | } 556 | 557 | private boolean inChild(int x, int y) { 558 | if (getChildCount() > 0) { 559 | final int scrollY = getScrollY(); 560 | final View child = getChildAt(0); 561 | return !(y < child.getTop() - scrollY 562 | || y >= child.getBottom() - scrollY 563 | || x < child.getLeft() 564 | || x >= child.getRight()); 565 | } 566 | return false; 567 | } 568 | 569 | private void initOrResetVelocityTracker() { 570 | if (mVelocityTracker == null) { 571 | mVelocityTracker = VelocityTracker.obtain(); 572 | } else { 573 | mVelocityTracker.clear(); 574 | } 575 | } 576 | 577 | private void initVelocityTrackerIfNotExists() { 578 | if (mVelocityTracker == null) { 579 | mVelocityTracker = VelocityTracker.obtain(); 580 | } 581 | } 582 | 583 | private void recycleVelocityTracker() { 584 | if (mVelocityTracker != null) { 585 | mVelocityTracker.recycle(); 586 | mVelocityTracker = null; 587 | } 588 | } 589 | 590 | @Override 591 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 592 | if (disallowIntercept) { 593 | recycleVelocityTracker(); 594 | } 595 | super.requestDisallowInterceptTouchEvent(disallowIntercept); 596 | } 597 | 598 | 599 | @Override 600 | public boolean onInterceptTouchEvent(MotionEvent ev) { 601 | /* 602 | * This method JUST determines whether we want to intercept the motion. 603 | * If we return true, onMotionEvent will be called and we do the actual 604 | * scrolling there. 605 | */ 606 | 607 | /* 608 | * Shortcut the most recurring case: the user is in the dragging 609 | * state and he is moving his finger. We want to intercept this 610 | * motion. 611 | */ 612 | final int action = ev.getAction(); 613 | if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 614 | return true; 615 | } 616 | 617 | switch (action & MotionEventCompat.ACTION_MASK) { 618 | case MotionEvent.ACTION_MOVE: { 619 | /* 620 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 621 | * whether the user has moved far enough from his original down touch. 622 | */ 623 | 624 | /* 625 | * Locally do absolute value. mLastMotionY is set to the y value 626 | * of the down event. 627 | */ 628 | final int activePointerId = mActivePointerId; 629 | if (activePointerId == INVALID_POINTER) { 630 | // If we don't have a valid id, the touch down wasn't on content. 631 | break; 632 | } 633 | 634 | final int pointerIndex = ev.findPointerIndex(activePointerId); 635 | if (pointerIndex == -1) { 636 | Log.e(TAG, "Invalid pointerId=" + activePointerId 637 | + " in onInterceptTouchEvent"); 638 | break; 639 | } 640 | 641 | final int y = (int) ev.getY(pointerIndex); 642 | final int yDiff = Math.abs(y - mLastMotionY); 643 | if (yDiff > mTouchSlop 644 | && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { 645 | mIsBeingDragged = true; 646 | mLastMotionY = y; 647 | initVelocityTrackerIfNotExists(); 648 | mVelocityTracker.addMovement(ev); 649 | mNestedYOffset = 0; 650 | final ViewParent parent = getParent(); 651 | if (parent != null) { 652 | parent.requestDisallowInterceptTouchEvent(true); 653 | } 654 | } 655 | break; 656 | } 657 | 658 | case MotionEvent.ACTION_DOWN: { 659 | final int y = (int) ev.getY(); 660 | if (!inChild((int) ev.getX(), (int) y)) { 661 | mIsBeingDragged = false; 662 | recycleVelocityTracker(); 663 | break; 664 | } 665 | 666 | /* 667 | * Remember location of down touch. 668 | * ACTION_DOWN always refers to pointer index 0. 669 | */ 670 | mLastMotionY = y; 671 | mActivePointerId = ev.getPointerId(0); 672 | 673 | initOrResetVelocityTracker(); 674 | mVelocityTracker.addMovement(ev); 675 | /* 676 | * If being flinged and user touches the screen, initiate drag; 677 | * otherwise don't. mScroller.isFinished should be false when 678 | * being flinged. We need to call computeScrollOffset() first so that 679 | * isFinished() is correct. 680 | */ 681 | mScroller.computeScrollOffset(); 682 | mIsBeingDragged = !mScroller.isFinished(); 683 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 684 | break; 685 | } 686 | 687 | case MotionEvent.ACTION_CANCEL: 688 | case MotionEvent.ACTION_UP: 689 | /* Release the drag */ 690 | mIsBeingDragged = false; 691 | mActivePointerId = INVALID_POINTER; 692 | recycleVelocityTracker(); 693 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { 694 | ViewCompat.postInvalidateOnAnimation(this); 695 | } 696 | stopNestedScroll(); 697 | break; 698 | case MotionEventCompat.ACTION_POINTER_UP: 699 | onSecondaryPointerUp(ev); 700 | break; 701 | } 702 | 703 | /* 704 | * The only time we want to intercept motion events is if we are in the 705 | * drag mode. 706 | */ 707 | return mIsBeingDragged; 708 | } 709 | 710 | @Override 711 | public boolean onTouchEvent(MotionEvent ev) { 712 | initVelocityTrackerIfNotExists(); 713 | 714 | isFling = false; 715 | MotionEvent vtev = MotionEvent.obtain(ev); 716 | 717 | final int actionMasked = MotionEventCompat.getActionMasked(ev); 718 | 719 | if (actionMasked == MotionEvent.ACTION_DOWN) { 720 | mNestedYOffset = 0; 721 | } 722 | vtev.offsetLocation(0, mNestedYOffset); 723 | 724 | switch (actionMasked) { 725 | case MotionEvent.ACTION_DOWN: { 726 | if (getChildCount() == 0) { 727 | return false; 728 | } 729 | if ((mIsBeingDragged = !mScroller.isFinished())) { 730 | final ViewParent parent = getParent(); 731 | if (parent != null) { 732 | parent.requestDisallowInterceptTouchEvent(true); 733 | } 734 | } 735 | 736 | /* 737 | * If being flinged and user touches, stop the fling. isFinished 738 | * will be false if being flinged. 739 | */ 740 | if (!mScroller.isFinished()) { 741 | mScroller.abortAnimation(); 742 | } 743 | 744 | // Remember where the motion event started 745 | mLastMotionY = (int) ev.getY(); 746 | mActivePointerId = ev.getPointerId(0); 747 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 748 | break; 749 | } 750 | case MotionEvent.ACTION_MOVE: 751 | final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 752 | if (activePointerIndex == -1) { 753 | Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 754 | break; 755 | } 756 | 757 | final int y = (int) ev.getY(activePointerIndex); 758 | int deltaY = mLastMotionY - y; 759 | if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { 760 | deltaY -= mScrollConsumed[1]; 761 | vtev.offsetLocation(0, mScrollOffset[1]); 762 | mNestedYOffset += mScrollOffset[1]; 763 | } 764 | if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 765 | final ViewParent parent = getParent(); 766 | if (parent != null) { 767 | parent.requestDisallowInterceptTouchEvent(true); 768 | } 769 | mIsBeingDragged = true; 770 | if (deltaY > 0) { 771 | deltaY -= mTouchSlop; 772 | } else { 773 | deltaY += mTouchSlop; 774 | } 775 | } 776 | if (mIsBeingDragged) { 777 | // Scroll to follow the motion event 778 | mLastMotionY = y - mScrollOffset[1]; 779 | 780 | final int oldY = getScrollY(); 781 | final int range = getScrollRange(); 782 | final int overscrollMode = getOverScrollMode(); 783 | boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS 784 | || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 785 | 786 | // Calling overScrollByCompat will call onOverScrolled, which 787 | // calls onScrollChanged if applicable. 788 | if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 789 | 0, true) && !hasNestedScrollingParent()) { 790 | // Break our velocity if we hit a scroll barrier. 791 | mVelocityTracker.clear(); 792 | } 793 | 794 | final int scrolledDeltaY = getScrollY() - oldY; 795 | final int unconsumedY = deltaY - scrolledDeltaY; 796 | if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { 797 | mLastMotionY -= mScrollOffset[1]; 798 | vtev.offsetLocation(0, mScrollOffset[1]); 799 | mNestedYOffset += mScrollOffset[1]; 800 | } else if (canOverscroll) { 801 | ensureGlows(); 802 | final int pulledToY = oldY + deltaY; 803 | //此处修改源码 804 | if (pulledToY < -getPulldownScroll()) { 805 | mEdgeGlowTop.onPull((float) deltaY / getHeight(), 806 | ev.getX(activePointerIndex) / getWidth()); 807 | if (!mEdgeGlowBottom.isFinished()) { 808 | mEdgeGlowBottom.onRelease(); 809 | } 810 | } else if (pulledToY > range) { 811 | mEdgeGlowBottom.onPull((float) deltaY / getHeight(), 812 | 1.f - ev.getX(activePointerIndex) 813 | / getWidth()); 814 | if (!mEdgeGlowTop.isFinished()) { 815 | mEdgeGlowTop.onRelease(); 816 | } 817 | } 818 | if (mEdgeGlowTop != null 819 | && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { 820 | ViewCompat.postInvalidateOnAnimation(this); 821 | } 822 | } 823 | } 824 | break; 825 | case MotionEvent.ACTION_UP: 826 | if (mIsBeingDragged) { 827 | final VelocityTracker velocityTracker = mVelocityTracker; 828 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 829 | int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, 830 | mActivePointerId); 831 | 832 | //此处修改源码 833 | if (getScrollY() < 0) {//越界时不考虑加速度 834 | Log.i("you", "it is scrollback..."); 835 | scrollBack(); 836 | } else { 837 | if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 838 | isFling = true; 839 | //Log.i("you", "it is fling..."); 840 | flingWithNestedDispatch(-initialVelocity); 841 | } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 842 | getScrollRange())) { 843 | ViewCompat.postInvalidateOnAnimation(this); 844 | } 845 | } 846 | } 847 | mActivePointerId = INVALID_POINTER; 848 | endDrag(); 849 | break; 850 | case MotionEvent.ACTION_CANCEL: 851 | if (mIsBeingDragged && getChildCount() > 0) { 852 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 853 | getScrollRange())) { 854 | ViewCompat.postInvalidateOnAnimation(this); 855 | } 856 | } 857 | mActivePointerId = INVALID_POINTER; 858 | endDrag(); 859 | break; 860 | case MotionEventCompat.ACTION_POINTER_DOWN: { 861 | final int index = MotionEventCompat.getActionIndex(ev); 862 | mLastMotionY = (int) ev.getY(index); 863 | mActivePointerId = ev.getPointerId(index); 864 | break; 865 | } 866 | case MotionEventCompat.ACTION_POINTER_UP: 867 | onSecondaryPointerUp(ev); 868 | mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 869 | break; 870 | } 871 | 872 | if (mVelocityTracker != null) { 873 | mVelocityTracker.addMovement(vtev); 874 | } 875 | vtev.recycle(); 876 | return true; 877 | } 878 | 879 | /** 880 | * 加速度滑动时,不可越界,手指拖动时可以 881 | */ 882 | private boolean isFling; 883 | 884 | /** 885 | * 允许下拉的高度 886 | * @return 887 | */ 888 | int getPulldownScroll() { 889 | return (getHeight() + getPaddingTop() + getPaddingBottom()) / PULLDOWN_SCALE; 890 | } 891 | 892 | /** 893 | * 滑动功能核心代码 894 | * @param deltaX 895 | * @param deltaY 896 | * @param scrollX 897 | * @param scrollY 898 | * @param scrollRangeX 899 | * @param scrollRangeY 900 | * @param maxOverScrollX 901 | * @param maxOverScrollY 902 | * @param isTouchEvent 903 | * @return 904 | */ 905 | boolean overScrollByCompat(int deltaX, int deltaY, 906 | int scrollX, int scrollY, 907 | int scrollRangeX, int scrollRangeY, 908 | int maxOverScrollX, int maxOverScrollY, 909 | boolean isTouchEvent) { 910 | final int overScrollMode = getOverScrollMode(); 911 | final boolean canScrollHorizontal = 912 | computeHorizontalScrollRange() > computeHorizontalScrollExtent(); 913 | final boolean canScrollVertical = 914 | computeVerticalScrollRange() > computeVerticalScrollExtent(); 915 | final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS 916 | || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); 917 | final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS 918 | || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); 919 | 920 | int newScrollX = scrollX + deltaX; 921 | if (!overScrollHorizontal) { 922 | maxOverScrollX = 0; 923 | } 924 | 925 | //此处修改原码 926 | if (scrollY < 0 && deltaY < 0) {//偏移往下滑动 927 | deltaY /= PULLDOWN_SCROLL; 928 | } 929 | 930 | int newScrollY = scrollY + deltaY; 931 | if (!overScrollVertical) { 932 | maxOverScrollY = 0; 933 | } 934 | 935 | // Clamp values if at the limits and record 936 | final int left = -maxOverScrollX; 937 | final int right = maxOverScrollX + scrollRangeX; 938 | //此处修改原码 939 | int top; 940 | if (isFling) { 941 | top = - maxOverScrollY; 942 | } else { 943 | top = -getPulldownScroll(); 944 | } 945 | 946 | final int bottom = maxOverScrollY + scrollRangeY; 947 | 948 | boolean clampedX = false; 949 | if (newScrollX > right) { 950 | newScrollX = right; 951 | clampedX = true; 952 | } else if (newScrollX < left) { 953 | newScrollX = left; 954 | clampedX = true; 955 | } 956 | 957 | boolean clampedY = false; 958 | if (newScrollY > bottom) {//滑动越出界限 959 | newScrollY = bottom; 960 | clampedY = true; 961 | } else if (newScrollY < top) { 962 | newScrollY = top; 963 | clampedY = true; 964 | } 965 | 966 | if (clampedY && isFling) {//这里isFling时才回弹 967 | //Log.i("you", top + " " + bottom + " "+deltaY+" "+newScrollY+" "+getScrollRange()); 968 | mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); 969 | } 970 | onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); 971 | return clampedX || clampedY; 972 | } 973 | 974 | /** 975 | * 回弹 976 | */ 977 | private boolean scrollBack() { 978 | int dy = getScrollY(); 979 | if (getScrollY() < 0) { 980 | mScroller.startScroll(0, dy, 0, -dy, computeScrollDuration(dy, 0)); 981 | invalidate(); 982 | return true; 983 | } 984 | return false; 985 | } 986 | 987 | private int computeScrollDuration(int dy, float velocityY) { 988 | final int height = (getHeight() + getPaddingTop() + getPaddingBottom()) / PULLDOWN_SCALE; 989 | final int halfHeight = height / 2; 990 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height); 991 | final float distance = halfHeight + halfHeight * distanceInfluenceForSnapDuration(distanceRatio); 992 | velocityY = Math.abs(velocityY); 993 | int duration; 994 | if (velocityY > 0) { 995 | duration = 4 * Math.round(1000 * Math.abs(distance / velocityY)); 996 | } else { 997 | final float pageDelta = (float) Math.abs(dy) / height; 998 | duration = (int) ((pageDelta + 1) * 100); 999 | } 1000 | duration = Math.min(duration, ANIMATED_SCROLL_GAP); 1001 | return duration; 1002 | } 1003 | 1004 | private float distanceInfluenceForSnapDuration(float f) { 1005 | f -= 0.5f; // center the values about 0. 1006 | f *= 0.3f * Math.PI / 2.0f; 1007 | return (float) Math.sin(f); 1008 | } 1009 | 1010 | private void onSecondaryPointerUp(MotionEvent ev) { 1011 | final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) 1012 | >> MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; 1013 | final int pointerId = ev.getPointerId(pointerIndex); 1014 | if (pointerId == mActivePointerId) { 1015 | // This was our active pointer going up. Choose a new 1016 | // active pointer and adjust accordingly. 1017 | // TODO: Make this decision more intelligent. 1018 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1019 | mLastMotionY = (int) ev.getY(newPointerIndex); 1020 | mActivePointerId = ev.getPointerId(newPointerIndex); 1021 | if (mVelocityTracker != null) { 1022 | mVelocityTracker.clear(); 1023 | } 1024 | } 1025 | } 1026 | 1027 | public boolean onGenericMotionEvent(MotionEvent event) { 1028 | if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { 1029 | switch (event.getAction()) { 1030 | case MotionEventCompat.ACTION_SCROLL: { 1031 | if (!mIsBeingDragged) { 1032 | final float vscroll = MotionEventCompat.getAxisValue(event, 1033 | MotionEventCompat.AXIS_VSCROLL); 1034 | if (vscroll != 0) { 1035 | final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); 1036 | final int range = getScrollRange(); 1037 | int oldScrollY = getScrollY(); 1038 | int newScrollY = oldScrollY - delta; 1039 | if (newScrollY < 0) { 1040 | newScrollY = 0; 1041 | } else if (newScrollY > range) { 1042 | newScrollY = range; 1043 | } 1044 | if (newScrollY != oldScrollY) { 1045 | super.scrollTo(getScrollX(), newScrollY); 1046 | return true; 1047 | } 1048 | } 1049 | } 1050 | } 1051 | } 1052 | } 1053 | return false; 1054 | } 1055 | 1056 | private float getVerticalScrollFactorCompat() { 1057 | if (mVerticalScrollFactor == 0) { 1058 | TypedValue outValue = new TypedValue(); 1059 | final Context context = getContext(); 1060 | if (!context.getTheme().resolveAttribute( 1061 | android.R.attr.listPreferredItemHeight, outValue, true)) { 1062 | throw new IllegalStateException( 1063 | "Expected theme to define listPreferredItemHeight."); 1064 | } 1065 | mVerticalScrollFactor = outValue.getDimension( 1066 | context.getResources().getDisplayMetrics()); 1067 | } 1068 | return mVerticalScrollFactor; 1069 | } 1070 | 1071 | @Override 1072 | protected void onOverScrolled(int scrollX, int scrollY, 1073 | boolean clampedX, boolean clampedY) { 1074 | super.scrollTo(scrollX, scrollY); 1075 | } 1076 | 1077 | int getScrollRange() { 1078 | int scrollRange = 0; 1079 | if (getChildCount() > 0) { 1080 | View child = getChildAt(0); 1081 | scrollRange = Math.max(0, 1082 | child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); 1083 | } 1084 | return scrollRange; 1085 | } 1086 | 1087 | /** 1088 | *

1089 | * Finds the next focusable component that fits in the specified bounds. 1090 | *

1091 | * 1092 | * @param topFocus look for a candidate is the one at the top of the bounds 1093 | * if topFocus is true, or at the bottom of the bounds if topFocus is 1094 | * false 1095 | * @param top the top offset of the bounds in which a focusable must be 1096 | * found 1097 | * @param bottom the bottom offset of the bounds in which a focusable must 1098 | * be found 1099 | * @return the next focusable component in the bounds or null if none can 1100 | * be found 1101 | */ 1102 | private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { 1103 | 1104 | List focusables = getFocusables(View.FOCUS_FORWARD); 1105 | View focusCandidate = null; 1106 | 1107 | /* 1108 | * A fully contained focusable is one where its top is below the bound's 1109 | * top, and its bottom is above the bound's bottom. A partially 1110 | * contained focusable is one where some part of it is within the 1111 | * bounds, but it also has some part that is not within bounds. A fully contained 1112 | * focusable is preferred to a partially contained focusable. 1113 | */ 1114 | boolean foundFullyContainedFocusable = false; 1115 | 1116 | int count = focusables.size(); 1117 | for (int i = 0; i < count; i++) { 1118 | View view = focusables.get(i); 1119 | int viewTop = view.getTop(); 1120 | int viewBottom = view.getBottom(); 1121 | 1122 | if (top < viewBottom && viewTop < bottom) { 1123 | /* 1124 | * the focusable is in the target area, it is a candidate for 1125 | * focusing 1126 | */ 1127 | 1128 | final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); 1129 | 1130 | if (focusCandidate == null) { 1131 | /* No candidate, take this one */ 1132 | focusCandidate = view; 1133 | foundFullyContainedFocusable = viewIsFullyContained; 1134 | } else { 1135 | final boolean viewIsCloserToBoundary = 1136 | (topFocus && viewTop < focusCandidate.getTop()) 1137 | || (!topFocus && viewBottom > focusCandidate.getBottom()); 1138 | 1139 | if (foundFullyContainedFocusable) { 1140 | if (viewIsFullyContained && viewIsCloserToBoundary) { 1141 | /* 1142 | * We're dealing with only fully contained views, so 1143 | * it has to be closer to the boundary to beat our 1144 | * candidate 1145 | */ 1146 | focusCandidate = view; 1147 | } 1148 | } else { 1149 | if (viewIsFullyContained) { 1150 | /* Any fully contained view beats a partially contained view */ 1151 | focusCandidate = view; 1152 | foundFullyContainedFocusable = true; 1153 | } else if (viewIsCloserToBoundary) { 1154 | /* 1155 | * Partially contained view beats another partially 1156 | * contained view if it's closer 1157 | */ 1158 | focusCandidate = view; 1159 | } 1160 | } 1161 | } 1162 | } 1163 | } 1164 | 1165 | return focusCandidate; 1166 | } 1167 | 1168 | /** 1169 | *

Handles scrolling in response to a "page up/down" shortcut press. This 1170 | * method will scroll the view by one page up or down and give the focus 1171 | * to the topmost/bottommost component in the new visible area. If no 1172 | * component is a good candidate for focus, this scrollview reclaims the 1173 | * focus.

1174 | * 1175 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1176 | * to go one page up or 1177 | * {@link android.view.View#FOCUS_DOWN} to go one page down 1178 | * @return true if the key event is consumed by this method, false otherwise 1179 | */ 1180 | public boolean pageScroll(int direction) { 1181 | boolean down = direction == View.FOCUS_DOWN; 1182 | int height = getHeight(); 1183 | 1184 | if (down) { 1185 | mTempRect.top = getScrollY() + height; 1186 | int count = getChildCount(); 1187 | if (count > 0) { 1188 | View view = getChildAt(count - 1); 1189 | if (mTempRect.top + height > view.getBottom()) { 1190 | mTempRect.top = view.getBottom() - height; 1191 | } 1192 | } 1193 | } else { 1194 | mTempRect.top = getScrollY() - height; 1195 | if (mTempRect.top < 0) { 1196 | mTempRect.top = 0; 1197 | } 1198 | } 1199 | mTempRect.bottom = mTempRect.top + height; 1200 | 1201 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1202 | } 1203 | 1204 | /** 1205 | *

Handles scrolling in response to a "home/end" shortcut press. This 1206 | * method will scroll the view to the top or bottom and give the focus 1207 | * to the topmost/bottommost component in the new visible area. If no 1208 | * component is a good candidate for focus, this scrollview reclaims the 1209 | * focus.

1210 | * 1211 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1212 | * to go the top of the view or 1213 | * {@link android.view.View#FOCUS_DOWN} to go the bottom 1214 | * @return true if the key event is consumed by this method, false otherwise 1215 | */ 1216 | public boolean fullScroll(int direction) { 1217 | boolean down = direction == View.FOCUS_DOWN; 1218 | int height = getHeight(); 1219 | 1220 | mTempRect.top = 0; 1221 | mTempRect.bottom = height; 1222 | 1223 | if (down) { 1224 | int count = getChildCount(); 1225 | if (count > 0) { 1226 | View view = getChildAt(count - 1); 1227 | mTempRect.bottom = view.getBottom() + getPaddingBottom(); 1228 | mTempRect.top = mTempRect.bottom - height; 1229 | } 1230 | } 1231 | 1232 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1233 | } 1234 | 1235 | /** 1236 | *

Scrolls the view to make the area defined by top and 1237 | * bottom visible. This method attempts to give the focus 1238 | * to a component visible in this area. If no component can be focused in 1239 | * the new visible area, the focus is reclaimed by this ScrollView.

1240 | * 1241 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1242 | * to go upward, {@link android.view.View#FOCUS_DOWN} to downward 1243 | * @param top the top offset of the new area to be made visible 1244 | * @param bottom the bottom offset of the new area to be made visible 1245 | * @return true if the key event is consumed by this method, false otherwise 1246 | */ 1247 | private boolean scrollAndFocus(int direction, int top, int bottom) { 1248 | boolean handled = true; 1249 | 1250 | int height = getHeight(); 1251 | int containerTop = getScrollY(); 1252 | int containerBottom = containerTop + height; 1253 | boolean up = direction == View.FOCUS_UP; 1254 | 1255 | View newFocused = findFocusableViewInBounds(up, top, bottom); 1256 | if (newFocused == null) { 1257 | newFocused = this; 1258 | } 1259 | 1260 | if (top >= containerTop && bottom <= containerBottom) { 1261 | handled = false; 1262 | } else { 1263 | int delta = up ? (top - containerTop) : (bottom - containerBottom); 1264 | doScrollY(delta); 1265 | } 1266 | 1267 | if (newFocused != findFocus()) newFocused.requestFocus(direction); 1268 | 1269 | return handled; 1270 | } 1271 | 1272 | /** 1273 | * Handle scrolling in response to an up or down arrow click. 1274 | * 1275 | * @param direction The direction corresponding to the arrow key that was 1276 | * pressed 1277 | * @return True if we consumed the event, false otherwise 1278 | */ 1279 | public boolean arrowScroll(int direction) { 1280 | 1281 | View currentFocused = findFocus(); 1282 | if (currentFocused == this) currentFocused = null; 1283 | 1284 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); 1285 | 1286 | final int maxJump = getMaxScrollAmount(); 1287 | 1288 | if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { 1289 | nextFocused.getDrawingRect(mTempRect); 1290 | offsetDescendantRectToMyCoords(nextFocused, mTempRect); 1291 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1292 | doScrollY(scrollDelta); 1293 | nextFocused.requestFocus(direction); 1294 | } else { 1295 | // no new focus 1296 | int scrollDelta = maxJump; 1297 | 1298 | if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { 1299 | scrollDelta = getScrollY(); 1300 | } else if (direction == View.FOCUS_DOWN) { 1301 | if (getChildCount() > 0) { 1302 | int daBottom = getChildAt(0).getBottom(); 1303 | int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); 1304 | if (daBottom - screenBottom < maxJump) { 1305 | scrollDelta = daBottom - screenBottom; 1306 | } 1307 | } 1308 | } 1309 | if (scrollDelta == 0) { 1310 | return false; 1311 | } 1312 | doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); 1313 | } 1314 | 1315 | if (currentFocused != null && currentFocused.isFocused() 1316 | && isOffScreen(currentFocused)) { 1317 | // previously focused item still has focus and is off screen, give 1318 | // it up (take it back to ourselves) 1319 | // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are 1320 | // sure to 1321 | // get it) 1322 | final int descendantFocusability = getDescendantFocusability(); // save 1323 | setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 1324 | requestFocus(); 1325 | setDescendantFocusability(descendantFocusability); // restore 1326 | } 1327 | return true; 1328 | } 1329 | 1330 | /** 1331 | * @return whether the descendant of this scroll view is scrolled off 1332 | * screen. 1333 | */ 1334 | private boolean isOffScreen(View descendant) { 1335 | return !isWithinDeltaOfScreen(descendant, 0, getHeight()); 1336 | } 1337 | 1338 | /** 1339 | * @return whether the descendant of this scroll view is within delta 1340 | * pixels of being on the screen. 1341 | */ 1342 | private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { 1343 | descendant.getDrawingRect(mTempRect); 1344 | offsetDescendantRectToMyCoords(descendant, mTempRect); 1345 | 1346 | return (mTempRect.bottom + delta) >= getScrollY() 1347 | && (mTempRect.top - delta) <= (getScrollY() + height); 1348 | } 1349 | 1350 | /** 1351 | * Smooth scroll by a Y delta 1352 | * 1353 | * @param delta the number of pixels to scroll by on the Y axis 1354 | */ 1355 | private void doScrollY(int delta) { 1356 | if (delta != 0) { 1357 | if (mSmoothScrollingEnabled) { 1358 | smoothScrollBy(0, delta); 1359 | } else { 1360 | scrollBy(0, delta); 1361 | } 1362 | } 1363 | } 1364 | 1365 | /** 1366 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 1367 | * 1368 | * @param dx the number of pixels to scroll by on the X axis 1369 | * @param dy the number of pixels to scroll by on the Y axis 1370 | */ 1371 | public final void smoothScrollBy(int dx, int dy) { 1372 | if (getChildCount() == 0) { 1373 | // Nothing to do. 1374 | return; 1375 | } 1376 | long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; 1377 | if (duration > ANIMATED_SCROLL_GAP) { 1378 | final int height = getHeight() - getPaddingBottom() - getPaddingTop(); 1379 | final int bottom = getChildAt(0).getHeight(); 1380 | final int maxY = Math.max(0, bottom - height); 1381 | final int scrollY = getScrollY(); 1382 | dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; 1383 | 1384 | mScroller.startScroll(getScrollX(), scrollY, 0, dy); 1385 | ViewCompat.postInvalidateOnAnimation(this); 1386 | } else { 1387 | if (!mScroller.isFinished()) { 1388 | mScroller.abortAnimation(); 1389 | } 1390 | scrollBy(dx, dy); 1391 | } 1392 | mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 1393 | } 1394 | 1395 | /** 1396 | * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1397 | * 1398 | * @param x the position where to scroll on the X axis 1399 | * @param y the position where to scroll on the Y axis 1400 | */ 1401 | public final void smoothScrollTo(int x, int y) { 1402 | smoothScrollBy(x - getScrollX(), y - getScrollY()); 1403 | } 1404 | 1405 | /** 1406 | *

The scroll range of a scroll view is the overall height of all of its 1407 | * children.

1408 | * @hide 1409 | */ 1410 | @Override 1411 | public int computeVerticalScrollRange() { 1412 | final int count = getChildCount(); 1413 | final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); 1414 | if (count == 0) { 1415 | return contentHeight; 1416 | } 1417 | 1418 | int scrollRange = getChildAt(0).getBottom(); 1419 | final int scrollY = getScrollY(); 1420 | final int overscrollBottom = Math.max(0, scrollRange - contentHeight); 1421 | if (scrollY < 0) { 1422 | scrollRange -= scrollY; 1423 | } else if (scrollY > overscrollBottom) { 1424 | scrollRange += scrollY - overscrollBottom; 1425 | } 1426 | 1427 | return scrollRange; 1428 | } 1429 | 1430 | /** @hide */ 1431 | @Override 1432 | public int computeVerticalScrollOffset() { 1433 | return Math.max(0, super.computeVerticalScrollOffset()); 1434 | } 1435 | 1436 | /** @hide */ 1437 | @Override 1438 | public int computeVerticalScrollExtent() { 1439 | return super.computeVerticalScrollExtent(); 1440 | } 1441 | 1442 | /** @hide */ 1443 | @Override 1444 | public int computeHorizontalScrollRange() { 1445 | return super.computeHorizontalScrollRange(); 1446 | } 1447 | 1448 | /** @hide */ 1449 | @Override 1450 | public int computeHorizontalScrollOffset() { 1451 | return super.computeHorizontalScrollOffset(); 1452 | } 1453 | 1454 | /** @hide */ 1455 | @Override 1456 | public int computeHorizontalScrollExtent() { 1457 | return super.computeHorizontalScrollExtent(); 1458 | } 1459 | 1460 | @Override 1461 | protected void measureChild(View child, int parentWidthMeasureSpec, 1462 | int parentHeightMeasureSpec) { 1463 | ViewGroup.LayoutParams lp = child.getLayoutParams(); 1464 | 1465 | int childWidthMeasureSpec; 1466 | int childHeightMeasureSpec; 1467 | 1468 | childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() 1469 | + getPaddingRight(), lp.width); 1470 | 1471 | childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1472 | 1473 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1474 | } 1475 | 1476 | @Override 1477 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 1478 | int parentHeightMeasureSpec, int heightUsed) { 1479 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 1480 | 1481 | final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 1482 | getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin 1483 | + widthUsed, lp.width); 1484 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 1485 | lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); 1486 | 1487 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1488 | } 1489 | 1490 | @Override 1491 | public void computeScroll() { 1492 | if (mScroller.computeScrollOffset()) { 1493 | int oldX = getScrollX(); 1494 | int oldY = getScrollY(); 1495 | int x = mScroller.getCurrX(); 1496 | int y = mScroller.getCurrY(); 1497 | if (oldX != x || oldY != y) { 1498 | final int range = getScrollRange(); 1499 | final int overscrollMode = getOverScrollMode(); 1500 | final boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS 1501 | || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 1502 | 1503 | overScrollByCompat(x - oldX, y - oldY, oldX, oldY, 0, range, 1504 | 0, 0, false); 1505 | 1506 | if (canOverscroll) { 1507 | ensureGlows(); 1508 | if (y <= 0 && oldY > 0) { 1509 | mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 1510 | } else if (y >= range && oldY < range ) { 1511 | mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 1512 | } 1513 | } 1514 | } else if (!isFling){ 1515 | //回弹过程中有可能会相同,也需要刷新 1516 | //Log.i("you", "postInvalidate..."); 1517 | postInvalidate(); 1518 | } 1519 | } 1520 | } 1521 | 1522 | /** 1523 | * Scrolls the view to the given child. 1524 | * 1525 | * @param child the View to scroll to 1526 | */ 1527 | private void scrollToChild(View child) { 1528 | child.getDrawingRect(mTempRect); 1529 | 1530 | /* Offset from child's local coordinates to ScrollView coordinates */ 1531 | offsetDescendantRectToMyCoords(child, mTempRect); 1532 | 1533 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1534 | 1535 | if (scrollDelta != 0) { 1536 | scrollBy(0, scrollDelta); 1537 | } 1538 | } 1539 | 1540 | /** 1541 | * If rect is off screen, scroll just enough to get it (or at least the 1542 | * first screen size chunk of it) on screen. 1543 | * 1544 | * @param rect The rectangle. 1545 | * @param immediate True to scroll immediately without animation 1546 | * @return true if scrolling was performed 1547 | */ 1548 | private boolean scrollToChildRect(Rect rect, boolean immediate) { 1549 | final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 1550 | final boolean scroll = delta != 0; 1551 | if (scroll) { 1552 | if (immediate) { 1553 | scrollBy(0, delta); 1554 | } else { 1555 | smoothScrollBy(0, delta); 1556 | } 1557 | } 1558 | return scroll; 1559 | } 1560 | 1561 | /** 1562 | * Compute the amount to scroll in the Y direction in order to get 1563 | * a rectangle completely on the screen (or, if taller than the screen, 1564 | * at least the first screen size chunk of it). 1565 | * 1566 | * @param rect The rect. 1567 | * @return The scroll delta. 1568 | */ 1569 | protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 1570 | if (getChildCount() == 0) return 0; 1571 | 1572 | int height = getHeight(); 1573 | int screenTop = getScrollY(); 1574 | int screenBottom = screenTop + height; 1575 | 1576 | int fadingEdge = getVerticalFadingEdgeLength(); 1577 | 1578 | // leave room for top fading edge as long as rect isn't at very top 1579 | if (rect.top > 0) { 1580 | screenTop += fadingEdge; 1581 | } 1582 | 1583 | // leave room for bottom fading edge as long as rect isn't at very bottom 1584 | if (rect.bottom < getChildAt(0).getHeight()) { 1585 | screenBottom -= fadingEdge; 1586 | } 1587 | 1588 | int scrollYDelta = 0; 1589 | 1590 | if (rect.bottom > screenBottom && rect.top > screenTop) { 1591 | // need to move down to get it in view: move down just enough so 1592 | // that the entire rectangle is in view (or at least the first 1593 | // screen size chunk). 1594 | 1595 | if (rect.height() > height) { 1596 | // just enough to get screen size chunk on 1597 | scrollYDelta += (rect.top - screenTop); 1598 | } else { 1599 | // get entire rect at bottom of screen 1600 | scrollYDelta += (rect.bottom - screenBottom); 1601 | } 1602 | 1603 | // make sure we aren't scrolling beyond the end of our content 1604 | int bottom = getChildAt(0).getBottom(); 1605 | int distanceToBottom = bottom - screenBottom; 1606 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom); 1607 | 1608 | } else if (rect.top < screenTop && rect.bottom < screenBottom) { 1609 | // need to move up to get it in view: move up just enough so that 1610 | // entire rectangle is in view (or at least the first screen 1611 | // size chunk of it). 1612 | 1613 | if (rect.height() > height) { 1614 | // screen size chunk 1615 | scrollYDelta -= (screenBottom - rect.bottom); 1616 | } else { 1617 | // entire rect at top 1618 | scrollYDelta -= (screenTop - rect.top); 1619 | } 1620 | 1621 | // make sure we aren't scrolling any further than the top our content 1622 | scrollYDelta = Math.max(scrollYDelta, -getScrollY()); 1623 | } 1624 | return scrollYDelta; 1625 | } 1626 | 1627 | @Override 1628 | public void requestChildFocus(View child, View focused) { 1629 | if (!mIsLayoutDirty) { 1630 | scrollToChild(focused); 1631 | } else { 1632 | // The child may not be laid out yet, we can't compute the scroll yet 1633 | mChildToScrollTo = focused; 1634 | } 1635 | super.requestChildFocus(child, focused); 1636 | } 1637 | 1638 | 1639 | /** 1640 | * When looking for focus in children of a scroll view, need to be a little 1641 | * more careful not to give focus to something that is scrolled off screen. 1642 | * 1643 | * This is more expensive than the default {@link android.view.ViewGroup} 1644 | * implementation, otherwise this behavior might have been made the default. 1645 | */ 1646 | @Override 1647 | protected boolean onRequestFocusInDescendants(int direction, 1648 | Rect previouslyFocusedRect) { 1649 | 1650 | // convert from forward / backward notation to up / down / left / right 1651 | // (ugh). 1652 | if (direction == View.FOCUS_FORWARD) { 1653 | direction = View.FOCUS_DOWN; 1654 | } else if (direction == View.FOCUS_BACKWARD) { 1655 | direction = View.FOCUS_UP; 1656 | } 1657 | 1658 | final View nextFocus = previouslyFocusedRect == null 1659 | ? FocusFinder.getInstance().findNextFocus(this, null, direction) 1660 | : FocusFinder.getInstance().findNextFocusFromRect( 1661 | this, previouslyFocusedRect, direction); 1662 | 1663 | if (nextFocus == null) { 1664 | return false; 1665 | } 1666 | 1667 | if (isOffScreen(nextFocus)) { 1668 | return false; 1669 | } 1670 | 1671 | return nextFocus.requestFocus(direction, previouslyFocusedRect); 1672 | } 1673 | 1674 | @Override 1675 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle, 1676 | boolean immediate) { 1677 | // offset into coordinate space of this scroll view 1678 | rectangle.offset(child.getLeft() - child.getScrollX(), 1679 | child.getTop() - child.getScrollY()); 1680 | 1681 | return scrollToChildRect(rectangle, immediate); 1682 | } 1683 | 1684 | @Override 1685 | public void requestLayout() { 1686 | mIsLayoutDirty = true; 1687 | super.requestLayout(); 1688 | } 1689 | 1690 | @Override 1691 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1692 | super.onLayout(changed, l, t, r, b); 1693 | mIsLayoutDirty = false; 1694 | // Give a child focus if it needs it 1695 | if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { 1696 | scrollToChild(mChildToScrollTo); 1697 | } 1698 | mChildToScrollTo = null; 1699 | 1700 | if (!mIsLaidOut) { 1701 | if (mSavedState != null) { 1702 | scrollTo(getScrollX(), mSavedState.scrollPosition); 1703 | mSavedState = null; 1704 | } // mScrollY default value is "0" 1705 | 1706 | final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; 1707 | final int scrollRange = Math.max(0, 1708 | childHeight - (b - t - getPaddingBottom() - getPaddingTop())); 1709 | 1710 | // Don't forget to clamp 1711 | if (getScrollY() > scrollRange) { 1712 | scrollTo(getScrollX(), scrollRange); 1713 | } else if (getScrollY() < 0) { 1714 | scrollTo(getScrollX(), 0); 1715 | } 1716 | } 1717 | 1718 | // Calling this with the present values causes it to re-claim them 1719 | scrollTo(getScrollX(), getScrollY()); 1720 | mIsLaidOut = true; 1721 | } 1722 | 1723 | @Override 1724 | public void onAttachedToWindow() { 1725 | super.onAttachedToWindow(); 1726 | 1727 | mIsLaidOut = false; 1728 | } 1729 | 1730 | @Override 1731 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1732 | super.onSizeChanged(w, h, oldw, oldh); 1733 | 1734 | View currentFocused = findFocus(); 1735 | if (null == currentFocused || this == currentFocused) { 1736 | return; 1737 | } 1738 | 1739 | // If the currently-focused view was visible on the screen when the 1740 | // screen was at the old height, then scroll the screen to make that 1741 | // view visible with the new screen height. 1742 | if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { 1743 | currentFocused.getDrawingRect(mTempRect); 1744 | offsetDescendantRectToMyCoords(currentFocused, mTempRect); 1745 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1746 | doScrollY(scrollDelta); 1747 | } 1748 | } 1749 | 1750 | /** 1751 | * Return true if child is a descendant of parent, (or equal to the parent). 1752 | */ 1753 | private static boolean isViewDescendantOf(View child, View parent) { 1754 | if (child == parent) { 1755 | return true; 1756 | } 1757 | 1758 | final ViewParent theParent = child.getParent(); 1759 | return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); 1760 | } 1761 | 1762 | /** 1763 | * Fling the scroll view 1764 | * 1765 | * @param velocityY The initial velocity in the Y direction. Positive 1766 | * numbers mean that the finger/cursor is moving down the screen, 1767 | * which means we want to scroll towards the top. 1768 | */ 1769 | public void fling(int velocityY) { 1770 | if (getChildCount() > 0) { 1771 | int height = getHeight() - getPaddingBottom() - getPaddingTop(); 1772 | int bottom = getChildAt(0).getHeight(); 1773 | mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, 1774 | Math.max(0, bottom - height), 0, height / 2); 1775 | 1776 | ViewCompat.postInvalidateOnAnimation(this); 1777 | } 1778 | } 1779 | 1780 | private void flingWithNestedDispatch(int velocityY) { 1781 | final int scrollY = getScrollY(); 1782 | final boolean canFling = (scrollY > 0 || velocityY > 0) 1783 | && (scrollY < getScrollRange() || velocityY < 0); 1784 | if (!dispatchNestedPreFling(0, velocityY)) { 1785 | dispatchNestedFling(0, velocityY, canFling); 1786 | if (canFling) { 1787 | fling(velocityY); 1788 | } 1789 | } 1790 | } 1791 | 1792 | private void endDrag() { 1793 | mIsBeingDragged = false; 1794 | 1795 | recycleVelocityTracker(); 1796 | stopNestedScroll(); 1797 | 1798 | if (mEdgeGlowTop != null) { 1799 | mEdgeGlowTop.onRelease(); 1800 | mEdgeGlowBottom.onRelease(); 1801 | } 1802 | } 1803 | 1804 | /** 1805 | * {@inheritDoc} 1806 | * 1807 | *

This version also clamps the scrolling to the bounds of our child. 1808 | */ 1809 | @Override 1810 | public void scrollTo(int x, int y) { 1811 | // we rely on the fact the View.scrollBy calls scrollTo. 1812 | if (getChildCount() > 0) { 1813 | View child = getChildAt(0); 1814 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); 1815 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); 1816 | if (x != getScrollX() || y != getScrollY()) { 1817 | super.scrollTo(x, y); 1818 | } 1819 | } 1820 | } 1821 | 1822 | private void ensureGlows() { 1823 | if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { 1824 | if (mEdgeGlowTop == null) { 1825 | Context context = getContext(); 1826 | mEdgeGlowTop = new EdgeEffectCompat(context); 1827 | mEdgeGlowBottom = new EdgeEffectCompat(context); 1828 | } 1829 | } else { 1830 | mEdgeGlowTop = null; 1831 | mEdgeGlowBottom = null; 1832 | } 1833 | } 1834 | 1835 | @Override 1836 | public void draw(Canvas canvas) { 1837 | super.draw(canvas); 1838 | if (mEdgeGlowTop != null) { 1839 | final int scrollY = getScrollY(); 1840 | if (!mEdgeGlowTop.isFinished()) { 1841 | final int restoreCount = canvas.save(); 1842 | final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 1843 | 1844 | canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); 1845 | mEdgeGlowTop.setSize(width, getHeight()); 1846 | if (mEdgeGlowTop.draw(canvas)) { 1847 | ViewCompat.postInvalidateOnAnimation(this); 1848 | } 1849 | canvas.restoreToCount(restoreCount); 1850 | } 1851 | if (!mEdgeGlowBottom.isFinished()) { 1852 | final int restoreCount = canvas.save(); 1853 | final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 1854 | final int height = getHeight(); 1855 | 1856 | canvas.translate(-width + getPaddingLeft(), 1857 | Math.max(getScrollRange(), scrollY) + height); 1858 | canvas.rotate(180, width, 0); 1859 | mEdgeGlowBottom.setSize(width, height); 1860 | if (mEdgeGlowBottom.draw(canvas)) { 1861 | ViewCompat.postInvalidateOnAnimation(this); 1862 | } 1863 | canvas.restoreToCount(restoreCount); 1864 | } 1865 | } 1866 | } 1867 | 1868 | private static int clamp(int n, int my, int child) { 1869 | if (my >= child || n < 0) { 1870 | /* my >= child is this case: 1871 | * |--------------- me ---------------| 1872 | * |------ child ------| 1873 | * or 1874 | * |--------------- me ---------------| 1875 | * |------ child ------| 1876 | * or 1877 | * |--------------- me ---------------| 1878 | * |------ child ------| 1879 | * 1880 | * n < 0 is this case: 1881 | * |------ me ------| 1882 | * |-------- child --------| 1883 | * |-- mScrollX --| 1884 | */ 1885 | return 0; 1886 | } 1887 | if ((my + n) > child) { 1888 | /* this case: 1889 | * |------ me ------| 1890 | * |------ child ------| 1891 | * |-- mScrollX --| 1892 | */ 1893 | return child - my; 1894 | } 1895 | return n; 1896 | } 1897 | 1898 | @Override 1899 | protected void onRestoreInstanceState(Parcelable state) { 1900 | if (!(state instanceof SavedState)) { 1901 | super.onRestoreInstanceState(state); 1902 | return; 1903 | } 1904 | 1905 | SavedState ss = (SavedState) state; 1906 | super.onRestoreInstanceState(ss.getSuperState()); 1907 | mSavedState = ss; 1908 | requestLayout(); 1909 | } 1910 | 1911 | @Override 1912 | protected Parcelable onSaveInstanceState() { 1913 | Parcelable superState = super.onSaveInstanceState(); 1914 | SavedState ss = new SavedState(superState); 1915 | ss.scrollPosition = getScrollY(); 1916 | return ss; 1917 | } 1918 | 1919 | static class SavedState extends BaseSavedState { 1920 | public int scrollPosition; 1921 | 1922 | SavedState(Parcelable superState) { 1923 | super(superState); 1924 | } 1925 | 1926 | SavedState(Parcel source) { 1927 | super(source); 1928 | scrollPosition = source.readInt(); 1929 | } 1930 | 1931 | @Override 1932 | public void writeToParcel(Parcel dest, int flags) { 1933 | super.writeToParcel(dest, flags); 1934 | dest.writeInt(scrollPosition); 1935 | } 1936 | 1937 | @Override 1938 | public String toString() { 1939 | return "HorizontalScrollView.SavedState{" 1940 | + Integer.toHexString(System.identityHashCode(this)) 1941 | + " scrollPosition=" + scrollPosition + "}"; 1942 | } 1943 | 1944 | public static final Parcelable.Creator CREATOR = 1945 | new Parcelable.Creator() { 1946 | @Override 1947 | public SavedState createFromParcel(Parcel in) { 1948 | return new SavedState(in); 1949 | } 1950 | 1951 | @Override 1952 | public SavedState[] newArray(int size) { 1953 | return new SavedState[size]; 1954 | } 1955 | }; 1956 | } 1957 | 1958 | static class AccessibilityDelegate extends AccessibilityDelegateCompat { 1959 | @Override 1960 | public boolean performAccessibilityAction(View host, int action, Bundle arguments) { 1961 | if (super.performAccessibilityAction(host, action, arguments)) { 1962 | return true; 1963 | } 1964 | final NestedScrollView nsvHost = (NestedScrollView) host; 1965 | if (!nsvHost.isEnabled()) { 1966 | return false; 1967 | } 1968 | switch (action) { 1969 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { 1970 | final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() 1971 | - nsvHost.getPaddingTop(); 1972 | final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, 1973 | nsvHost.getScrollRange()); 1974 | if (targetScrollY != nsvHost.getScrollY()) { 1975 | nsvHost.smoothScrollTo(0, targetScrollY); 1976 | return true; 1977 | } 1978 | } 1979 | return false; 1980 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { 1981 | final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() 1982 | - nsvHost.getPaddingTop(); 1983 | final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); 1984 | if (targetScrollY != nsvHost.getScrollY()) { 1985 | nsvHost.smoothScrollTo(0, targetScrollY); 1986 | return true; 1987 | } 1988 | } 1989 | return false; 1990 | } 1991 | return false; 1992 | } 1993 | 1994 | @Override 1995 | public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 1996 | super.onInitializeAccessibilityNodeInfo(host, info); 1997 | final NestedScrollView nsvHost = (NestedScrollView) host; 1998 | info.setClassName(ScrollView.class.getName()); 1999 | if (nsvHost.isEnabled()) { 2000 | final int scrollRange = nsvHost.getScrollRange(); 2001 | if (scrollRange > 0) { 2002 | info.setScrollable(true); 2003 | if (nsvHost.getScrollY() > 0) { 2004 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2005 | } 2006 | if (nsvHost.getScrollY() < scrollRange) { 2007 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2008 | } 2009 | } 2010 | } 2011 | } 2012 | 2013 | @Override 2014 | public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 2015 | super.onInitializeAccessibilityEvent(host, event); 2016 | final NestedScrollView nsvHost = (NestedScrollView) host; 2017 | event.setClassName(ScrollView.class.getName()); 2018 | final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); 2019 | final boolean scrollable = nsvHost.getScrollRange() > 0; 2020 | record.setScrollable(scrollable); 2021 | record.setScrollX(nsvHost.getScrollX()); 2022 | record.setScrollY(nsvHost.getScrollY()); 2023 | record.setMaxScrollX(nsvHost.getScrollX()); 2024 | record.setMaxScrollY(nsvHost.getScrollRange()); 2025 | } 2026 | } 2027 | 2028 | } 2029 | 2030 | 2031 | 2032 | --------------------------------------------------------------------------------