Repository: NateRobinson/CardStackViewpager Branch: master Commit: 6b219109f1af Files: 46 Total size: 174.8 KB Directory structure: gitextract_qfkxk7en/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── gu/ │ │ └── cardstackviewpager/ │ │ └── ApplicationTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── gu/ │ │ │ └── cardstackviewpager/ │ │ │ ├── activity/ │ │ │ │ ├── AboutActivity.java │ │ │ │ ├── HomeActivity.java │ │ │ │ └── WebViewActivity.java │ │ │ ├── adapter/ │ │ │ │ └── ContentFragmentAdapter.java │ │ │ └── fragment/ │ │ │ └── CardFragment.java │ │ └── res/ │ │ ├── anim/ │ │ │ ├── slide_left_in.xml │ │ │ ├── slide_left_out.xml │ │ │ ├── slide_right_in.xml │ │ │ └── slide_right_out.xml │ │ ├── drawable/ │ │ │ ├── back_iv_bg.xml │ │ │ ├── custom_ll_bg.xml │ │ │ ├── github_click_bg.xml │ │ │ └── progress_bar_h5.xml │ │ ├── layout/ │ │ │ ├── activity_about.xml │ │ │ ├── activity_home.xml │ │ │ ├── activity_webview.xml │ │ │ └── fragment_card.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test/ │ └── java/ │ └── com/ │ └── gu/ │ └── cardstackviewpager/ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── library/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── gu/ │ │ └── library/ │ │ └── ApplicationTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── gu/ │ │ └── library/ │ │ ├── OrientedViewPager.java │ │ ├── transformer/ │ │ │ ├── VerticalBaseTransformer.java │ │ │ └── VerticalStackTransformer.java │ │ └── utils/ │ │ └── ScreenUtils.java │ └── test/ │ └── java/ │ └── com/ │ └── gu/ │ └── library/ │ └── ExampleUnitTest.java └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures /app/build .idea ================================================ FILE: README.md ================================================ # CardStackViewpager 卡片翻页效果的Viewpager 这个`CardStackViewpager `的灵感来自Github上面的 [`FlippableStackView`](https://github.com/blipinsk/FlippableStackView)开源项目,而我想实现的效果方向上恰好与`FlippableStackView`相反,并且细节上也有些区别,详见下面的效果对比图: ###### FlippableStackView运行效果图: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/two.gif?raw=true) ###### CardStackViewpager运行效果图: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/one.gif?raw=true) 这里讲一个小插曲,自己尝试实现`CardStackViewpager`的过程中,由于一开始对`PageTransformer`的`onTransform(View page, float position)`实在很困惑,于是我用自己小学般的英语写了一封邮件给`FlippableStackView`的开发者,尴尬的是,至今他没回我邮件。 回归正题,下面我就来具体讲一下`CardStackViewpager `的实现思路,其实整个核心就在下面这一段代码,把下面这段代码搞懂了,就可以通过自定义自己的`PageTransformer`实现各种各样想要的Viewpager效果了。 #### 核心的VerticalStackTransformer的onTransform方法最终版 ``` @Override protected void onTransform(View page, float position) { if (position <= 0.0f) { page.setAlpha(1.0f); Log.e("onTransform", "position <= 0.0f ==>" + position); page.setTranslationY(0f); //控制停止滑动切换的时候,只有最上面的一张卡片可以点击 page.setClickable(true); } else if (position <= 3.0f) { Log.e("onTransform", "position <= 3.0f ==>" + position); float scale = (float) (page.getWidth() - ScreenUtils.dp2px(context, spaceBetweenFirAndSecWith * position)) / (float) (page.getWidth()); //控制下面卡片的可见度 page.setAlpha(1.0f); //控制停止滑动切换的时候,只有最上面的一张卡片可以点击 page.setClickable(false); page.setPivotX(page.getWidth() / 2f); page.setPivotY(page.getHeight() / 2f); page.setScaleX(scale); page.setScaleY(scale); page.setTranslationY(-page.getHeight() * position + (page.getHeight() * 0.5f) * (1 - scale) + ScreenUtils.dp2px(context, spaceBetweenFirAndSecHeight) * position); } } ``` ###### 在分析上面的代码之前,我们需要有以下几个知识准备: 1. Viewpager的`setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer)`方法的第一个参数,用来控制加入到Viewpager的Views对象是正序的还是倒序的,这里为了实现我们想要的效果,需要让第一个添加到布局的View来到第一个展示,所以传入`true`; 2. Viewpager的`setOffscreenPageLimit(int limit)`方法,设置有多少的缓存Views,这个将决定我们的卡片重叠展示的效果显示几层卡片效果。 现在我们继续看上面的`onTransform(View page, float position)`方法,这个方法设计的很巧妙,当初我在探索的时候,通过打印日志来判断这个方法是如何执行的时候,发现这这个`position`的值看似毫无规律,后来我想到以前数学里推理定理时的方法,从`特殊情况入手`,再`一点点分析其他情况`,然后一步步的实现上面的代码。 #### 第一步,分析应用初始化进来的时候的position 此时的`onTransform(View page, float position)`方法如下: ``` @Override protected void onTransform(View page, float position) { Log.e("onTransform","position ==>"+position); //设置每个卡片y方向偏移量,这样可以使卡片都完全叠加起来 page.setTranslationY(-page.getHeight() * position); } ``` 对应日志如下: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/three.png?raw=true) 根据这个日志很明显的可以判断得到:由于我现在设置的`setOffscreenPageLimit(int limit)`值为4,所以可以看到position有上面几种情况,显而易见,每个position对应了一张卡片,这个时候界面的效果如图: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/five.png?raw=true) 现在猜想2,3,4,5号卡片就在1号卡片下面,现在要想个法子证实我们的猜想,将`onTransform(View page, float position)`方法改成下面这样: ``` @Override protected void onTransform(View page, float position) { Log.e("onTransform","position ==>"+position); //设置卡片透明度 page.setAlpha(0.5f); //设置缩放中点 page.setPivotX(page.getWidth() / 2f); page.setPivotY(page.getHeight() / 2f); //设置缩放的比例 此处设置两个相邻的卡片的缩放比率为0.9f page.setScaleX((float) Math.pow(0.9f,position)); page.setScaleY((float) Math.pow(0.9f,position)); //设置每个卡片y方向偏移量,这样可以使卡片都完全叠加起来 page.setTranslationY(-page.getHeight() * position); } ``` 运行起来之后,证实了我们的想法: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/six.png?raw=true) #### 第二步,实现卡片叠加的最终效果 分析上面的图片效果,可以发现,把第二张卡片往下移动一段距离之后,就可以形成一个卡片叠加的初步效果了,变成下面这样: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/seven.png?raw=true) 其他的卡片,道理一样,那么如何实现这个向下偏移的值呢,这个值如何以一个表达式表现出来呢,先看下面的A,B,C步骤的分析图: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/eight.png?raw=true) 显而易见,相隔两张卡片的偏移量为:`(H2-H1)+d1`,我们稍微改变一下`onTransform(View page, float position)`方法如下: ``` @Override protected void onTransform(View page, float position) { Log.e("onTransform", "position ==>" + position); page.setAlpha(0.5f); page.setPivotX(page.getWidth() / 2f); page.setPivotY(page.getHeight() / 2f); page.setScaleX((float) Math.pow(0.9f, position)); page.setScaleY((float) Math.pow(0.9f, position)); //修改过的代码 page.setTranslationY(-page.getHeight() * position + (page.getHeight() * 0.5f) * (1 - (float) Math.pow(0.9f, position)) + ScreenUtils.dp2px(context, 10)); } ``` 此时的效果图如下: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/nine.png?raw=true) 卡片半透明的时候,效果还不是特别的明显,把`page.setAlpha(0.5f)`改为`page.setAlpha(1.0f)`再试一次: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/ten.png?raw=true) 惊喜的发现这不就是卡片叠加效果嘛,虽然现在的效果细节还有点问题,我们不急,这个细节问题简单分析一下就会想到,是我们的缩放比例问题导致的,继续下一步的优化,我们将会解决这个问题。 #### 第三步,根据相邻卡片的间距值动态设置缩放值 上面的`onTransform(View page, float position)`方法中,我们的x,y缩放比例都是写的一个固定值`0.9f`,这个显然不能满足日常需求,我这里是设置上下两张卡片的宽度比来作为最终想要的缩放比例,修改`onTransform(View page, float position)`方法如下: ``` @Override protected void onTransform(View page, float position) { Log.e("onTransform", "position ==>" + position); float scale = (float) (page.getWidth() - ScreenUtils.dp2px(context, 20 * position)) / (float) (page.getWidth()); page.setAlpha(1.0f); page.setPivotX(page.getWidth() / 2f); page.setPivotY(page.getHeight() / 2f); page.setScaleX(scale); page.setScaleY(scale); //修改过的代码 page.setTranslationY(-page.getHeight() * position + (page.getHeight() * 0.5f) * (1 - scale) + ScreenUtils.dp2px(context, 10) * position); } ``` 再跑一下程序,完美的卡片效果就出现了: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/eleven.png?raw=true) #### 第四步,特殊到一般,实现最终的卡片滑动效果 此时,我们尝试一下滑动Viewpager,发现卡片的切换效果并没有如期的出现,通过多次尝试和分析,我发现,由于我们这里没有对当前滑动过去的那张卡片做特殊处理,这里的特殊处理指的是:为了实现卡片抽动的切换效果,当前滑动的卡片应该不用执行任何缩放和偏移的操作,修改为`page.setTranslationY(0f);`,具体代码如下: >这里列出一篇博客: [http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/0814/1650.html](http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/0814/1650.html),他主要讲了对`onTransform(View page, float position)`中position的理解 ``` @Override protected void onTransform(View page, float position) { Log.e("onTransform", "position ==>" + position); if (position <= 0.0f) { page.setAlpha(1.0f); //出现卡片抽动效果的关键代码 page.setTranslationY(0f); } else { float scale = (float) (page.getWidth() - ScreenUtils.dp2px(context, 20 * position)) / (float) (page.getWidth()); page.setAlpha(1.0f); page.setPivotX(page.getWidth() / 2f); page.setPivotY(page.getHeight() / 2f); page.setScaleX(scale); page.setScaleY(scale); //修改过的代码 page.setTranslationY(-page.getHeight() * position + (page.getHeight() * 0.5f) * (1 - scale) + ScreenUtils.dp2px(context, 10) * position); } } ``` 至此,已经可以实现文章开头的动画效果了。回头想一下,我们一直在基于特殊的情况写代码,最后发现其实他就是所有一般情况中的一种,只不过特殊情况由于他的特殊性最容易进行分析总结,更有利于我们编写出易懂的代码。 最后补充下,在实际项目中,在每张卡片上可能还有有点击区域,更可能整张卡片都是一个点击区域,这个时候就会发现一个问题,当处于这种情况的时候: ![enter image description here](https://github.com/NateRobinson/CardStackViewpager/blob/master/img/eleven.png?raw=true) 我不但可以点到卡片1,也会点到卡片2,卡片3。。。这样肯定不行的,所以我们再次回到`onTransform(View page, float position)`方法,在里面加一个控制: ``` @Override protected void onTransform(View page, float position) { Log.e("onTransform", "position ==>" + position); if (position <= 0.0f) { //最上面的卡片可以点击 page.setClickable(true); ....... } else { //下面的卡片不可点击 page.setClickable(false); ........ } } ``` 另外我们可能只需要4张卡片重叠的效果就行,这个时候改变一下判断条件即可: ``` @Override protected void onTransform(View page, float position) { Log.e("onTransform", "position ==>" + position); if (position <= 0.0f) { ...... //控制显示几张卡片 } else if(position <= 3.0f) { ...... } } ``` 至此这边文章就要结束了,这是我的总结,希望能帮助大家对`onTransform(View page, float position)`方法有一个更深的理解。 ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 27 buildToolsVersion "28.0.2" defaultConfig { applicationId "com.gu.cardstackviewpager" minSdkVersion 15 targetSdkVersion 27 versionCode 1 versionName "1.0.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') testImplementation 'junit:junit:4.12' implementation 'com.android.support:appcompat-v7:27.1.1' implementation project(':library') implementation 'com.android.support:cardview-v7:27.1.1' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in D:\AndroidTools\sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/androidTest/java/com/gu/cardstackviewpager/ApplicationTest.java ================================================ package com.gu.cardstackviewpager; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/gu/cardstackviewpager/activity/AboutActivity.java ================================================ package com.gu.cardstackviewpager.activity; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.View; import com.gu.cardstackviewpager.R; /** * Created by Nate on 2016/7/22. */ public class AboutActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { //设置切换动画 overridePendingTransition(R.anim.slide_right_in, R.anim.slide_left_out); super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); findViewById(R.id.back_ll).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); findViewById(R.id.my_blog_ll).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle bundle = new Bundle(); bundle.putString(WebViewActivity.URL_KEY, getString(R.string.my_blog)); Intent intent = new Intent(AboutActivity.this, WebViewActivity.class); intent.putExtras(bundle); startActivity(intent); } }); findViewById(R.id.my_git_ll).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle bundle = new Bundle(); bundle.putString(WebViewActivity.URL_KEY, getString(R.string.my_github)); Intent intent = new Intent(AboutActivity.this, WebViewActivity.class); intent.putExtras(bundle); startActivity(intent); } }); } @Override public void finish() { super.finish(); //设置切换动画 overridePendingTransition(R.anim.slide_left_in, R.anim.slide_right_out); } } ================================================ FILE: app/src/main/java/com/gu/cardstackviewpager/activity/HomeActivity.java ================================================ package com.gu.cardstackviewpager.activity; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.view.View; import com.gu.cardstackviewpager.R; import com.gu.cardstackviewpager.adapter.ContentFragmentAdapter; import com.gu.cardstackviewpager.fragment.CardFragment; import com.gu.library.OrientedViewPager; import com.gu.library.transformer.VerticalStackTransformer; import java.util.ArrayList; import java.util.List; /** * Created by Nate on 2016/7/22. */ public class HomeActivity extends AppCompatActivity { private OrientedViewPager mOrientedViewPager; private ContentFragmentAdapter mContentFragmentAdapter; private List mFragments = new ArrayList<>(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); mOrientedViewPager = (OrientedViewPager) findViewById(R.id.view_pager); //制造数据 for (int i = 0; i < 10; i++) { mFragments.add(CardFragment.newInstance(i + 1)); } mContentFragmentAdapter = new ContentFragmentAdapter(getSupportFragmentManager(), mFragments); //设置viewpager的方向为竖直 mOrientedViewPager.setOrientation(OrientedViewPager.Orientation.VERTICAL); //设置limit mOrientedViewPager.setOffscreenPageLimit(4); //设置transformer mOrientedViewPager.setPageTransformer(true, new VerticalStackTransformer(getApplicationContext())); mOrientedViewPager.setAdapter(mContentFragmentAdapter); //跳转关于我的界面 findViewById(R.id.about_iv).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(HomeActivity.this, AboutActivity.class); startActivity(intent); } }); } } ================================================ FILE: app/src/main/java/com/gu/cardstackviewpager/activity/WebViewActivity.java ================================================ package com.gu.cardstackviewpager.activity; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.ProgressBar; import com.gu.cardstackviewpager.R; /** * Created by Nate on 2016/7/22. */ public class WebViewActivity extends AppCompatActivity { public static final String URL_KEY = "url_key"; private WebView mWebView; private ProgressBar mProgressBar; private String url = "https://github.com/NateRobinson"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { //设置切换动画 overridePendingTransition(R.anim.slide_right_in, R.anim.slide_left_out); super.onCreate(savedInstanceState); setContentView(R.layout.activity_webview); if (getIntent().getExtras() != null) { url = getIntent().getExtras().getString(WebViewActivity.URL_KEY); } mWebView= (WebView) findViewById(R.id.web_view); mProgressBar= (ProgressBar) findViewById(R.id.web_view_progress_bar); WebSettings mWebSettings = mWebView.getSettings(); mWebSettings.setJavaScriptEnabled(true); mWebSettings.setUseWideViewPort(true); mWebSettings.setSupportZoom(true); mWebSettings.setBuiltInZoomControls(true); mWebSettings.setDisplayZoomControls(false); mWebSettings.setUseWideViewPort(true); mWebSettings.setLoadWithOverviewMode(true); mWebView.setWebChromeClient(new WebChromeClient()); mWebView.setWebViewClient(new WebViewClient()); mWebView.loadUrl(url); } public class WebChromeClient extends android.webkit.WebChromeClient { @Override public void onProgressChanged(WebView view, int newProgress) { if (newProgress == 100) { mProgressBar.setVisibility(View.GONE); } else { if (mProgressBar.getVisibility() == View.GONE) { mProgressBar.setVisibility(View.VISIBLE); } mProgressBar.setProgress(newProgress); } super.onProgressChanged(view, newProgress); } } class WebViewClient extends android.webkit.WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } } @Override public void onBackPressed() { if (mWebView.canGoBack()) { mWebView.goBack(); } else { super.onBackPressed(); } } @Override public void finish() { super.finish(); //设置切换动画 overridePendingTransition(R.anim.slide_left_in, R.anim.slide_right_out); } } ================================================ FILE: app/src/main/java/com/gu/cardstackviewpager/adapter/ContentFragmentAdapter.java ================================================ package com.gu.cardstackviewpager.adapter; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.PagerAdapter; import java.util.ArrayList; import java.util.List; public class ContentFragmentAdapter extends FragmentStatePagerAdapter { private List fragments = new ArrayList<>(); private int itemPosition= PagerAdapter.POSITION_UNCHANGED; public ContentFragmentAdapter(FragmentManager fm, List fragments) { super(fm); this.fragments = fragments; } @Override public Fragment getItem(int position) { return fragments.get(position); } @Override public int getCount() { return fragments.size(); } @Override public CharSequence getPageTitle(int position) { return ""; } @Override public int getItemPosition(Object object) { return getItemPosition(); } public int getItemPosition() { return itemPosition; } public void setItemPosition(int itemPosition) { this.itemPosition = itemPosition; } } ================================================ FILE: app/src/main/java/com/gu/cardstackviewpager/fragment/CardFragment.java ================================================ package com.gu.cardstackviewpager.fragment; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.gu.cardstackviewpager.R; /** * Created by Nate on 2016/7/22. */ public class CardFragment extends Fragment { private static final String INDEX_KEY = "index_key"; public static CardFragment newInstance(int index) { CardFragment fragment = new CardFragment(); Bundle bdl = new Bundle(); bdl.putInt(INDEX_KEY, index); fragment.setArguments(bdl); return fragment; } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { final View v = inflater.inflate(R.layout.fragment_card, container, false); TextView cardNumTv = (TextView) v.findViewById(R.id.card_num_tv); final Bundle bundle = getArguments(); if (bundle != null) { cardNumTv.setText(bundle.getInt(INDEX_KEY, 0) + ""); } cardNumTv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getActivity(), "点击了" + bundle.getInt(INDEX_KEY, 0) + "", Toast.LENGTH_SHORT).show(); } }); return v; } } ================================================ FILE: app/src/main/res/anim/slide_left_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_left_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_right_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_right_out.xml ================================================ ================================================ FILE: app/src/main/res/drawable/back_iv_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/custom_ll_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/github_click_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/progress_bar_h5.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_about.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_home.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_webview.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_card.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #00BA9C #05917a #141d26 #00000000 #FFFFFF #efefef #00BA9C ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 60sp 22sp 18sp 16sp 14sp 12sp 20dp 20dp 20dp 40dp 8dp 8dp 10dp 8dp 10dp 8dp 10dp 8dp 10dp 8dp 10dp ================================================ FILE: app/src/main/res/values/strings.xml ================================================ CardStackViewpager Nate Robinson 840501291 https://github.com/NateRobinson http://blog.csdn.net/u011771755 Never give up~ ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/test/java/com/gu/cardstackviewpager/ExampleUnitTest.java ================================================ package com.gu.cardstackviewpager; import org.junit.Test; import static org.junit.Assert.*; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:3.2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() maven { url "https://jitpack.io" } } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Dec 28 10:00:20 PST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: library/.gitignore ================================================ /build ================================================ FILE: library/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion 27 buildToolsVersion "28.0.2" defaultConfig { minSdkVersion 15 targetSdkVersion 27 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.12' implementation 'com.android.support:appcompat-v7:27.1.1' } ================================================ FILE: library/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in D:\AndroidTools\sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: library/src/androidTest/java/com/gu/library/ApplicationTest.java ================================================ package com.gu.library; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: library/src/main/AndroidManifest.xml ================================================ ================================================ FILE: library/src/main/java/com/gu/library/OrientedViewPager.java ================================================ /** * Copyright 2015 Bartosz Lipinski * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gu.library; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.PagerAdapter; import android.support.v4.view.VelocityTrackerCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewConfigurationCompat; import android.support.v4.view.ViewPager; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v4.widget.EdgeEffectCompat; import android.util.AttributeSet; import android.util.Log; import android.view.FocusFinder; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.animation.Interpolator; import android.widget.Scroller; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; /** * Created by Bartosz Lipinski * Based on castorflex's VerticalViewPager (https://github.com/castorflex/VerticalViewPager) * * 03.05.15 */ public class OrientedViewPager extends ViewGroup { public enum Orientation { VERTICAL, HORIZONTAL } private static final String TAG = "ViewPager"; private static final boolean DEBUG = false; private static final boolean USE_CACHE = false; private static final int DEFAULT_OFFSCREEN_PAGES = 1; private static final int MAX_SETTLE_DURATION = 600; // ms private static final int MIN_DISTANCE_FOR_FLING = 25; // dips private static final int DEFAULT_GUTTER_SIZE = 16; // dips private static final int MIN_FLING_VELOCITY = 400; // dips private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_gravity }; /** * Used to track what the expected number of items in the adapter should be. * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. */ private int mExpectedAdapterCount; private static class ItemInfo { Object object; int position; boolean scrolling; float sizeFactor; float offset; } private static final Comparator COMPARATOR = new Comparator() { @Override public int compare(ItemInfo lhs, ItemInfo rhs) { return lhs.position - rhs.position; } }; private static final Interpolator sInterpolator = new Interpolator() { public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; private final ArrayList mItems = new ArrayList(); private final ItemInfo mTempItem = new ItemInfo(); private final Rect mTempRect = new Rect(); private Orientation mOrientation = Orientation.HORIZONTAL; private PagerAdapter mAdapter; private int mCurItem; // Index of currently displayed page. private int mRestoredCurItem = -1; private Parcelable mRestoredAdapterState = null; private ClassLoader mRestoredClassLoader = null; private Scroller mScroller; private PagerObserver mObserver; private int mPageMargin; private Drawable mMarginDrawable; private int mTopLeftPageBounds; private int mBottomRightPageBounds; // Offsets of the first and last items, if known. // Set during population, used to determine if we are at the beginning // or end of the pager data set during touch scrolling. private float mFirstOffset = -Float.MAX_VALUE; private float mLastOffset = Float.MAX_VALUE; private int mChildWidthMeasureSpec; private int mChildHeightMeasureSpec; private boolean mInLayout; private boolean mScrollingCacheEnabled; private boolean mPopulatePending; private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; private boolean mIsBeingDragged; private boolean mIsUnableToDrag; private boolean mIgnoreGutter; private int mDefaultGutterSize; private int mGutterSize; private int mTouchSlop; /** * Position of the last motion event. */ private float mLastMotionX; private float mLastMotionY; private float mInitialMotionX; private float mInitialMotionY; /** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. */ private int mActivePointerId = INVALID_POINTER; /** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */ private static final int INVALID_POINTER = -1; /** * Determines speed during touch scrolling */ private VelocityTracker mVelocityTracker; private int mMinimumVelocity; private int mMaximumVelocity; private int mFlingDistance; private int mCloseEnough; // If the pager is at least this close to its final position, complete the scroll // on touch down and let the user interact with the content inside instead of // "catching" the flinging pager. private static final int CLOSE_ENOUGH = 2; // dp private boolean mFakeDragging; private long mFakeDragBeginTime; private EdgeEffectCompat mTopLeftEdge; private EdgeEffectCompat mRightBottomEdge; private boolean mFirstLayout = true; private boolean mNeedCalculatePageOffsets = false; private boolean mCalledSuper; private int mDecorChildCount; private ViewPager.OnPageChangeListener mOnPageChangeListener; private ViewPager.OnPageChangeListener mInternalPageChangeListener; private OnAdapterChangeListener mAdapterChangeListener; private ViewPager.PageTransformer mPageTransformer; private Method mSetChildrenDrawingOrderEnabled; private static final int DRAW_ORDER_DEFAULT = 0; private static final int DRAW_ORDER_FORWARD = 1; private static final int DRAW_ORDER_REVERSE = 2; private int mDrawingOrder; private ArrayList mDrawingOrderedChildren; private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); /** * Indicates that the pager is in an idle, settled state. The current page * is fully in view and no animation is in progress. */ public static final int SCROLL_STATE_IDLE = 0; /** * Indicates that the pager is currently being dragged by the user. */ public static final int SCROLL_STATE_DRAGGING = 1; /** * Indicates that the pager is in the process of settling to a final position. */ public static final int SCROLL_STATE_SETTLING = 2; private final Runnable mEndScrollRunnable = new Runnable() { public void run() { setScrollState(SCROLL_STATE_IDLE); populate(); } }; private int mScrollState = SCROLL_STATE_IDLE; /** * Used internally to monitor when adapters are switched. */ interface OnAdapterChangeListener { public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); } /** * Used internally to tag special types of child views that should be added as * pager decorations by default. */ interface Decor { } public OrientedViewPager(Context context) { super(context); initViewPager(); } public OrientedViewPager(Context context, AttributeSet attrs) { super(context, attrs); initViewPager(); } void initViewPager() { setWillNotDraw(false); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setFocusable(true); final Context context = getContext(); mScroller = new Scroller(context, sInterpolator); final ViewConfiguration configuration = ViewConfiguration.get(context); final float density = context.getResources().getDisplayMetrics().density; mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mTopLeftEdge = new EdgeEffectCompat(context); mRightBottomEdge = new EdgeEffectCompat(context); mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); mCloseEnough = (int) (CLOSE_ENOUGH * density); mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); if (ViewCompat.getImportantForAccessibility(this) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } } public void setOrientation(Orientation orientation) { mOrientation = orientation; } @Override protected void onDetachedFromWindow() { removeCallbacks(mEndScrollRunnable); super.onDetachedFromWindow(); } private void setScrollState(int newState) { if (mScrollState == newState) { return; } mScrollState = newState; if (mPageTransformer != null) { // PageTransformers can do complex things that benefit from hardware layers. enableLayers(newState != SCROLL_STATE_IDLE); } if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrollStateChanged(newState); } } /** * Set a PagerAdapter that will supply views for this pager as needed. * * @param adapter Adapter to use */ public void setAdapter(PagerAdapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mObserver); mAdapter.startUpdate(this); for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); mAdapter.destroyItem(this, ii.position, ii.object); } mAdapter.finishUpdate(this); mItems.clear(); removeNonDecorViews(); mCurItem = 0; scrollTo(0, 0); } final PagerAdapter oldAdapter = mAdapter; mAdapter = adapter; mExpectedAdapterCount = 0; if (mAdapter != null) { if (mObserver == null) { mObserver = new PagerObserver(); } mAdapter.registerDataSetObserver(mObserver); mPopulatePending = false; final boolean wasFirstLayout = mFirstLayout; mFirstLayout = true; mExpectedAdapterCount = mAdapter.getCount(); if (mRestoredCurItem >= 0) { mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); setCurrentItemInternal(mRestoredCurItem, false, true); mRestoredCurItem = -1; mRestoredAdapterState = null; mRestoredClassLoader = null; } else if (!wasFirstLayout) { populate(); } else { requestLayout(); } } if (mAdapterChangeListener != null && oldAdapter != adapter) { mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); } } private void removeNonDecorViews() { for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) { removeViewAt(i); i--; } } } /** * Retrieve the current adapter supplying pages. * * @return The currently registered PagerAdapter */ public PagerAdapter getAdapter() { return mAdapter; } void setOnAdapterChangeListener(OnAdapterChangeListener listener) { mAdapterChangeListener = listener; } private int getClientSize() { return (mOrientation == Orientation.VERTICAL) ? getMeasuredHeight() - getPaddingTop() - getPaddingBottom() : getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); } /** * Set the currently selected page. If the ViewPager has already been through its first * layout with its current adapter there will be a smooth animated transition between * the current item and the specified item. * * @param item Item index to select */ public void setCurrentItem(int item) { mPopulatePending = false; setCurrentItemInternal(item, !mFirstLayout, false); } /** * Set the currently selected page. * * @param item Item index to select * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately */ public void setCurrentItem(int item, boolean smoothScroll) { mPopulatePending = false; setCurrentItemInternal(item, smoothScroll, false); } public int getCurrentItem() { return mCurItem; } void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { setCurrentItemInternal(item, smoothScroll, always, 0); } void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { if (mAdapter == null || mAdapter.getCount() <= 0) { setScrollingCacheEnabled(false); return; } if (!always && mCurItem == item && mItems.size() != 0) { setScrollingCacheEnabled(false); return; } if (item < 0) { item = 0; } else if (item >= mAdapter.getCount()) { item = mAdapter.getCount() - 1; } final int pageLimit = mOffscreenPageLimit; if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { // We are doing a jump by more than one page. To avoid // glitches, we want to keep all current pages in the view // until the scroll ends. for (int i = 0; i < mItems.size(); i++) { mItems.get(i).scrolling = true; } } final boolean dispatchSelected = mCurItem != item; if (mFirstLayout) { // We don't have any idea how big we are yet and shouldn't have any pages either. // Just set things up and let the pending layout handle things. mCurItem = item; if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } requestLayout(); } else { populate(item); scrollToItem(item, smoothScroll, velocity, dispatchSelected); } } private void scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected) { final ItemInfo curInfo = infoForPosition(item); int dest = 0; if (curInfo != null) { final int size = getClientSize(); dest = (int) (size * Math.max(mFirstOffset, Math.min(curInfo.offset, mLastOffset))); } if (smoothScroll) { if (mOrientation == Orientation.VERTICAL) { smoothScrollTo(0, dest, velocity); } else { smoothScrollTo(dest, 0, velocity); } if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } } else { if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } completeScroll(false); if (mOrientation == Orientation.VERTICAL) { scrollTo(0, dest); } else { scrollTo(dest, 0); } pageScrolled(dest); } } /** * Set a listener that will be invoked whenever the page changes or is incrementally * scrolled. See {@link android.support.v4.view.ViewPager.OnPageChangeListener}. * * @param listener Listener to set */ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { mOnPageChangeListener = listener; } /** * Set a {@link android.support.v4.view.ViewPager.PageTransformer} that will be called for each * attached page whenever * the scroll position is changed. This allows the application to apply custom property * transformations to each page, overriding the default sliding look and feel. *

*

Note: Prior to Android 3.0 the property animation APIs did not exist. * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.

* * @param reverseDrawingOrder true if the supplied PageTransformer requires page views * to be drawn from last to first instead of first to last. * @param transformer PageTransformer that will modify each page's animation properties */ public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer) { if (Build.VERSION.SDK_INT >= 11) { final boolean hasTransformer = transformer != null; final boolean needsPopulate = hasTransformer != (mPageTransformer != null); mPageTransformer = transformer; setChildrenDrawingOrderEnabledCompat(hasTransformer); if (hasTransformer) { mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; } else { mDrawingOrder = DRAW_ORDER_DEFAULT; } if (needsPopulate) populate(); } } void setChildrenDrawingOrderEnabledCompat(boolean enable) { if (Build.VERSION.SDK_INT >= 7) { if (mSetChildrenDrawingOrderEnabled == null) { try { mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE }); } catch (NoSuchMethodException e) { Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); } } try { mSetChildrenDrawingOrderEnabled.invoke(this, enable); } catch (Exception e) { Log.e(TAG, "Error changing children drawing order", e); } } } @Override protected int getChildDrawingOrder(int childCount, int i) { final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; return result; } /** * Set a separate OnPageChangeListener for internal use by the support library. * * @param listener Listener to set * @return The old listener that was set, if any. */ ViewPager.OnPageChangeListener setInternalPageChangeListener( ViewPager.OnPageChangeListener listener) { ViewPager.OnPageChangeListener oldListener = mInternalPageChangeListener; mInternalPageChangeListener = listener; return oldListener; } /** * Returns the number of pages that will be retained to either side of the * current page in the view hierarchy in an idle state. Defaults to 1. * * @return How many pages will be kept offscreen on either side * @see #setOffscreenPageLimit(int) */ public int getOffscreenPageLimit() { return mOffscreenPageLimit; } /** * Set the number of pages that should be retained to either side of the * current page in the view hierarchy in an idle state. Pages beyond this * limit will be recreated from the adapter when needed. *

*

This is offered as an optimization. If you know in advance the number * of pages you will need to support or have lazy-loading mechanisms in place * on your pages, tweaking this setting can have benefits in perceived smoothness * of paging animations and interaction. If you have a small number of pages (3-4) * that you can keep active all at once, less time will be spent in layout for * newly created view subtrees as the user pages back and forth.

*

*

You should keep this limit low, especially if your pages have complex layouts. * This setting defaults to 1.

* * @param limit How many pages will be kept offscreen in an idle state. */ public void setOffscreenPageLimit(int limit) { if (limit < DEFAULT_OFFSCREEN_PAGES) { Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + DEFAULT_OFFSCREEN_PAGES); limit = DEFAULT_OFFSCREEN_PAGES; } if (limit != mOffscreenPageLimit) { mOffscreenPageLimit = limit; populate(); } } /** * Set the margin between pages. * * @param marginPixels Distance between adjacent pages in pixels * @see #getPageMargin() * @see #setPageMarginDrawable(Drawable) * @see #setPageMarginDrawable(int) */ public void setPageMargin(int marginPixels) { final int oldMargin = mPageMargin; mPageMargin = marginPixels; final int size = (mOrientation == Orientation.VERTICAL) ? getHeight() : getWidth(); recomputeScrollPosition(size, size, marginPixels, oldMargin); requestLayout(); } /** * Return the margin between pages. * * @return The size of the margin in pixels */ public int getPageMargin() { return mPageMargin; } /** * Set a drawable that will be used to fill the margin between pages. * * @param d Drawable to display between pages */ public void setPageMarginDrawable(Drawable d) { mMarginDrawable = d; if (d != null) refreshDrawableState(); setWillNotDraw(d == null); invalidate(); } /** * Set a drawable that will be used to fill the margin between pages. * * @param resId Resource ID of a drawable to display between pages */ public void setPageMarginDrawable(int resId) { setPageMarginDrawable(getContext().getResources().getDrawable(resId)); } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || who == mMarginDrawable; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); final Drawable d = mMarginDrawable; if (d != null && d.isStateful()) { d.setState(getDrawableState()); } } // We want the duration of the page snap animation to be influenced by the distance that // the screen has to travel, however, we don't want this duration to be effected in a // purely linear fashion. Instead, we use this method to moderate the effect that the distance // of travel has on the overall snap duration. float distanceInfluenceForSnapDuration(float f) { f -= 0.5f; // center the values about 0. f *= 0.3f * Math.PI / 2.0f; return (float) Math.sin(f); } /** * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. * * @param x the number of pixels to scroll by on the X axis * @param y the number of pixels to scroll by on the Y axis */ void smoothScrollTo(int x, int y) { smoothScrollTo(x, y, 0); } /** * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. * * @param x the number of pixels to scroll by on the X axis * @param y the number of pixels to scroll by on the Y axis * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) */ void smoothScrollTo(int x, int y, int velocity) { if (getChildCount() == 0) { // Nothing to do. setScrollingCacheEnabled(false); return; } int sx = getScrollX(); int sy = getScrollY(); int dx = x - sx; int dy = y - sy; if (dx == 0 && dy == 0) { completeScroll(false); populate(); setScrollState(SCROLL_STATE_IDLE); return; } setScrollingCacheEnabled(true); setScrollState(SCROLL_STATE_SETTLING); final int size = getClientSize(); final int halfSize = size / 2; final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / size); final float distance = halfSize + halfSize * distanceInfluenceForSnapDuration(distanceRatio); int duration = 0; velocity = Math.abs(velocity); if (velocity > 0) { duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); } else { final float pageSize = size * mAdapter.getPageWidth(mCurItem); final float pageDelta = (float) Math.abs(dx) / (pageSize + mPageMargin); duration = (int) ((pageDelta + 1) * 100); } duration = Math.min(duration, MAX_SETTLE_DURATION); mScroller.startScroll(sx, sy, dx, dy, duration); ViewCompat.postInvalidateOnAnimation(this); } ItemInfo addNewItem(int position, int index) { ItemInfo ii = new ItemInfo(); ii.position = position; ii.object = mAdapter.instantiateItem(this, position); ii.sizeFactor = mAdapter.getPageWidth(position); if (index < 0 || index >= mItems.size()) { mItems.add(ii); } else { mItems.add(index, ii); } return ii; } void dataSetChanged() { // This method only gets called if our observer is attached, so mAdapter is non-null. final int adapterCount = mAdapter.getCount(); mExpectedAdapterCount = adapterCount; boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && mItems.size() < adapterCount; int newCurrItem = mCurItem; boolean isUpdating = false; for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); final int newPos = mAdapter.getItemPosition(ii.object); if (newPos == PagerAdapter.POSITION_UNCHANGED) { continue; } if (newPos == PagerAdapter.POSITION_NONE) { mItems.remove(i); i--; if (!isUpdating) { mAdapter.startUpdate(this); isUpdating = true; } mAdapter.destroyItem(this, ii.position, ii.object); needPopulate = true; if (mCurItem == ii.position) { // Keep the current item in the valid range newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); needPopulate = true; } continue; } if (ii.position != newPos) { if (ii.position == mCurItem) { // Our current item changed position. Follow it. newCurrItem = newPos; } ii.position = newPos; needPopulate = true; } } if (isUpdating) { mAdapter.finishUpdate(this); } Collections.sort(mItems, COMPARATOR); if (needPopulate) { // Reset our known page widths; populate will recompute them. final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) { lp.heightFactor = 0.f; } } setCurrentItemInternal(newCurrItem, false, true); requestLayout(); } } void populate() { populate(mCurItem); } void populate(int newCurrentItem) { ItemInfo oldCurInfo = null; int focusDirection = View.FOCUS_FORWARD; if (mCurItem != newCurrentItem) { focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP; oldCurInfo = infoForPosition(mCurItem); mCurItem = newCurrentItem; } if (mAdapter == null) { sortChildDrawingOrder(); return; } // Bail now if we are waiting to populate. This is to hold off // on creating views from the time the user releases their finger to // fling to a new position until we have finished the scroll to // that position, avoiding glitches from happening at that point. if (mPopulatePending) { if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); sortChildDrawingOrder(); return; } // Also, don't populate until we are attached to a window. This is to // avoid trying to populate before we have restored our view hierarchy // state and conflicting with what is restored. if (getWindowToken() == null) { return; } mAdapter.startUpdate(this); final int pageLimit = mOffscreenPageLimit; final int startPos = Math.max(0, mCurItem - pageLimit); final int N = mAdapter.getCount(); final int endPos = Math.min(N - 1, mCurItem + pageLimit); if (N != mExpectedAdapterCount) { String resName; try { resName = getResources().getResourceName(getId()); } catch (Resources.NotFoundException e) { resName = Integer.toHexString(getId()); } throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + " contents without calling PagerAdapter#notifyDataSetChanged!" + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + " Pager id: " + resName + " Pager class: " + getClass() + " Problematic adapter: " + mAdapter.getClass()); } // Locate the currently focused item or add it if needed. int curIndex = -1; ItemInfo curItem = null; for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); if (ii.position >= mCurItem) { if (ii.position == mCurItem) curItem = ii; break; } } if (curItem == null && N > 0) { curItem = addNewItem(mCurItem, curIndex); } // Fill 3x the available width or up to the number of offscreen // pages requested to either side, whichever is larger. // If we have no current item we have no work to do. if (curItem != null) { float extraSizeTopLeft = 0.f; int itemIndex = curIndex - 1; ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; final int clientSize = getClientSize(); final float topLeftSizeNeeded = clientSize <= 0 ? 0 : 2.f - curItem.sizeFactor + (float) getPaddingLeft() / (float) clientSize; for (int pos = mCurItem - 1; pos >= 0; pos--) { if (extraSizeTopLeft >= topLeftSizeNeeded && pos < startPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } itemIndex--; curIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraSizeTopLeft += ii.sizeFactor; itemIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } else { ii = addNewItem(pos, itemIndex + 1); extraSizeTopLeft += ii.sizeFactor; curIndex++; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } float extraSizeBottomRight = curItem.sizeFactor; itemIndex = curIndex + 1; if (extraSizeBottomRight < 2.f) { ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; final float bottomRightSizeNeeded = clientSize <= 0 ? 0 : (float) getPaddingRight() / (float) clientSize + 2.f; for (int pos = mCurItem + 1; pos < N; pos++) { if (extraSizeBottomRight >= bottomRightSizeNeeded && pos > endPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraSizeBottomRight += ii.sizeFactor; itemIndex++; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } else { ii = addNewItem(pos, itemIndex); itemIndex++; extraSizeBottomRight += ii.sizeFactor; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } } calculatePageOffsets(curItem, curIndex, oldCurInfo); } if (DEBUG) { Log.i(TAG, "Current page list:"); for (int i = 0; i < mItems.size(); i++) { Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); } } mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); mAdapter.finishUpdate(this); // Check width measurement of current pages and drawing sort order. // Update LayoutParams as needed. final int childCount = getChildCount(); if (mOrientation == Orientation.VERTICAL) { for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.childIndex = i; if (!lp.isDecor && lp.heightFactor == 0.f) { // 0 means requery the adapter for this, it doesn't have a valid width // . final ItemInfo ii = infoForChild(child); if (ii != null) { lp.heightFactor = ii.sizeFactor; lp.position = ii.position; } } } } else { for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.childIndex = i; if (!lp.isDecor && lp.widthFactor == 0.f) { // 0 means requery the adapter for this, it doesn't have a valid width. final ItemInfo ii = infoForChild(child); if (ii != null) { lp.widthFactor = ii.sizeFactor; lp.position = ii.position; } } } } sortChildDrawingOrder(); if (hasFocus()) { View currentFocused = findFocus(); ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; if (ii == null || ii.position != mCurItem) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { if (child.requestFocus(focusDirection)) { break; } } } } } } private void sortChildDrawingOrder() { if (mDrawingOrder != DRAW_ORDER_DEFAULT) { if (mDrawingOrderedChildren == null) { mDrawingOrderedChildren = new ArrayList(); } else { mDrawingOrderedChildren.clear(); } final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); mDrawingOrderedChildren.add(child); } Collections.sort(mDrawingOrderedChildren, sPositionComparator); } } private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { final int N = mAdapter.getCount(); final int size = getClientSize(); final float marginOffset = size > 0 ? (float) mPageMargin / size : 0; // Fix up offsets for later layout. if (oldCurInfo != null) { final int oldCurPosition = oldCurInfo.position; // Base offsets off of oldCurInfo. if (oldCurPosition < curItem.position) { int itemIndex = 0; ItemInfo ii = null; float offset = oldCurInfo.offset + oldCurInfo.sizeFactor + marginOffset; for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size(); pos++) { ii = mItems.get(itemIndex); while (pos > ii.position && itemIndex < mItems.size() - 1) { itemIndex++; ii = mItems.get(itemIndex); } while (pos < ii.position) { // We don't have an item populated for this, // ask the adapter for an offset. offset += mAdapter.getPageWidth(pos) + marginOffset; pos++; } ii.offset = offset; offset += ii.sizeFactor + marginOffset; } } else if (oldCurPosition > curItem.position) { int itemIndex = mItems.size() - 1; ItemInfo ii = null; float offset = oldCurInfo.offset; for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) { ii = mItems.get(itemIndex); while (pos < ii.position && itemIndex > 0) { itemIndex--; ii = mItems.get(itemIndex); } while (pos > ii.position) { // We don't have an item populated for this, // ask the adapter for an offset. offset -= mAdapter.getPageWidth(pos) + marginOffset; pos--; } offset -= ii.sizeFactor + marginOffset; ii.offset = offset; } } } // Base all offsets off of curItem. final int itemCount = mItems.size(); float offset = curItem.offset; int pos = curItem.position - 1; mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; mLastOffset = curItem.position == N - 1 ? curItem.offset + curItem.sizeFactor - 1 : Float.MAX_VALUE; // Previous pages for (int i = curIndex - 1; i >= 0; i--, pos--) { final ItemInfo ii = mItems.get(i); while (pos > ii.position) { offset -= mAdapter.getPageWidth(pos--) + marginOffset; } offset -= ii.sizeFactor + marginOffset; ii.offset = offset; if (ii.position == 0) mFirstOffset = offset; } offset = curItem.offset + curItem.sizeFactor + marginOffset; pos = curItem.position + 1; // Next pages for (int i = curIndex + 1; i < itemCount; i++, pos++) { final ItemInfo ii = mItems.get(i); while (pos < ii.position) { offset += mAdapter.getPageWidth(pos++) + marginOffset; } if (ii.position == N - 1) { mLastOffset = offset + ii.sizeFactor - 1; } ii.offset = offset; offset += ii.sizeFactor + marginOffset; } mNeedCalculatePageOffsets = false; } /** * This is the persistent state that is saved by ViewPager. Only needed * if you are creating a sublass of ViewPager that must save its own * state, in which case it should implement a subclass of this which * contains that state. */ public static class ViewPagerSavedState extends BaseSavedState { int position; Parcelable adapterState; ClassLoader loader; public ViewPagerSavedState(Parcelable superState) { super(superState); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(position); out.writeParcelable(adapterState, flags); } @Override public String toString() { return "FragmentPager.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " position=" + position + "}"; } public static final Parcelable.Creator CREATOR = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { @Override public ViewPagerSavedState createFromParcel(Parcel in, ClassLoader loader) { return new ViewPagerSavedState(in, loader); } @Override public ViewPagerSavedState[] newArray(int size) { return new ViewPagerSavedState[size]; } }); ViewPagerSavedState(Parcel in, ClassLoader loader) { super(in); if (loader == null) { loader = getClass().getClassLoader(); } position = in.readInt(); adapterState = in.readParcelable(loader); this.loader = loader; } } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); ViewPagerSavedState ss = new ViewPagerSavedState(superState); ss.position = mCurItem; if (mAdapter != null) { ss.adapterState = mAdapter.saveState(); } return ss; } @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof ViewPagerSavedState)) { super.onRestoreInstanceState(state); return; } ViewPagerSavedState ss = (ViewPagerSavedState) state; super.onRestoreInstanceState(ss.getSuperState()); if (mAdapter != null) { mAdapter.restoreState(ss.adapterState, ss.loader); setCurrentItemInternal(ss.position, false, true); } else { mRestoredCurItem = ss.position; mRestoredAdapterState = ss.adapterState; mRestoredClassLoader = ss.loader; } } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (!checkLayoutParams(params)) { params = generateLayoutParams(params); } final LayoutParams lp = (LayoutParams) params; lp.isDecor |= child instanceof Decor; if (mInLayout) { if (lp != null && lp.isDecor) { throw new IllegalStateException("Cannot add pager decor view during layout"); } lp.needsMeasure = true; addViewInLayout(child, index, params); } else { super.addView(child, index, params); } if (USE_CACHE) { if (child.getVisibility() != GONE) { child.setDrawingCacheEnabled(mScrollingCacheEnabled); } else { child.setDrawingCacheEnabled(false); } } } @Override public void removeView(View view) { if (mInLayout) { removeViewInLayout(view); } else { super.removeView(view); } } ItemInfo infoForChild(View child) { for (int i = 0; i < mItems.size(); i++) { ItemInfo ii = mItems.get(i); if (mAdapter.isViewFromObject(child, ii.object)) { return ii; } } return null; } ItemInfo infoForAnyChild(View child) { ViewParent parent; while ((parent = child.getParent()) != this) { if (parent == null || !(parent instanceof View)) { return null; } child = (View) parent; } return infoForChild(child); } ItemInfo infoForPosition(int position) { for (int i = 0; i < mItems.size(); i++) { ItemInfo ii = mItems.get(i); if (ii.position == position) { return ii; } } return null; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mFirstLayout = true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // For simple implementation, our internal size is always 0. // We depend on the container to specify the layout size of // our view. We can't really know what it is since we will be // adding and removing different arbitrary views and do not // want the layout to change as this happens. setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec)); final int measuredSize = (mOrientation == Orientation.VERTICAL) ? getMeasuredHeight() : getMeasuredWidth(); final int maxGutterSize = measuredSize / 10; mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); // Children are just made to fill our space. int childWidthSize; int childHeightSize; if (mOrientation == Orientation.VERTICAL) { childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); childHeightSize = measuredSize - getPaddingTop() - getPaddingBottom(); } else { childWidthSize = measuredSize - getPaddingLeft() - getPaddingRight(); childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); } /* * Make sure all children have been properly measured. Decor views first. * Right now we cheat and make this less complicated by assuming decor * views won't intersect. We will pin to edges based on gravity. */ int size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp != null && lp.isDecor) { final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; int widthMode = MeasureSpec.AT_MOST; int heightMode = MeasureSpec.AT_MOST; boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; if (consumeVertical) { widthMode = MeasureSpec.EXACTLY; } else if (consumeHorizontal) { heightMode = MeasureSpec.EXACTLY; } int widthSize = childWidthSize; int heightSize = childHeightSize; if (lp.width != LayoutParams.WRAP_CONTENT) { widthMode = MeasureSpec.EXACTLY; if (lp.width != LayoutParams.FILL_PARENT) { widthSize = lp.width; } } if (lp.height != LayoutParams.WRAP_CONTENT) { heightMode = MeasureSpec.EXACTLY; if (lp.height != LayoutParams.FILL_PARENT) { heightSize = lp.height; } } final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); child.measure(widthSpec, heightSpec); if (consumeVertical) { childHeightSize -= child.getMeasuredHeight(); } else if (consumeHorizontal) { childWidthSize -= child.getMeasuredWidth(); } } } } mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); // Make sure we have created all fragments that we need to have shown. mInLayout = true; populate(); mInLayout = false; // Page views next. size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { if (DEBUG) { Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec); } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp == null || !lp.isDecor) { if (mOrientation == Orientation.VERTICAL) { final int heightSpec = MeasureSpec.makeMeasureSpec( (int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY); child.measure(mChildWidthMeasureSpec, heightSpec); } else { final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); child.measure(widthSpec, mChildHeightMeasureSpec); } } } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Make sure scroll position is set correctly. if (mOrientation == Orientation.VERTICAL) { if (h != oldh) { recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin); } } else { if (w != oldw) { recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); } } } private void recomputeScrollPosition(int size, int oldSize, int margin, int oldMargin) { if (mOrientation == Orientation.VERTICAL) { if (oldSize > 0 && !mItems.isEmpty()) { final int heightWithMargin = size - getPaddingTop() - getPaddingBottom() + margin; final int oldHeightWithMargin = oldSize - getPaddingTop() - getPaddingBottom() + oldMargin; final int ypos = getScrollY(); final float pageOffset = (float) ypos / oldHeightWithMargin; final int newOffsetPixels = (int) (pageOffset * heightWithMargin); scrollTo(getScrollX(), newOffsetPixels); if (!mScroller.isFinished()) { // We now return to your regularly scheduled scroll, already in progress. final int newDuration = mScroller.getDuration() - mScroller.timePassed(); ItemInfo targetInfo = infoForPosition(mCurItem); mScroller.startScroll(0, newOffsetPixels, 0, (int) (targetInfo.offset * size), newDuration); } } else { final ItemInfo ii = infoForPosition(mCurItem); final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; final int scrollPos = (int) (scrollOffset * (size - getPaddingTop() - getPaddingBottom())); if (scrollPos != getScrollY()) { completeScroll(false); scrollTo(getScrollX(), scrollPos); } } } else { if (oldSize > 0 && !mItems.isEmpty()) { final int widthWithMargin = size - getPaddingLeft() - getPaddingRight() + margin; final int oldWidthWithMargin = oldSize - getPaddingLeft() - getPaddingRight() + oldMargin; final int xpos = getScrollX(); final float pageOffset = (float) xpos / oldWidthWithMargin; final int newOffsetPixels = (int) (pageOffset * widthWithMargin); scrollTo(newOffsetPixels, getScrollY()); if (!mScroller.isFinished()) { // We now return to your regularly scheduled scroll, already in progress. final int newDuration = mScroller.getDuration() - mScroller.timePassed(); ItemInfo targetInfo = infoForPosition(mCurItem); mScroller.startScroll(newOffsetPixels, 0, (int) (targetInfo.offset * size), 0, newDuration); } } else { final ItemInfo ii = infoForPosition(mCurItem); final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; final int scrollPos = (int) (scrollOffset * (size - getPaddingLeft() - getPaddingRight())); if (scrollPos != getScrollX()) { completeScroll(false); scrollTo(scrollPos, getScrollY()); } } } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int width = r - l; int height = b - t; int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); final int scroll = (mOrientation == Orientation.VERTICAL) ? getScrollY() : getScrollX(); int decorCount = 0; // First pass - decor views. We need to do this in two passes so that // we have the proper offsets for non-decor views later. for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childLeft = 0; int childTop = 0; if (lp.isDecor) { final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (hgrav) { default: childLeft = paddingLeft; break; case Gravity.LEFT: childLeft = paddingLeft; paddingLeft += child.getMeasuredWidth(); break; case Gravity.CENTER_HORIZONTAL: childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT: childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } switch (vgrav) { default: childTop = paddingTop; break; case Gravity.TOP: childTop = paddingTop; paddingTop += child.getMeasuredHeight(); break; case Gravity.CENTER_VERTICAL: childTop = Math.max((height - child.getMeasuredHeight()) / 2, paddingTop); break; case Gravity.BOTTOM: childTop = height - paddingBottom - child.getMeasuredHeight(); paddingBottom += child.getMeasuredHeight(); break; } if (mOrientation == Orientation.VERTICAL) { childTop += scroll; } else { childLeft += scroll; } child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); decorCount++; } } } final int childSize = (mOrientation == Orientation.VERTICAL) ? height - paddingTop - paddingBottom : width - paddingLeft - paddingRight; // Page views. Do this once we have the right padding offsets from above. for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); ItemInfo ii; if (!lp.isDecor && (ii = infoForChild(child)) != null) { int topLeftoff = (int) (childSize * ii.offset); int childLeft; int childTop; if (mOrientation == Orientation.VERTICAL) { childLeft = paddingLeft; childTop = paddingTop + topLeftoff; if (lp.needsMeasure) { // This was added during layout and needs measurement. // Do it now that we know what we're working with. lp.needsMeasure = false; final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (width - paddingLeft - paddingRight), MeasureSpec.EXACTLY); final int heightSpec = MeasureSpec.makeMeasureSpec( (int) (childSize * lp.heightFactor), MeasureSpec.EXACTLY); child.measure(widthSpec, heightSpec); } } else { childLeft = paddingLeft + topLeftoff; childTop = paddingTop; if (lp.needsMeasure) { // This was added during layout and needs measurement. // Do it now that we know what we're working with. lp.needsMeasure = false; final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (childSize * lp.widthFactor), MeasureSpec.EXACTLY); final int heightSpec = MeasureSpec.makeMeasureSpec( (int) (height - paddingTop - paddingBottom), MeasureSpec.EXACTLY); child.measure(widthSpec, heightSpec); } } if (DEBUG) { Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + "x" + child.getMeasuredHeight()); } child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); } } } mTopLeftPageBounds = (mOrientation == Orientation.VERTICAL) ? paddingLeft : paddingTop; mBottomRightPageBounds = (mOrientation == Orientation.VERTICAL) ? width - paddingRight : height - paddingBottom; mDecorChildCount = decorCount; if (mFirstLayout) { scrollToItem(mCurItem, false, 0, false); } mFirstLayout = false; } @Override public void computeScroll() { if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { scrollTo(x, y); if (mOrientation == Orientation.VERTICAL) { if (!pageScrolled(y)) { mScroller.abortAnimation(); scrollTo(x, 0); } } else { if (!pageScrolled(x)) { mScroller.abortAnimation(); scrollTo(0, y); } } } // Keep on drawing until the animation has finished. ViewCompat.postInvalidateOnAnimation(this); return; } // Done with scroll, clean up state. completeScroll(true); } private boolean pageScrolled(int pos) { if (mItems.size() == 0) { mCalledSuper = false; onPageScrolled(0, 0, 0); if (!mCalledSuper) { throw new IllegalStateException( "onPageScrolled did not call superclass implementation"); } return false; } final ItemInfo ii = infoForCurrentScrollPosition(); final int size = getClientSize(); final int sizeWithMargin = size + mPageMargin; final float marginOffset = (float) mPageMargin / size; final int currentPage = ii.position; final float pageOffset = (((float) pos / size) - ii.offset) / (ii.sizeFactor + marginOffset); final int offsetPixels = (int) (pageOffset * sizeWithMargin); mCalledSuper = false; onPageScrolled(currentPage, pageOffset, offsetPixels); if (!mCalledSuper) { throw new IllegalStateException( "onPageScrolled did not call superclass implementation"); } return true; } /** * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * If you override this method you must call through to the superclass implementation * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled * returns. * * @param position Position index of the first page currently being displayed. * Page position+1 will be visible if positionOffset is nonzero. * @param offset Value from [0, 1) indicating the offset from the page at position. * @param offsetPixels Value in pixels indicating the offset from position. */ protected void onPageScrolled(int position, float offset, int offsetPixels) { // Offset any decor views if needed - keep them on-screen at all times. if (mDecorChildCount > 0) { if (mOrientation == Orientation.VERTICAL) { final int scrollY = getScrollY(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); final int height = getHeight(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) continue; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; int childTop = 0; switch (vgrav) { default: childTop = paddingTop; break; case Gravity.TOP: childTop = paddingTop; paddingTop += child.getHeight(); break; case Gravity.CENTER_VERTICAL: childTop = Math.max((height - child.getMeasuredHeight()) / 2, paddingTop); break; case Gravity.BOTTOM: childTop = height - paddingBottom - child.getMeasuredHeight(); paddingBottom += child.getMeasuredHeight(); break; } childTop += scrollY; final int childOffset = childTop - child.getTop(); if (childOffset != 0) { child.offsetTopAndBottom(childOffset); } } } else { final int scrollX = getScrollX(); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); final int width = getWidth(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) continue; final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; int childLeft = 0; switch (hgrav) { default: childLeft = paddingLeft; break; case Gravity.LEFT: childLeft = paddingLeft; paddingLeft += child.getWidth(); break; case Gravity.CENTER_HORIZONTAL: childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT: childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } childLeft += scrollX; final int childOffset = childLeft - child.getLeft(); if (childOffset != 0) { child.offsetLeftAndRight(childOffset); } } } } if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); } if (mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); } if (mPageTransformer != null) { final int scroll = (mOrientation == Orientation.VERTICAL) ? getScrollY() : getScrollX(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.isDecor) continue; final float transformPos = (float) (((mOrientation == Orientation.VERTICAL) ? child.getTop() : child.getLeft()) - scroll) / getClientSize(); mPageTransformer.transformPage(child, transformPos); } } mCalledSuper = true; } private void completeScroll(boolean postEvents) { boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; if (needPopulate) { // Done with scroll, no longer want to cache view drawing. setScrollingCacheEnabled(false); mScroller.abortAnimation(); int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { scrollTo(x, y); } } mPopulatePending = false; for (int i = 0; i < mItems.size(); i++) { ItemInfo ii = mItems.get(i); if (ii.scrolling) { needPopulate = true; ii.scrolling = false; } } if (needPopulate) { if (postEvents) { ViewCompat.postOnAnimation(this, mEndScrollRunnable); } else { mEndScrollRunnable.run(); } } } private boolean isGutterDrag(float axis, float dAxis) { return (axis < mGutterSize && dAxis > 0) || (axis > (mOrientation == Orientation.VERTICAL ? getHeight() : getWidth()) - mGutterSize && dAxis < 0); } private void enableLayers(boolean enable) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final int layerType = enable ? ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; ViewCompat.setLayerType(getChildAt(i), layerType, null); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; // Always take care of the touch gesture being complete. if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the drag. if (DEBUG) Log.v(TAG, "Intercept done!"); mIsBeingDragged = false; mIsUnableToDrag = false; mActivePointerId = INVALID_POINTER; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } return false; } // Nothing more to do here if we have decided whether or not we // are dragging. if (action != MotionEvent.ACTION_DOWN) { if (mIsBeingDragged) { if (DEBUG) Log.v(TAG, "Intercept returning true!"); return true; } if (mIsUnableToDrag) { if (DEBUG) Log.v(TAG, "Intercept returning false!"); return false; } } switch (action) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); if (mOrientation == Orientation.VERTICAL) { final float y = MotionEventCompat.getY(ev, pointerIndex); final float dy = y - mLastMotionY; final float yDiff = Math.abs(dy); final float x = MotionEventCompat.getX(ev, pointerIndex); final float xDiff = Math.abs(x - mInitialMotionX); if (DEBUG) { Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); } if (dy != 0 && !isGutterDrag(mLastMotionY, dy) && canScroll(this, false, (int) dy, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } if (yDiff > mTouchSlop && yDiff * 0.5f > xDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop : mInitialMotionY - mTouchSlop; mLastMotionX = x; setScrollingCacheEnabled(true); } else if (xDiff > mTouchSlop) { // The finger has moved enough in the vertical // direction to be counted as a drag... abort // any attempt to drag horizontally, to work correctly // with children that have scrolling containers. if (DEBUG) Log.v(TAG, "Starting unable to drag!"); mIsUnableToDrag = true; } if (mIsBeingDragged) { // Scroll to follow the motion event if (performDrag(y)) { ViewCompat.postInvalidateOnAnimation(this); } } } else { final float x = MotionEventCompat.getX(ev, pointerIndex); final float dx = x - mLastMotionX; final float xDiff = Math.abs(dx); final float y = MotionEventCompat.getY(ev, pointerIndex); final float yDiff = Math.abs(y - mInitialMotionY); if (DEBUG) { Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); } if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) { // The finger has moved enough in the vertical // direction to be counted as a drag... abort // any attempt to drag horizontally, to work correctly // with children that have scrolling containers. if (DEBUG) Log.v(TAG, "Starting unable to drag!"); mIsUnableToDrag = true; } if (mIsBeingDragged) { // Scroll to follow the motion event if (performDrag(x)) { ViewCompat.postInvalidateOnAnimation(this); } } } break; } case MotionEvent.ACTION_DOWN: { /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsUnableToDrag = false; mScroller.computeScrollOffset(); if (mOrientation == Orientation.VERTICAL) { if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > mCloseEnough) { // Let the user 'catch' the pager as it animates. mScroller.abortAnimation(); mPopulatePending = false; populate(); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } else { completeScroll(false); mIsBeingDragged = false; } } else { if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { // Let the user 'catch' the pager as it animates. mScroller.abortAnimation(); mPopulatePending = false; populate(); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } else { completeScroll(false); mIsBeingDragged = false; } } if (DEBUG) { Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + " mIsBeingDragged=" + mIsBeingDragged + "mIsUnableToDrag=" + mIsUnableToDrag); } break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; } @Override public boolean onTouchEvent(MotionEvent ev) { if (mFakeDragging) { // A fake drag is in progress already, ignore this real one // but still eat the touch events. // (It is likely that the user is multi-touching the screen.) return true; } if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { // Don't handle edge touches immediately -- they may actually belong to one of our // descendants. return false; } if (mAdapter == null || mAdapter.getCount() == 0) { // Nothing to present or scroll; nothing to touch. return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); final int action = ev.getAction(); boolean needsInvalidate = false; switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { mScroller.abortAnimation(); mPopulatePending = false; populate(); // Remember where the motion event started mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); break; } case MotionEvent.ACTION_MOVE: if (!mIsBeingDragged) { final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float y = MotionEventCompat.getY(ev, pointerIndex); final float yDiff = Math.abs(y - mLastMotionY); final float x = MotionEventCompat.getX(ev, pointerIndex); final float xDiff = Math.abs(x - mLastMotionX); if (DEBUG) { Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); } if (mOrientation == Orientation.VERTICAL) { if (yDiff > mTouchSlop && yDiff > xDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop : mInitialMotionY - mTouchSlop; mLastMotionX = x; setScrollState(SCROLL_STATE_DRAGGING); setScrollingCacheEnabled(true); // Disallow Parent Intercept, just in case ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } else { if (xDiff > mTouchSlop && xDiff > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollState(SCROLL_STATE_DRAGGING); setScrollingCacheEnabled(true); // Disallow Parent Intercept, just in case ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } } // Not else! Note that mIsBeingDragged can be set above. if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = MotionEventCompat.findPointerIndex( ev, mActivePointerId); if (mOrientation == Orientation.VERTICAL) { final float y = MotionEventCompat.getY(ev, activePointerIndex); needsInvalidate |= performDrag(y); } else { final float x = MotionEventCompat.getX(ev, activePointerIndex); needsInvalidate |= performDrag(x); } } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { int currentPage; int initialVelocity; int totalDelta; float pageOffset; if (mOrientation == Orientation.VERTICAL) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); initialVelocity = (int) VelocityTrackerCompat.getYVelocity( velocityTracker, mActivePointerId); mPopulatePending = true; final int height = getClientSize(); final int scrollY = getScrollY(); final ItemInfo ii = infoForCurrentScrollPosition(); currentPage = ii.position; pageOffset = (((float) scrollY / height) - ii.offset) / ii.sizeFactor; final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float y = MotionEventCompat.getY(ev, activePointerIndex); totalDelta = (int) (y - mInitialMotionY); } else { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); initialVelocity = (int) VelocityTrackerCompat.getXVelocity( velocityTracker, mActivePointerId); mPopulatePending = true; final int width = getClientSize(); final int scrollX = getScrollX(); final ItemInfo ii = infoForCurrentScrollPosition(); currentPage = ii.position; pageOffset = (((float) scrollX / width) - ii.offset) / ii.sizeFactor; final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, activePointerIndex); totalDelta = (int) (x - mInitialMotionX); } int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); setCurrentItemInternal(nextPage, true, true, initialVelocity); mActivePointerId = INVALID_POINTER; endDrag(); needsInvalidate = mTopLeftEdge.onRelease() | mRightBottomEdge.onRelease(); } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged) { scrollToItem(mCurItem, true, 0, false); mActivePointerId = INVALID_POINTER; endDrag(); needsInvalidate = mTopLeftEdge.onRelease() | mRightBottomEdge.onRelease(); } break; case MotionEventCompat.ACTION_POINTER_DOWN: { int index; if (mOrientation == Orientation.VERTICAL) { index = MotionEventCompat.getActionIndex(ev); final float y = MotionEventCompat.getY(ev, index); mLastMotionY = y; } else { index = MotionEventCompat.getActionIndex(ev); final float x = MotionEventCompat.getX(ev, index); mLastMotionX = x; } mActivePointerId = MotionEventCompat.getPointerId(ev, index); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); if (mOrientation == Orientation.VERTICAL) { mLastMotionY = MotionEventCompat.getY(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); } else { mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); } break; } if (needsInvalidate) { ViewCompat.postInvalidateOnAnimation(this); } return true; } private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(disallowIntercept); } } private boolean performDrag(float dimen) { boolean needsInvalidate = false; if (mOrientation == Orientation.VERTICAL) { float y = dimen; final float deltaY = mLastMotionY - y; mLastMotionY = y; float oldScrollY = getScrollY(); float scrollY = oldScrollY + deltaY; final int height = getClientSize(); float topBound = height * mFirstOffset; float bottomBound = height * mLastOffset; boolean topAbsolute = true; boolean bottomAbsolute = true; final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); if (firstItem.position != 0) { topAbsolute = false; topBound = firstItem.offset * height; } if (lastItem.position != mAdapter.getCount() - 1) { bottomAbsolute = false; bottomBound = lastItem.offset * height; } if (scrollY < topBound) { if (topAbsolute) { float over = topBound - scrollY; needsInvalidate = mTopLeftEdge.onPull(Math.abs(over) / height); } scrollY = topBound; } else if (scrollY > bottomBound) { if (bottomAbsolute) { float over = scrollY - bottomBound; needsInvalidate = mRightBottomEdge.onPull(Math.abs(over) / height); } scrollY = bottomBound; } // Don't lose the rounded component mLastMotionX += scrollY - (int) scrollY; scrollTo(getScrollX(), (int) scrollY); pageScrolled((int) scrollY); } else { float x = dimen; final float deltaX = mLastMotionX - x; mLastMotionX = x; float oldScrollX = getScrollX(); float scrollX = oldScrollX + deltaX; final int width = getClientSize(); float leftBound = width * mFirstOffset; float rightBound = width * mLastOffset; boolean leftAbsolute = true; boolean rightAbsolute = true; final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); if (firstItem.position != 0) { leftAbsolute = false; leftBound = firstItem.offset * width; } if (lastItem.position != mAdapter.getCount() - 1) { rightAbsolute = false; rightBound = lastItem.offset * width; } if (scrollX < leftBound) { if (leftAbsolute) { float over = leftBound - scrollX; needsInvalidate = mTopLeftEdge.onPull(Math.abs(over) / width); } scrollX = leftBound; } else if (scrollX > rightBound) { if (rightAbsolute) { float over = scrollX - rightBound; needsInvalidate = mRightBottomEdge.onPull(Math.abs(over) / width); } scrollX = rightBound; } // Don't lose the rounded component mLastMotionX += scrollX - (int) scrollX; scrollTo((int) scrollX, getScrollY()); pageScrolled((int) scrollX); } return needsInvalidate; } /** * @return Info about the page at the current scroll position. * This can be synthetic for a missing middle page; the 'object' field can be null. */ private ItemInfo infoForCurrentScrollPosition() { final int size = getClientSize(); final float scrollOffset = size > 0 ? (float) ((mOrientation == Orientation.VERTICAL) ? getScrollY() : getScrollX()) / size : 0; final float marginOffset = size > 0 ? (float) mPageMargin / size : 0; int lastPos = -1; float lastOffset = 0.f; float lastSize = 0.f; boolean first = true; ItemInfo lastItem = null; for (int i = 0; i < mItems.size(); i++) { ItemInfo ii = mItems.get(i); float offset; if (!first && ii.position != lastPos + 1) { // Create a synthetic item for a missing page. ii = mTempItem; ii.offset = lastOffset + lastSize + marginOffset; ii.position = lastPos + 1; ii.sizeFactor = mAdapter.getPageWidth(ii.position); i--; } offset = ii.offset; final float topLeftBound = offset; final float bottomRightBound = offset + ii.sizeFactor + marginOffset; if (first || scrollOffset >= topLeftBound) { if (scrollOffset < bottomRightBound || i == mItems.size() - 1) { return ii; } } else { return lastItem; } first = false; lastPos = ii.position; lastOffset = offset; lastSize = ii.sizeFactor; lastItem = ii; } return lastItem; } private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaDimen) { int targetPage; if (Math.abs(deltaDimen) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { targetPage = velocity > 0 ? currentPage : currentPage + 1; } else { final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; targetPage = (int) (currentPage + pageOffset + truncator); } if (mItems.size() > 0) { final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); // Only let the user target pages we have items for targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); } return targetPage; } @Override public void draw(Canvas canvas) { super.draw(canvas); boolean needsInvalidate = false; final int overScrollMode = ViewCompat.getOverScrollMode(this); if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && mAdapter != null && mAdapter.getCount() > 1)) { if (mOrientation == Orientation.VERTICAL) { if (!mTopLeftEdge.isFinished()) { final int restoreCount = canvas.save(); final int height = getHeight(); final int width = getWidth() - getPaddingLeft() - getPaddingRight(); canvas.translate(getPaddingLeft(), mFirstOffset * height); mTopLeftEdge.setSize(width, height); needsInvalidate |= mTopLeftEdge.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mRightBottomEdge.isFinished()) { final int restoreCount = canvas.save(); final int height = getHeight(); final int width = getWidth() - getPaddingLeft() - getPaddingRight(); canvas.rotate(180); canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height); mRightBottomEdge.setSize(width, height); needsInvalidate |= mRightBottomEdge.draw(canvas); canvas.restoreToCount(restoreCount); } } else { if (!mTopLeftEdge.isFinished()) { final int restoreCount = canvas.save(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); final int width = getWidth(); canvas.rotate(270); canvas.translate(-height + getPaddingTop(), mFirstOffset * width); mTopLeftEdge.setSize(height, width); needsInvalidate |= mTopLeftEdge.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mRightBottomEdge.isFinished()) { final int restoreCount = canvas.save(); final int width = getWidth(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); canvas.rotate(90); canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); mRightBottomEdge.setSize(height, width); needsInvalidate |= mRightBottomEdge.draw(canvas); canvas.restoreToCount(restoreCount); } } } else { mTopLeftEdge.finish(); mRightBottomEdge.finish(); } if (needsInvalidate) { // Keep animating ViewCompat.postInvalidateOnAnimation(this); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the margin drawable between pages if needed. if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { if (mOrientation == Orientation.VERTICAL) { final int scrollY = getScrollY(); final int height = getHeight(); final float marginOffset = (float) mPageMargin / height; int itemIndex = 0; ItemInfo ii = mItems.get(0); float offset = ii.offset; final int itemCount = mItems.size(); final int firstPos = ii.position; final int lastPos = mItems.get(itemCount - 1).position; for (int pos = firstPos; pos < lastPos; pos++) { while (pos > ii.position && itemIndex < itemCount) { ii = mItems.get(++itemIndex); } float drawAt; if (pos == ii.position) { drawAt = (ii.offset + ii.sizeFactor) * height; offset = ii.offset + ii.sizeFactor + marginOffset; } else { float heightFactor = mAdapter.getPageWidth(pos); drawAt = (offset + heightFactor) * height; offset += heightFactor + marginOffset; } if (drawAt + mPageMargin > scrollY) { mMarginDrawable.setBounds(mTopLeftPageBounds, (int) drawAt, mBottomRightPageBounds, (int) (drawAt + mPageMargin + 0.5f)); mMarginDrawable.draw(canvas); } if (drawAt > scrollY + height) { break; // No more visible, no sense in continuing } } } else { final int scrollX = getScrollX(); final int width = getWidth(); final float marginOffset = (float) mPageMargin / width; int itemIndex = 0; ItemInfo ii = mItems.get(0); float offset = ii.offset; final int itemCount = mItems.size(); final int firstPos = ii.position; final int lastPos = mItems.get(itemCount - 1).position; for (int pos = firstPos; pos < lastPos; pos++) { while (pos > ii.position && itemIndex < itemCount) { ii = mItems.get(++itemIndex); } float drawAt; if (pos == ii.position) { drawAt = (ii.offset + ii.sizeFactor) * width; offset = ii.offset + ii.sizeFactor + marginOffset; } else { float widthFactor = mAdapter.getPageWidth(pos); drawAt = (offset + widthFactor) * width; offset += widthFactor + marginOffset; } if (drawAt + mPageMargin > scrollX) { mMarginDrawable.setBounds((int) drawAt, mTopLeftPageBounds, (int) (drawAt + mPageMargin + 0.5f), mBottomRightPageBounds); mMarginDrawable.draw(canvas); } if (drawAt > scrollX + width) { break; // No more visible, no sense in continuing } } } } } /** * Start a fake drag of the pager. *

*

A fake drag can be useful if you want to synchronize the motion of the ViewPager * with the touch scrolling of another view, while still letting the ViewPager * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. *

*

During a fake drag the ViewPager will ignore all touch events. If a real drag * is already in progress, this method will return false. * * @return true if the fake drag began successfully, false if it could not be started. * @see #fakeDragBy(float) * @see #endFakeDrag() */ public boolean beginFakeDrag() { if (mIsBeingDragged) { return false; } mFakeDragging = true; setScrollState(SCROLL_STATE_DRAGGING); if (mOrientation == Orientation.VERTICAL) { mInitialMotionY = mLastMotionY = 0; } else { mInitialMotionX = mLastMotionX = 0; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } final long time = SystemClock.uptimeMillis(); final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); mVelocityTracker.addMovement(ev); ev.recycle(); mFakeDragBeginTime = time; return true; } /** * End a fake drag of the pager. * * @see #beginFakeDrag() * @see #fakeDragBy(float) */ public void endFakeDrag() { if (!mFakeDragging) { throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); } final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); if (mOrientation == Orientation.VERTICAL) { int initialVelocity = (int) VelocityTrackerCompat.getYVelocity( velocityTracker, mActivePointerId); mPopulatePending = true; final int size = getClientSize(); final int scrollY = getScrollY(); final ItemInfo ii = infoForCurrentScrollPosition(); final int currentPage = ii.position; final float pageOffset = (((float) scrollY / size) - ii.offset) / ii.sizeFactor; final int totalDelta = (int) (mLastMotionY - mInitialMotionY); int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); setCurrentItemInternal(nextPage, true, true, initialVelocity); } else { int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( velocityTracker, mActivePointerId); mPopulatePending = true; final int size = getClientSize(); final int scrollX = getScrollX(); final ItemInfo ii = infoForCurrentScrollPosition(); final int currentPage = ii.position; final float pageOffset = (((float) scrollX / size) - ii.offset) / ii.sizeFactor; final int totalDelta = (int) (mLastMotionX - mInitialMotionX); int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); setCurrentItemInternal(nextPage, true, true, initialVelocity); } endDrag(); mFakeDragging = false; } /** * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. * * @param offset Offset in pixels to drag by. * @see #beginFakeDrag() * @see #endFakeDrag() */ public void fakeDragBy(float offset) { if (!mFakeDragging) { throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); } if (mOrientation == Orientation.VERTICAL) { mLastMotionY += offset; float oldScrollY = getScrollY(); float scrollY = oldScrollY - offset; final int height = getClientSize(); float topBound = height * mFirstOffset; float bottomBound = height * mLastOffset; final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); if (firstItem.position != 0) { topBound = firstItem.offset * height; } if (lastItem.position != mAdapter.getCount() - 1) { bottomBound = lastItem.offset * height; } if (scrollY < topBound) { scrollY = topBound; } else if (scrollY > bottomBound) { scrollY = bottomBound; } // Don't lose the rounded component mLastMotionY += scrollY - (int) scrollY; scrollTo(getScrollX(), (int) scrollY); pageScrolled((int) scrollY); // Synthesize an event for the VelocityTracker. final long time = SystemClock.uptimeMillis(); final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 0, mLastMotionY, 0); mVelocityTracker.addMovement(ev); ev.recycle(); } else { mLastMotionX += offset; float oldScrollX = getScrollX(); float scrollX = oldScrollX - offset; final int width = getClientSize(); float leftBound = width * mFirstOffset; float rightBound = width * mLastOffset; final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); if (firstItem.position != 0) { leftBound = firstItem.offset * width; } if (lastItem.position != mAdapter.getCount() - 1) { rightBound = lastItem.offset * width; } if (scrollX < leftBound) { scrollX = leftBound; } else if (scrollX > rightBound) { scrollX = rightBound; } // Don't lose the rounded component mLastMotionX += scrollX - (int) scrollX; scrollTo((int) scrollX, getScrollY()); pageScrolled((int) scrollX); // Synthesize an event for the VelocityTracker. final long time = SystemClock.uptimeMillis(); final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, mLastMotionX, 0, 0); mVelocityTracker.addMovement(ev); ev.recycle(); } } /** * Returns true if a fake drag is in progress. * * @return true if currently in a fake drag, false otherwise. * @see #beginFakeDrag() * @see #fakeDragBy(float) * @see #endFakeDrag() */ public boolean isFakeDragging() { return mFakeDragging; } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; if (mOrientation == Orientation.VERTICAL) { mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex); } else { mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); } mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); } } } private void endDrag() { mIsBeingDragged = false; mIsUnableToDrag = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } private void setScrollingCacheEnabled(boolean enabled) { if (mScrollingCacheEnabled != enabled) { mScrollingCacheEnabled = enabled; if (USE_CACHE) { final int size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { child.setDrawingCacheEnabled(enabled); } } } } } public boolean internalCanScrollVertically(int direction) { if (mAdapter == null) { return false; } final int size = getClientSize(); final int scroll = (mOrientation == Orientation.VERTICAL) ? getScrollY() : getScrollX(); if (direction < 0) { return (scroll > (int) (size * mFirstOffset)); } else if (direction > 0) { return (scroll < (int) (size * mLastOffset)); } else { return false; } } /** * Tests scrollability within child views of v given a delta of dx. * * @param v View to test for horizontal scrollability * @param checkV Whether the view v passed should itself be checked for scrollability (true), * or just its children (false). * @param delta Delta scrolled in pixels * @param x X coordinate of the active touch point * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ protected boolean canScroll(View v, boolean checkV, int delta, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { // TODO: Add versioned support here for transformed views. // This will not work for transformed views in Honeycomb+ final View child = group.getChildAt(i); if (mOrientation == Orientation.VERTICAL) { if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && canScroll(child, true, delta, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } else { if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, delta, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } } return checkV && ViewCompat.canScrollVertically(v, -delta); } @Override public boolean dispatchKeyEvent(KeyEvent event) { // Let the focused view and/or our descendants get the key first return super.dispatchKeyEvent(event) || executeKeyEvent(event); } /** * You can call this function yourself to have the scroll view perform * scrolling from a key event, just as if the event had been dispatched to * it by the view hierarchy. * * @param event The key event to execute. * @return Return true if the event was handled, else false. */ public boolean executeKeyEvent(KeyEvent event) { boolean handled = false; if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_LEFT: handled = arrowScroll(FOCUS_LEFT); break; case KeyEvent.KEYCODE_DPAD_RIGHT: handled = arrowScroll(FOCUS_RIGHT); break; case KeyEvent.KEYCODE_TAB: if (Build.VERSION.SDK_INT >= 11) { // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD // before Android 3.0. Ignore the tab key on those devices. if (event.hasNoModifiers()) { handled = arrowScroll(FOCUS_FORWARD); } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { handled = arrowScroll(FOCUS_BACKWARD); } } break; } } return handled; } public boolean arrowScroll(int direction) { View currentFocused = findFocus(); if (currentFocused == this) { currentFocused = null; } else if (currentFocused != null) { boolean isChild = false; for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; parent = parent.getParent()) { if (parent == this) { isChild = true; break; } } if (!isChild) { // This would cause the focus search down below to fail in fun ways. final StringBuilder sb = new StringBuilder(); sb.append(currentFocused.getClass().getSimpleName()); for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; parent = parent.getParent()) { sb.append(" => ").append(parent.getClass().getSimpleName()); } Log.e(TAG, "arrowScroll tried to find focus based on non-child " + "current focused view " + sb.toString()); currentFocused = null; } } boolean handled = false; View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); if (nextFocused != null && nextFocused != currentFocused) { if (direction == View.FOCUS_UP) { // If there is nothing to the left, or this is causing us to // jump to the right, then what we really want to do is page left. if (mOrientation == Orientation.VERTICAL) { final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top; final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top; if (currentFocused != null && nextTop >= currTop) { handled = pageBack(); } else { handled = nextFocused.requestFocus(); } } else { final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; if (currentFocused != null && nextLeft >= currLeft) { handled = pageBack(); } else { handled = nextFocused.requestFocus(); } } } else if (direction == View.FOCUS_DOWN) { // If there is nothing to the right, or this is causing us to // jump to the left, then what we really want to do is page right. if (mOrientation == Orientation.VERTICAL) { final int nextDown = getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom; final int currDown = getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom; if (currentFocused != null && nextDown <= currDown) { handled = pageForward(); } else { handled = nextFocused.requestFocus(); } } else { final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; if (currentFocused != null && nextLeft <= currLeft) { handled = pageForward(); } else { handled = nextFocused.requestFocus(); } } } } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) { // Trying to move left and nothing there; try to page. handled = pageBack(); } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) { // Trying to move right and nothing there; try to page. handled = pageForward(); } if (handled) { playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); } return handled; } private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { if (outRect == null) { outRect = new Rect(); } if (child == null) { outRect.set(0, 0, 0, 0); return outRect; } outRect.left = child.getLeft(); outRect.right = child.getRight(); outRect.top = child.getTop(); outRect.bottom = child.getBottom(); ViewParent parent = child.getParent(); while (parent instanceof ViewGroup && parent != this) { final ViewGroup group = (ViewGroup) parent; outRect.left += group.getLeft(); outRect.right += group.getRight(); outRect.top += group.getTop(); outRect.bottom += group.getBottom(); parent = group.getParent(); } return outRect; } boolean pageBack() { if (mCurItem > 0) { setCurrentItem(mCurItem - 1, true); return true; } return false; } boolean pageForward() { if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) { setCurrentItem(mCurItem + 1, true); return true; } return false; } /** * We only want the current page that is being shown to be focusable. */ @Override public void addFocusables(ArrayList views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { child.addFocusables(views, direction, focusableMode); } } } } // we add ourselves (if focusable) in all cases except for when we are // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is // to avoid the focus search finding layouts when a more precise search // among the focusable children would be more interesting. if ( descendantFocusability != FOCUS_AFTER_DESCENDANTS || // No focusable descendants (focusableCount == views.size())) { // Note that we can't call the superclass here, because it will // add all views in. So we need to do the same thing View does. if (!isFocusable()) { return; } if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && isInTouchMode() && !isFocusableInTouchMode()) { return; } if (views != null) { views.add(this); } } } /** * We only want the current page that is being shown to be touchable. */ @Override public void addTouchables(ArrayList views) { // Note that we don't call super.addTouchables(), which means that // we don't call View.addTouchables(). This is okay because a ViewPager // is itself not touchable. for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { child.addTouchables(views); } } } } /** * We only want the current page that is being shown to be focusable. */ @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { int index; int increment; int end; int count = getChildCount(); if ((direction & FOCUS_FORWARD) != 0) { index = 0; increment = 1; end = count; } else { index = count - 1; increment = -1; end = -1; } for (int i = index; i != end; i += increment) { View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { if (child.requestFocus(direction, previouslyFocusedRect)) { return true; } } } } return false; } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { // Dispatch scroll events from this ViewPager. if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { return super.dispatchPopulateAccessibilityEvent(event); } // Dispatch all other accessibility events from the current page. final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { final ItemInfo ii = infoForChild(child); if (ii != null && ii.position == mCurItem && child.dispatchPopulateAccessibilityEvent(event)) { return true; } } } return false; } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return generateDefaultLayoutParams(); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams && super.checkLayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } class MyAccessibilityDelegate extends AccessibilityDelegateCompat { @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); event.setClassName(ViewPager.class.getName()); final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain(); recordCompat.setScrollable(canScroll()); if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED && mAdapter != null) { recordCompat.setItemCount(mAdapter.getCount()); recordCompat.setFromIndex(mCurItem); recordCompat.setToIndex(mCurItem); } } @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); info.setClassName(ViewPager.class.getName()); info.setScrollable(canScroll()); if (mOrientation == Orientation.VERTICAL) { if (internalCanScrollVertically(1)) { info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); } if (internalCanScrollVertically(-1)) { info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); } } else { if (canScrollHorizontally(1)) { info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); } if (canScrollHorizontally(-1)) { info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); } } } @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (super.performAccessibilityAction(host, action, args)) { return true; } switch (action) { case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { if ((mOrientation == Orientation.VERTICAL && internalCanScrollVertically(1)) || (mOrientation == Orientation.HORIZONTAL && canScrollHorizontally(1))) { setCurrentItem(mCurItem + 1); return true; } } return false; case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { if ((mOrientation == Orientation.VERTICAL && internalCanScrollVertically(-1)) || (mOrientation == Orientation.HORIZONTAL && canScrollHorizontally(-1))) { setCurrentItem(mCurItem - 1); return true; } } return false; } return false; } private boolean canScroll() { return (mAdapter != null) && (mAdapter.getCount() > 1); } } private class PagerObserver extends DataSetObserver { @Override public void onChanged() { dataSetChanged(); } @Override public void onInvalidated() { dataSetChanged(); } } /** * Layout parameters that should be supplied for views added to a * ViewPager. */ public static class LayoutParams extends ViewGroup.LayoutParams { /** * true if this view is a decoration on the pager itself and not * a view supplied by the adapter. */ public boolean isDecor; /** * Gravity setting for use on decor views only: * Where to position the view page within the overall ViewPager * container; constants are defined in {@link android.view.Gravity}. */ public int gravity; /** * Width as a 0-1 multiplier of the measured pager height */ float heightFactor = 0.f; /** * Width as a 0-1 multiplier of the measured pager width */ float widthFactor = 0.f; /** * true if this view was added during layout and needs to be measured * before being positioned. */ boolean needsMeasure; /** * Adapter position this view is for if !isDecor */ int position; /** * Current child index within the ViewPager that this view occupies */ int childIndex; public LayoutParams() { super(FILL_PARENT, FILL_PARENT); } public LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); gravity = a.getInteger(0, Gravity.TOP); a.recycle(); } } static class ViewPositionComparator implements Comparator { @Override public int compare(View lhs, View rhs) { final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); if (llp.isDecor != rlp.isDecor) { return llp.isDecor ? 1 : -1; } return llp.position - rlp.position; } } // Following classes and the interface are needed for the Maven Central upload script to work properly. // They are being introduced here, sort of temporarily (until I find a better solution for this issue). /** * Callbacks a {@link Parcelable} creator should implement. */ public interface ParcelableCompatCreatorCallbacks { /** * Create a new instance of the Parcelable class, instantiating it * from the given Parcel whose data had previously been written by * {@link Parcelable#writeToParcel Parcelable.writeToParcel()} and * using the given ClassLoader. * * @param in The Parcel to read the object's data from. * @param loader The ClassLoader that this object is being created in. * @return Returns a new instance of the Parcelable class. */ public T createFromParcel(Parcel in, ClassLoader loader); /** * Create a new array of the Parcelable class. * * @param size Size of the array. * @return Returns an array of the Parcelable class, with every entry * initialized to null. */ public T[] newArray(int size); } /** * Helper for accessing features in {@link android.os.Parcelable} * introduced after API level 4 in a backwards compatible fashion. */ public static class ParcelableCompat { /** * Factory method for {@link Parcelable.Creator}. * * @param callbacks Creator callbacks implementation. * @return New creator. */ public static Parcelable.Creator newCreator( OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) { if (android.os.Build.VERSION.SDK_INT >= 13) { return ParcelableCompatCreatorHoneycombMR2Stub.instantiate(callbacks); } return new CompatCreator(callbacks); } public static class CompatCreator implements Parcelable.Creator { final OrientedViewPager.ParcelableCompatCreatorCallbacks mCallbacks; public CompatCreator(OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) { mCallbacks = callbacks; } @Override public T createFromParcel(Parcel source) { return mCallbacks.createFromParcel(source, null); } @Override public T[] newArray(int size) { return mCallbacks.newArray(size); } } } static class ParcelableCompatCreatorHoneycombMR2Stub { public static Parcelable.Creator instantiate( OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) { return new ParcelableCompatCreatorHoneycombMR2(callbacks); } } static class ParcelableCompatCreatorHoneycombMR2 implements Parcelable.ClassLoaderCreator { private final OrientedViewPager.ParcelableCompatCreatorCallbacks mCallbacks; public ParcelableCompatCreatorHoneycombMR2( OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) { mCallbacks = callbacks; } public T createFromParcel(Parcel in) { return mCallbacks.createFromParcel(in, null); } public T createFromParcel(Parcel in, ClassLoader loader) { return mCallbacks.createFromParcel(in, loader); } public T[] newArray(int size) { return mCallbacks.newArray(size); } } } ================================================ FILE: library/src/main/java/com/gu/library/transformer/VerticalBaseTransformer.java ================================================ package com.gu.library.transformer; import android.support.v4.view.ViewPager; import android.view.View; /** * Created by Nate on 2016/7/22. */ public abstract class VerticalBaseTransformer implements ViewPager.PageTransformer { /** * Called each {@link #transformPage(View, float)}. * * @param page Apply the transformation to this page * @param position Position of page relative to the current front-and-center position of the pager. 0 is front and * center. 1 is one full page position to the right, and -1 is one page position to the left. */ protected abstract void onTransform(View page, float position); /** * Apply a property transformation to the given page. For most use cases, this method should not be overridden. * Instead use {@link #transformPage(View, float)} to perform typical transformations. * * @param page Apply the transformation to this page * @param position Position of page relative to the current front-and-center position of the pager. 0 is front and * center. 1 is one full page position to the right, and -1 is one page position to the left. */ @Override public void transformPage(View page, float position) { onPreTransform(page, position); onTransform(page, position); onPostTransform(page, position); } /** * If the position offset of a fragment is less than negative one or greater than one, returning true will set the * fragment alpha to 0f. Otherwise fragment alpha is always defaulted to 1f. * * @return */ protected boolean hideOffscreenPages() { return true; } /** * Indicates if the default animations of the view pager should be used. * * @return */ protected boolean isPagingEnabled() { return false; } /** * Called each {@link #transformPage(View, float)} before {{@link #onTransform(View, float)}. *

* The default implementation attempts to reset all view properties. This is useful when toggling transforms that do * not modify the same page properties. For instance changing from a transformation that applies rotation to a * transformation that fades can inadvertently leave a fragment stuck with a rotation or with some degree of applied * alpha. * * @param page Apply the transformation to this page * @param position Position of page relative to the current front-and-center position of the pager. 0 is front and * center. 1 is one full page position to the right, and -1 is one page position to the left. */ protected void onPreTransform(View page, float position) { final float width = page.getWidth(); final float height = page.getHeight(); page.setRotationX(0); page.setRotationY(0); page.setRotation(0); page.setScaleX(1); page.setScaleY(1); page.setPivotX(0); page.setPivotY(0); page.setTranslationX(0); page.setTranslationY(isPagingEnabled() ? 0f : -height * position); if (hideOffscreenPages()) { page.setAlpha(position <= -1f || position >= 1f ? 0f : 1f); } else { page.setAlpha(1f); } /*final float normalizedposition = Math.abs(Math.abs(position) - 1); page.setAlpha(normalizedposition);*/ } /** * Called each {@link #transformPage(View, float)} after {@link #onTransform(View, float)}. * * @param page Apply the transformation to this page * @param position Position of page relative to the current front-and-center position of the pager. 0 is front and * center. 1 is one full page position to the right, and -1 is one page position to the left. */ protected void onPostTransform(View page, float position) { } /** * Same as {@link Math#min(double, double)} without double casting, zero closest to infinity handling, or NaN support. * * @param val * @param min * @return */ protected static final float min(float val, float min) { return val < min ? min : val; } } ================================================ FILE: library/src/main/java/com/gu/library/transformer/VerticalStackTransformer.java ================================================ package com.gu.library.transformer; import android.content.Context; import android.util.Log; import android.view.View; import com.gu.library.utils.ScreenUtils; /** * Created by Nate on 2016/7/22. */ public class VerticalStackTransformer extends VerticalBaseTransformer { private Context context; private int spaceBetweenFirAndSecWith = 10 * 2;//第一张卡片和第二张卡片宽度差 dp单位 private int spaceBetweenFirAndSecHeight = 10;//第一张卡片和第二张卡片高度差 dp单位 public VerticalStackTransformer(Context context) { this.context = context; } public VerticalStackTransformer(Context context, int spaceBetweenFirAndSecWith, int spaceBetweenFirAndSecHeight) { this.context = context; this.spaceBetweenFirAndSecWith = spaceBetweenFirAndSecWith; this.spaceBetweenFirAndSecHeight = spaceBetweenFirAndSecHeight; } @Override protected void onTransform(View page, float position) { if (position <= 0.0f) { page.setAlpha(1.0f); Log.e("onTransform", "position <= 0.0f ==>" + position); page.setTranslationY(0f); //控制停止滑动切换的时候,只有最上面的一张卡片可以点击 page.setClickable(true); } else if (position <= 3.0f) { Log.e("onTransform", "position <= 3.0f ==>" + position); float scale = (float) (page.getWidth() - ScreenUtils.dp2px(context, spaceBetweenFirAndSecWith * position)) / (float) (page.getWidth()); //控制下面卡片的可见度 page.setAlpha(1.0f); //控制停止滑动切换的时候,只有最上面的一张卡片可以点击 page.setClickable(false); page.setPivotX(page.getWidth() / 2f); page.setPivotY(page.getHeight() / 2f); page.setScaleX(scale); page.setScaleY(scale); page.setTranslationY(-page.getHeight() * position + (page.getHeight() * 0.5f) * (1 - scale) + ScreenUtils.dp2px(context, spaceBetweenFirAndSecHeight) * position); } } } ================================================ FILE: library/src/main/java/com/gu/library/utils/ScreenUtils.java ================================================ package com.gu.library.utils; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; import android.view.WindowManager; import android.widget.ImageView; /** * Created by Nate on 2015/9/10. 屏幕相关工具类,可以获取屏幕宽高度,还有截取屏幕 */ public class ScreenUtils { private ScreenUtils() { /* cannot be instantiated */ throw new UnsupportedOperationException("cannot be instantiated"); } /** * 获得屏幕高度 * * @param context * @return */ public static int getScreenWidth(Context context) { WindowManager wm = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); DisplayMetrics outMetrics = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(outMetrics); return outMetrics.widthPixels; } /** * 获得屏幕宽度 * * @param context * @return */ public static int getScreenHeight(Context context) { WindowManager wm = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); DisplayMetrics outMetrics = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(outMetrics); return outMetrics.heightPixels; } /** * 获得状态栏的高度 * * @param context * @return */ public static int getStatusHeight(Context context) { int statusHeight = -1; try { Class clazz = Class.forName("com.android.internal.R$dimen"); Object object = clazz.newInstance(); int height = Integer.parseInt(clazz.getField("status_bar_height") .get(object).toString()); statusHeight = context.getResources().getDimensionPixelSize(height); } catch (Exception e) { e.printStackTrace(); } return statusHeight; } /** * 获取当前屏幕截图,包含状态栏 * * @param activity * @return */ public static Bitmap snapShotWithStatusBar(Activity activity) { View view = activity.getWindow().getDecorView(); view.setDrawingCacheEnabled(true); view.buildDrawingCache(); Bitmap bmp = view.getDrawingCache(); int width = getScreenWidth(activity); int height = getScreenHeight(activity); Bitmap bp = null; bp = Bitmap.createBitmap(bmp, 0, 0, width, height); view.destroyDrawingCache(); return bp; } /** * 获取当前屏幕截图,不包含状态栏 * * @param activity * @return */ public static Bitmap snapShotWithoutStatusBar(Activity activity) { View view = activity.getWindow().getDecorView(); view.setDrawingCacheEnabled(true); view.buildDrawingCache(); Bitmap bmp = view.getDrawingCache(); Rect frame = new Rect(); activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame); int statusBarHeight = frame.top; int width = getScreenWidth(activity); int height = getScreenHeight(activity); Bitmap bp = null; bp = Bitmap.createBitmap(bmp, 0, statusBarHeight, width, height - statusBarHeight); view.destroyDrawingCache(); return bp; } /** * dp转px * * @param context * @param dpVal * @return */ public static int dp2px(Context context, float dpVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics()); } /** * sp转px * * @param context * @param spVal * @return */ public static int sp2px(Context context, float spVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, context.getResources().getDisplayMetrics()); } /** * px转dp * * @param context * @param pxVal * @return */ public static float px2dp(Context context, float pxVal) { final float scale = context.getResources().getDisplayMetrics().density; return (pxVal / scale); } /** * px转sp * * @param context * @param pxVal * @return */ public static float px2sp(Context context, float pxVal) { return (pxVal / context.getResources().getDisplayMetrics().scaledDensity); } /** * 动态设置图片宽高 */ /** * 动态设置图片宽高 */ public static float[] getBitmapConfiguration(Bitmap bitmap, ImageView imageView, float screenRadio) { int screenWidth = getScreenWidth(imageView.getContext()); float rawWidth = 0; float rawHeight = 0; float width = 0; float height = 0; if (bitmap == null) { width = (float) (screenWidth / screenRadio); height = (float) width; imageView.setScaleType(ImageView.ScaleType.FIT_XY); } else { rawWidth = bitmap.getWidth(); rawHeight = bitmap.getHeight(); if (rawHeight > 10 * rawWidth) { imageView.setScaleType(ImageView.ScaleType.CENTER); } else { imageView.setScaleType(ImageView.ScaleType.FIT_XY); } float radio = rawHeight / rawWidth; width = (float) (screenWidth / screenRadio); height = (float) (radio * width); } return new float[]{width, height}; } } ================================================ FILE: library/src/test/java/com/gu/library/ExampleUnitTest.java ================================================ package com.gu.library; import org.junit.Test; import static org.junit.Assert.*; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: settings.gradle ================================================ include ':app', ':library'