├── MoviesDemoProject ├── MoviesDemo │ ├── .gitignore │ ├── libs │ │ └── joda-time-2.3.jar │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── drawable-xhdpi │ │ │ │ ├── bg_tabs.9.png │ │ │ │ ├── star_empty.png │ │ │ │ ├── star_full.png │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── poster_frozen.jpg │ │ │ │ ├── poster_thor.jpg │ │ │ │ ├── bg_actionbar.9.png │ │ │ │ ├── poster_gravity.jpg │ │ │ │ ├── tab_selected.9.png │ │ │ │ ├── ic_action_search.png │ │ │ │ ├── poster_about_time.jpg │ │ │ │ ├── poster_bad_grandpa.jpg │ │ │ │ ├── poster_enders_game.jpg │ │ │ │ ├── poster_enough_said.jpg │ │ │ │ ├── poster_prisoners.jpg │ │ │ │ ├── tab_unselected.9.png │ │ │ │ ├── card_overlay_left.9.png │ │ │ │ ├── card_overlay_right.9.png │ │ │ │ ├── poster_runner_runner.jpg │ │ │ │ ├── poster_the_counselor.jpg │ │ │ │ ├── card_overlay_middle.9.png │ │ │ │ ├── poster_the_fifth_estate.jpg │ │ │ │ ├── tab_selected_pressed.9.png │ │ │ │ ├── tab_unselected_pressed.9.png │ │ │ │ └── poster_cloudy_with_a_chance_of_meatballs_2.jpg │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── layout │ │ │ │ ├── include_header_footer_space.xml │ │ │ │ ├── fragment_space.xml │ │ │ │ ├── activity_movies.xml │ │ │ │ ├── row_movie_pair.xml │ │ │ │ └── include_movie_row.xml │ │ │ ├── values-sw600dp │ │ │ │ └── dimens.xml │ │ │ ├── values-sw720dp-land │ │ │ │ └── dimens.xml │ │ │ ├── values-v11 │ │ │ │ └── styles.xml │ │ │ ├── drawable │ │ │ │ ├── ratingbar_movie.xml │ │ │ │ ├── bg_film_rating.xml │ │ │ │ └── tab_indicator.xml │ │ │ ├── values-v14 │ │ │ │ └── styles.xml │ │ │ └── menu │ │ │ │ └── movies.xml │ │ │ ├── assets │ │ │ ├── fonts │ │ │ │ └── RobotoCondensed-Bold.ttf │ │ │ └── data.json │ │ │ ├── java │ │ │ └── com │ │ │ │ └── idunnolol │ │ │ │ └── moviesdemo │ │ │ │ ├── util │ │ │ │ ├── FontCache.java │ │ │ │ ├── BitmapCache.java │ │ │ │ └── ResourceUtils.java │ │ │ │ ├── view │ │ │ │ ├── DecorFrameLayout.java │ │ │ │ ├── CenteringRelativeLayout.java │ │ │ │ ├── SlidingListView.java │ │ │ │ ├── SlidingPairView.java │ │ │ │ ├── SlidingRevealViewGroup.java │ │ │ │ ├── MovieRowView.java │ │ │ │ └── ViewPager.java │ │ │ │ ├── ui │ │ │ │ ├── AboutDialogFragment.java │ │ │ │ ├── MoviesApplication.java │ │ │ │ └── MoviesActivity.java │ │ │ │ ├── data │ │ │ │ └── Movie.java │ │ │ │ └── widget │ │ │ │ └── MovieAdapter.java │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── settings.gradle ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew.bat └── gradlew ├── .gitignore ├── README.md └── LICENSE /MoviesDemoProject/MoviesDemo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /MoviesDemoProject/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':MoviesDemo' 2 | -------------------------------------------------------------------------------- /MoviesDemoProject/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /MoviesDemoProject/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/libs/joda-time-2.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/libs/joda-time-2.3.jar -------------------------------------------------------------------------------- /MoviesDemoProject/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/bg_tabs.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/bg_tabs.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_empty.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/star_full.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 8dp 5 | 6 | 7 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_frozen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_frozen.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_thor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_thor.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/assets/fonts/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/assets/fonts/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/bg_actionbar.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/bg_actionbar.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_gravity.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_gravity.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_selected.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_selected.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/ic_action_search.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_about_time.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_about_time.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_bad_grandpa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_bad_grandpa.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_enders_game.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_enders_game.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_enough_said.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_enough_said.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_prisoners.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_prisoners.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_unselected.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_unselected.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/card_overlay_left.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/card_overlay_left.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/card_overlay_right.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/card_overlay_right.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_runner_runner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_runner_runner.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_the_counselor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_the_counselor.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/card_overlay_middle.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/card_overlay_middle.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_the_fifth_estate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_the_fifth_estate.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_selected_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_selected_pressed.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_unselected_pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/tab_unselected_pressed.9.png -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #242324 4 | 5 | #A3A4A5 6 | #3eb365 7 | 8 | 9 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/layout/include_header_footer_space.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_cloudy_with_a_chance_of_meatballs_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlew/android-movies-demo/HEAD/MoviesDemoProject/MoviesDemo/src/main/res/drawable-xhdpi/poster_cloudy_with_a_chance_of_meatballs_2.jpg -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values-sw600dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /MoviesDemoProject/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 16 10:05:23 CDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.8-bin.zip 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/layout/fragment_space.xml: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values-sw720dp-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 128dp 5 | 6 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable/ratingbar_movie.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable/bg_film_rating.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | android-movies-demo 2 | =================== 3 | 4 | This is a sample application showing off some interesting design/development interactions for a talk given at Droidcon 2013. 5 | 6 | Video f Android Sample App: Movies 7 | 8 | As it is a sample, it's got a lot of hardcoded data and what have you. It also only works on phones in portrait. Sorry! -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/menu/movies.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:0.6.+' 7 | } 8 | } 9 | apply plugin: 'android' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | android { 16 | compileSdkVersion 18 17 | buildToolsVersion "18.0.1" 18 | 19 | defaultConfig { 20 | minSdkVersion 14 21 | targetSdkVersion 18 22 | } 23 | } 24 | 25 | dependencies { 26 | compile fileTree(dir: 'libs', include: '*.jar') 27 | compile 'com.android.support:appcompat-v7:18.0.0' 28 | } 29 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/util/FontCache.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.util; 2 | 3 | import android.content.Context; 4 | import android.graphics.Typeface; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public class FontCache { 10 | 11 | private static Map sCachedFonts = new HashMap(); 12 | 13 | public static Typeface getTypeface(Context context, String assetPath) { 14 | if (!sCachedFonts.containsKey(assetPath)) { 15 | Typeface tf = Typeface.createFromAsset(context.getAssets(), assetPath); 16 | sCachedFonts.put(assetPath, tf); 17 | } 18 | 19 | return sCachedFonts.get(assetPath); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/view/DecorFrameLayout.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.FrameLayout; 6 | 7 | /** 8 | * This is an unremarkable FrameLayout that is only special because it is 9 | * a Decor View, which makes ViewPager leave it around. 10 | */ 11 | public class DecorFrameLayout extends FrameLayout implements ViewPager.Decor { 12 | 13 | public DecorFrameLayout(Context context) { 14 | super(context); 15 | } 16 | 17 | public DecorFrameLayout(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public DecorFrameLayout(Context context, AttributeSet attrs, int defStyle) { 22 | super(context, attrs, defStyle); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/ui/AboutDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.ui; 2 | 3 | import android.app.AlertDialog; 4 | import android.app.Dialog; 5 | import android.os.Bundle; 6 | import android.support.v4.app.DialogFragment; 7 | import android.view.ContextThemeWrapper; 8 | 9 | import com.idunnolol.moviesdemo.R; 10 | 11 | public class AboutDialogFragment extends DialogFragment { 12 | @Override 13 | public Dialog onCreateDialog(Bundle savedInstanceState) { 14 | ContextThemeWrapper context = new ContextThemeWrapper(getActivity(), 15 | android.R.style.Theme_Holo_Dialog); 16 | AlertDialog.Builder builder = new AlertDialog.Builder(context); 17 | 18 | builder.setMessage(R.string.about_msg); 19 | builder.setNeutralButton(android.R.string.ok, null); 20 | 21 | return builder.create(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Movies Demo 5 | Movies 6 | 7 | Search 8 | About 9 | 10 | This is a small demo application for a DroidCon 2013 talk. There are many missing pieces because it is only meant to demonstrate a few aspects of design/development, not the entire process. 11 | 12 | Now Playing 13 | Everything 14 | Upcoming 15 | 16 | 17 | %s DAY 18 | %s DAYS 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/layout/activity_movies.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/layout/row_movie_pair.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Daniel Lew 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/drawable/tab_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/view/CenteringRelativeLayout.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.RelativeLayout; 7 | 8 | /** 9 | * As this is translated, its children get translated back in the opposite direction 10 | * This has the effect of making it look "centered" even though it is sliding. 11 | */ 12 | public class CenteringRelativeLayout extends RelativeLayout { 13 | 14 | public CenteringRelativeLayout(Context context) { 15 | super(context); 16 | } 17 | 18 | public CenteringRelativeLayout(Context context, AttributeSet attrs) { 19 | super(context, attrs); 20 | } 21 | 22 | public CenteringRelativeLayout(Context context, AttributeSet attrs, int defStyle) { 23 | super(context, attrs, defStyle); 24 | } 25 | 26 | @Override 27 | public void setTranslationX(float translationX) { 28 | super.setTranslationX(translationX); 29 | 30 | float childCounter = -translationX / 2; 31 | int childCount = getChildCount(); 32 | for (int a = 0; a < childCount; a++) { 33 | View child = getChildAt(a); 34 | child.setTranslationX(childCounter); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/util/BitmapCache.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.util; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.util.Log; 7 | import android.util.LruCache; 8 | 9 | 10 | public class BitmapCache { 11 | 12 | private static Context sAppContext; 13 | 14 | private static LruCache sCache; 15 | 16 | private static final int SCALE = 1024; // Measure everything in KB 17 | 18 | public static void init(Context context) { 19 | sAppContext = context.getApplicationContext(); 20 | 21 | // Use 1/4th of the available memory for this memory cache. 22 | final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / SCALE); 23 | final int cacheSize = maxMemory / 4; 24 | 25 | Log.i("MovieDemo", "Creating Bitmap cache size of: " + cacheSize + " (total memory " + maxMemory + ")"); 26 | 27 | sCache = new LruCache(cacheSize) { 28 | @Override 29 | protected int sizeOf(Integer key, Bitmap bitmap) { 30 | return bitmap.getByteCount() / SCALE; 31 | } 32 | }; 33 | } 34 | 35 | public static Bitmap getBitmap(int drawableResId) { 36 | Bitmap bitmap = sCache.get(drawableResId); 37 | if (bitmap == null) { 38 | bitmap = BitmapFactory.decodeResource(sAppContext.getResources(), drawableResId); 39 | sCache.put(drawableResId, bitmap); 40 | } 41 | return bitmap; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/view/SlidingListView.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.ListView; 7 | 8 | import com.idunnolol.moviesdemo.R; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * Allows SlidingPairViews to operate without having to layout constantly 15 | */ 16 | public class SlidingListView extends ListView { 17 | 18 | public SlidingListView(Context context) { 19 | super(context); 20 | } 21 | 22 | public SlidingListView(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | } 25 | 26 | public SlidingListView(Context context, AttributeSet attrs, int defStyle) { 27 | super(context, attrs, defStyle); 28 | } 29 | 30 | // For performance reasons, we want to set the slide directly on the views instead of 31 | // setting the slide in the adapter and notifying that the dataset changed. 32 | public void setSlide(float slide) { 33 | if (getWidth() == 0) { 34 | return; 35 | } 36 | 37 | for (SlidingPairView child : getAllSlidingPairViewChildren()) { 38 | child.setSlide(slide); 39 | } 40 | } 41 | 42 | public void setUseHardwareLayers(boolean useHardwareLayers) { 43 | for (SlidingPairView child : getAllSlidingPairViewChildren()) { 44 | child.setUseHardwareLayers(useHardwareLayers); 45 | } 46 | } 47 | 48 | private List getAllSlidingPairViewChildren() { 49 | List children = new ArrayList(); 50 | int childCount = getChildCount(); 51 | for (int a = 0; a < childCount; a++) { 52 | View view = getChildAt(a).findViewById(R.id.sliding_pair); 53 | if (view != null) { 54 | children.add((SlidingPairView) view); 55 | } 56 | } 57 | return children; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 22 | 23 | 28 | 29 | 30 | 35 | 36 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/data/Movie.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.data; 2 | 3 | import org.joda.time.DateTime; 4 | import org.joda.time.DateTimeZone; 5 | import org.joda.time.LocalTime; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class Movie { 11 | 12 | private String mTitle; 13 | 14 | private int mPosterResId; 15 | 16 | // G, PG, PG-13, R 17 | private String mFilmRating; 18 | 19 | // Out of 5 20 | private int mScore; 21 | 22 | private List mShowTimes; 23 | 24 | private int mDaysTillRelease; 25 | 26 | // Cached; calculated from showtimes 27 | private List mShowTimeInUtcMillis; 28 | 29 | public String getTitle() { 30 | return mTitle; 31 | } 32 | 33 | public void setTitle(String title) { 34 | mTitle = title; 35 | } 36 | 37 | public int getPosterResId() { 38 | return mPosterResId; 39 | } 40 | 41 | public void setPosterResId(int posterResId) { 42 | mPosterResId = posterResId; 43 | } 44 | 45 | public String getFilmRating() { 46 | return mFilmRating; 47 | } 48 | 49 | public void setFilmRating(String filmRating) { 50 | mFilmRating = filmRating; 51 | } 52 | 53 | public int getScore() { 54 | return mScore; 55 | } 56 | 57 | public void setScore(int score) { 58 | mScore = score; 59 | } 60 | 61 | public List getShowTimes() { 62 | return mShowTimes; 63 | } 64 | 65 | public void setShowTimes(List showTimes) { 66 | mShowTimes = showTimes; 67 | 68 | mShowTimeInUtcMillis = new ArrayList(); 69 | for (LocalTime time : showTimes) { 70 | DateTime utcDateTime = time.toDateTimeToday(DateTimeZone.UTC); 71 | mShowTimeInUtcMillis.add(utcDateTime.getMillis()); 72 | } 73 | } 74 | 75 | public List getShowTimesInUtcMillis() { 76 | return mShowTimeInUtcMillis; 77 | } 78 | 79 | public int getDaysTillRelease() { 80 | return mDaysTillRelease; 81 | } 82 | 83 | public void setDaysTillRelease(int daysTillRelease) { 84 | mDaysTillRelease = daysTillRelease; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/util/ResourceUtils.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.util; 2 | 3 | import android.util.Log; 4 | 5 | import java.lang.reflect.Field; 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | /** 10 | * Utilities relating to resources (anything under /res/ or deals 11 | * with the Resources class). 12 | */ 13 | public class ResourceUtils { 14 | 15 | /** Cache of resources ids, for speed */ 16 | private static Map, Map> mIdentifierCache = new ConcurrentHashMap, Map>(); 17 | 18 | /** 19 | * Retrieves a resource id dynamically, via reflection. It's much faster 20 | * than Resources.getIdentifier(), however it only allows you to get 21 | * identifiers from your own package. 22 | * 23 | * Note that this method is still slower than retrieving resources 24 | * directly (e.g., R.drawable.MyResource) - it should only be used 25 | * when dynamically retrieving ids. 26 | * 27 | * @param type the type of resource (e.g. R.drawable.class, R.layout.class, etc.) 28 | * @param name the name of the resource 29 | * @return the resource id, or -1 if not found 30 | */ 31 | public static int getIdentifier(Class type, String name) { 32 | // See if the cache already contains this identifier 33 | Map typeCache; 34 | if (!mIdentifierCache.containsKey(type)) { 35 | typeCache = new ConcurrentHashMap(); 36 | mIdentifierCache.put(type, typeCache); 37 | } 38 | else { 39 | typeCache = mIdentifierCache.get(type); 40 | } 41 | 42 | if (typeCache.containsKey(name)) { 43 | return typeCache.get(name); 44 | } 45 | 46 | // Retrieve the identifier 47 | try { 48 | Field field = type.getField(name); 49 | int resId = field.getInt(null); 50 | 51 | if (resId != -1) { 52 | typeCache.put(name, resId); 53 | } 54 | 55 | return resId; 56 | } 57 | catch (Exception e) { 58 | Log.e("MovieDemo", "Failed to retrieve identifier: type=" + type + " name=" + name, e); 59 | return -1; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/assets/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Cloudy With a Chance of Meatballs 2", 4 | "poster": "poster_cloudy_with_a_chance_of_meatballs_2", 5 | "rating": "PG", 6 | "score": 3, 7 | "showtimes": [ 8 | "18:20", 9 | "20:45" 10 | ] 11 | }, 12 | { 13 | "name": "The Counselor", 14 | "poster": "poster_the_counselor", 15 | "rating": "R", 16 | "daysUntilRelease": 2 17 | }, 18 | { 19 | "name": "Gravity", 20 | "poster": "poster_gravity", 21 | "rating": "PG-13", 22 | "score": 5, 23 | "showtimes": [ 24 | "18:30", 25 | "21:00" 26 | ] 27 | }, 28 | { 29 | "name": "Bad Grandpa", 30 | "poster": "poster_bad_grandpa", 31 | "rating": "R", 32 | "daysUntilRelease": 3 33 | }, 34 | { 35 | "name": "The Fifth Estate", 36 | "poster": "poster_the_fifth_estate", 37 | "rating": "R", 38 | "score": 2, 39 | "showtimes": [ 40 | "15:40", 41 | "19:30" 42 | ] 43 | }, 44 | { 45 | "name": "Ender's Game", 46 | "poster": "poster_enders_game", 47 | "rating": "PG-13", 48 | "daysUntilRelease": 3 49 | }, 50 | { 51 | "name": "Enough Said", 52 | "poster": "poster_enough_said", 53 | "rating": "PG-13", 54 | "score": 5, 55 | "showtimes": [ 56 | "18:20", 57 | "20:45" 58 | ] 59 | }, 60 | { 61 | "name": "About Time", 62 | "poster": "poster_about_time", 63 | "rating": "R", 64 | "score": 4, 65 | "daysUntilRelease": 5 66 | }, 67 | { 68 | "name": "Prisoners", 69 | "poster": "poster_prisoners", 70 | "rating": "R", 71 | "score": 4, 72 | "showtimes": [ 73 | "15:20", 74 | "20:55" 75 | ] 76 | }, 77 | { 78 | "name": "Thor: The Dark World", 79 | "poster": "poster_thor", 80 | "rating": "PG-13", 81 | "score": 4, 82 | "daysUntilRelease": 5 83 | }, 84 | { 85 | "name": "Runner Runner", 86 | "poster": "poster_runner_runner", 87 | "rating": "R", 88 | "score": 1, 89 | "showtimes": [ 90 | "15:20", 91 | "20:55" 92 | ] 93 | }, 94 | { 95 | "name": "Frozen", 96 | "poster": "poster_frozen", 97 | "rating": "G", 98 | "daysUntilRelease": 5 99 | } 100 | ] -------------------------------------------------------------------------------- /MoviesDemoProject/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/view/SlidingPairView.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.FrameLayout; 6 | 7 | import com.idunnolol.moviesdemo.R; 8 | 9 | /** 10 | * Has two children - a left and right sliding pair. 11 | */ 12 | public class SlidingPairView extends FrameLayout { 13 | 14 | private float mSlide; 15 | 16 | // This one starts on the left, and reveals TO the right 17 | private SlidingRevealViewGroup mSlideRight; 18 | 19 | // This one starts on the right, reveals TO the left 20 | private SlidingRevealViewGroup mSlideLeft; 21 | 22 | public SlidingPairView(Context context) { 23 | super(context); 24 | } 25 | 26 | public SlidingPairView(Context context, AttributeSet attrs) { 27 | super(context, attrs); 28 | } 29 | 30 | public SlidingPairView(Context context, AttributeSet attrs, int defStyle) { 31 | super(context, attrs, defStyle); 32 | } 33 | 34 | @Override 35 | protected void onFinishInflate() { 36 | super.onFinishInflate(); 37 | 38 | mSlideRight = (SlidingRevealViewGroup) findViewById(R.id.slide_reveal_right); 39 | mSlideLeft = (SlidingRevealViewGroup) findViewById(R.id.slide_reveal_left); 40 | 41 | mSlideRight.setReveal(SlidingRevealViewGroup.Reveal.RIGHT); 42 | mSlideLeft.setReveal(SlidingRevealViewGroup.Reveal.RIGHT); 43 | } 44 | 45 | @Override 46 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 47 | super.onLayout(changed, l, t, r, b); 48 | 49 | if (changed) { 50 | updateSlide(); 51 | } 52 | } 53 | 54 | public void setSlide(float slide) { 55 | mSlide = slide; 56 | updateSlide(); 57 | } 58 | 59 | private void updateSlide() { 60 | // TODO: Clean up math around here 61 | float slideHideX = mSlideLeft.getSlideHideX(); 62 | float paddingLeftAndRight = mSlideRight.getPaddingLeft() + mSlideRight.getPaddingRight(); 63 | 64 | if (mSlide < 0) { 65 | mSlideRight.setRevealPercent(-mSlide); 66 | mSlideRight.setTranslationX(0); 67 | mSlideLeft.setRevealPercent(0); 68 | mSlideLeft.setTranslationX(slideHideX + mSlideRight.getSlideRevealX() - paddingLeftAndRight); 69 | } 70 | else if (mSlide == 0) { 71 | mSlideRight.setRevealPercent(0); 72 | mSlideRight.setTranslationX(0); 73 | mSlideLeft.setRevealPercent(0); 74 | mSlideLeft.setTranslationX(slideHideX - paddingLeftAndRight); 75 | } 76 | else { 77 | mSlideLeft.setRevealPercent(mSlide); 78 | mSlideLeft.setTranslationX((1 - mSlide) * slideHideX - (1 - mSlide) * paddingLeftAndRight); 79 | mSlideRight.setRevealPercent(0); 80 | mSlideRight.setTranslationX(-mSlideLeft.getSlideRevealX()); 81 | } 82 | } 83 | 84 | public void setUseHardwareLayers(boolean useHardwareLayers) { 85 | mSlideRight.setUseHardwareLayers(useHardwareLayers); 86 | mSlideLeft.setUseHardwareLayers(useHardwareLayers); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/ui/MoviesApplication.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.ui; 2 | 3 | import android.app.Application; 4 | import android.util.JsonReader; 5 | 6 | import com.idunnolol.moviesdemo.R; 7 | import com.idunnolol.moviesdemo.data.Movie; 8 | import com.idunnolol.moviesdemo.util.BitmapCache; 9 | import com.idunnolol.moviesdemo.util.ResourceUtils; 10 | 11 | import org.joda.time.LocalDate; 12 | import org.joda.time.LocalTime; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.InputStreamReader; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | public class MoviesApplication extends Application { 21 | 22 | private List mDemoData; 23 | 24 | @Override 25 | public void onCreate() { 26 | super.onCreate(); 27 | 28 | LocalDate date; 29 | 30 | BitmapCache.init(this); 31 | 32 | // Load all demo data on start; normally a horrible practice but I'll allow it for a demo 33 | mDemoData = new ArrayList(); 34 | try { 35 | InputStream is = null; 36 | try { 37 | is = getAssets().open("data.json"); 38 | JsonReader reader = new JsonReader(new InputStreamReader(is)); 39 | 40 | reader.beginArray(); 41 | while (reader.hasNext()) { 42 | reader.beginObject(); 43 | 44 | Movie movie = new Movie(); 45 | while (reader.hasNext()) { 46 | String name = reader.nextName(); 47 | 48 | if (name.equals("name")) { 49 | movie.setTitle(reader.nextString()); 50 | } 51 | else if (name.equals("poster")) { 52 | movie.setPosterResId(ResourceUtils.getIdentifier(R.drawable.class, reader.nextString())); 53 | } 54 | else if (name.equals("rating")) { 55 | movie.setFilmRating(reader.nextString()); 56 | } 57 | else if (name.equals("score")) { 58 | movie.setScore(reader.nextInt()); 59 | } 60 | else if (name.equals("showtimes")) { 61 | reader.beginArray(); 62 | List localTimes = new ArrayList(); 63 | while(reader.hasNext()) { 64 | localTimes.add(LocalTime.parse(reader.nextString())); 65 | } 66 | movie.setShowTimes(localTimes); 67 | reader.endArray(); 68 | } 69 | else if (name.equals("daysUntilRelease")) { 70 | movie.setDaysTillRelease(reader.nextInt()); 71 | } 72 | else { 73 | reader.skipValue(); 74 | } 75 | } 76 | mDemoData.add(movie); 77 | 78 | reader.endObject(); 79 | } 80 | reader.endArray(); 81 | String str = convertStreamToString(is); 82 | } 83 | finally { 84 | is.close(); 85 | } 86 | } 87 | catch (IOException e) { 88 | throw new RuntimeException(e); 89 | } 90 | 91 | // Preload all bitmaps into memory, so the demo runs smoother 92 | for (Movie movie : mDemoData) { 93 | BitmapCache.getBitmap(movie.getPosterResId()); 94 | } 95 | } 96 | 97 | private static String convertStreamToString(InputStream is) { 98 | java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); 99 | return s.hasNext() ? s.next() : ""; 100 | } 101 | 102 | public List getDemoData() { 103 | return mDemoData; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/widget/MovieAdapter.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.widget; 2 | 3 | import android.content.Context; 4 | import android.util.Pair; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | 10 | import com.idunnolol.moviesdemo.R; 11 | import com.idunnolol.moviesdemo.data.Movie; 12 | import com.idunnolol.moviesdemo.view.MovieRowView; 13 | import com.idunnolol.moviesdemo.view.SlidingPairView; 14 | 15 | import java.util.List; 16 | 17 | public class MovieAdapter extends BaseAdapter { 18 | 19 | private Context mContext; 20 | 21 | private MovieAdapterListener mListener; 22 | 23 | private List mMovies; 24 | 25 | private float mSlide; 26 | 27 | private int mCellSize; 28 | 29 | public MovieAdapter(Context context, List movies, MovieAdapterListener listener, int cellSize) { 30 | mContext = context; 31 | mMovies = movies; 32 | mListener = listener; 33 | mCellSize = cellSize; 34 | } 35 | 36 | public void setSlide(float slide) { 37 | mSlide = slide; 38 | } 39 | 40 | @Override 41 | public int getCount() { 42 | return (int) Math.ceil(mMovies.size() / 2.0f); 43 | } 44 | 45 | @Override 46 | public Pair getItem(int position) { 47 | int index = position * 2; 48 | return Pair.create(mMovies.get(index), mMovies.get(index + 1)); 49 | } 50 | 51 | @Override 52 | public long getItemId(int position) { 53 | return position; 54 | } 55 | 56 | @Override 57 | public boolean areAllItemsEnabled() { 58 | return false; 59 | } 60 | 61 | @Override 62 | public boolean isEnabled(int position) { 63 | return false; 64 | } 65 | 66 | @Override 67 | public View getView(int position, View convertView, ViewGroup parent) { 68 | ViewHolder vh; 69 | if (convertView == null) { 70 | convertView = LayoutInflater.from(mContext).inflate(R.layout.row_movie_pair, parent, false); 71 | 72 | vh = new ViewHolder(); 73 | vh.mSlidingPairView = (SlidingPairView) convertView.findViewById(R.id.sliding_pair); 74 | vh.mNowPlayingMovie = (MovieRowView) convertView.findViewById(R.id.slide_reveal_right); 75 | vh.mUpcomingMovie = (MovieRowView) convertView.findViewById(R.id.slide_reveal_left); 76 | 77 | // We set the cell size dynamically, because it's measured on the size of 78 | // the screen ahead of time 79 | vh.mNowPlayingMovie.setCellSize(mCellSize); 80 | vh.mUpcomingMovie.setCellSize(mCellSize); 81 | 82 | convertView.setTag(vh); 83 | } 84 | else { 85 | vh = (ViewHolder) convertView.getTag(); 86 | } 87 | 88 | final Pair moviePair = getItem(position); 89 | 90 | vh.mSlidingPairView.setSlide(mSlide); 91 | 92 | vh.mNowPlayingMovie.bind(moviePair.first); 93 | vh.mUpcomingMovie.bind(moviePair.second); 94 | 95 | vh.mNowPlayingMovie.setOnClickListener(new View.OnClickListener() { 96 | @Override 97 | public void onClick(View v) { 98 | mListener.onMovieClicked(moviePair.first, true); 99 | } 100 | }); 101 | 102 | vh.mUpcomingMovie.setOnClickListener(new View.OnClickListener() { 103 | @Override 104 | public void onClick(View v) { 105 | mListener.onMovieClicked(moviePair.second, false); 106 | } 107 | }); 108 | 109 | return convertView; 110 | } 111 | 112 | private static final class ViewHolder { 113 | SlidingPairView mSlidingPairView; 114 | MovieRowView mNowPlayingMovie; 115 | MovieRowView mUpcomingMovie; 116 | } 117 | 118 | ////////////////////////////////////////////////////////////////////////// 119 | // Listener 120 | 121 | public interface MovieAdapterListener { 122 | public void onMovieClicked(Movie movie, boolean isOnLeft); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/view/SlidingRevealViewGroup.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.RelativeLayout; 7 | 8 | import com.idunnolol.moviesdemo.R; 9 | 10 | /** 11 | * Holds two views which "slide" open 12 | * 13 | * It is constructed via three view: 14 | * 0 - A Spacer. This represents the space the cover takes up when slid out. 15 | * 1 - The sliding view. This hides itself. 16 | * 2 - The cover view. This is always visible. 17 | */ 18 | public class SlidingRevealViewGroup extends RelativeLayout { 19 | 20 | public enum Reveal { 21 | LEFT, 22 | RIGHT 23 | } 24 | 25 | // Which side to reveal 26 | private Reveal mReveal; 27 | 28 | // Represents how far in we are sliding; 0% == totally revealed, 100% == totally hidden 29 | private float mRevealPercent; 30 | 31 | // Internal Views 32 | private View mSpaceView; 33 | private View mSlidingView; 34 | private View mCoverView; 35 | private View mSlideOverlayLeft; 36 | private View mSlideOverlayMiddle; 37 | private View mSlideOverlayRight; 38 | 39 | public SlidingRevealViewGroup(Context context) { 40 | this(context, null); 41 | } 42 | 43 | public SlidingRevealViewGroup(Context context, AttributeSet attrs) { 44 | this(context, attrs, 0); 45 | } 46 | 47 | public SlidingRevealViewGroup(Context context, AttributeSet attrs, int defStyle) { 48 | super(context, attrs, defStyle); 49 | } 50 | 51 | @Override 52 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 53 | super.onLayout(changed, l, t, r, b); 54 | 55 | if (changed) { 56 | updateSlide(); 57 | } 58 | } 59 | 60 | @Override 61 | protected void onFinishInflate() { 62 | super.onFinishInflate(); 63 | 64 | mSpaceView = getChildAt(0); 65 | mSlidingView = getChildAt(1); 66 | mCoverView = getChildAt(2); 67 | 68 | mSlideOverlayLeft = findViewById(R.id.slide_overlay_left); 69 | mSlideOverlayMiddle = findViewById(R.id.slide_overlay_middle); 70 | mSlideOverlayRight = findViewById(R.id.slide_overlay_right); 71 | } 72 | 73 | public void setReveal(Reveal reveal) { 74 | mReveal = reveal; 75 | } 76 | 77 | public void setRevealPercent(float revealPercent) { 78 | mRevealPercent = revealPercent; 79 | 80 | updateSlide(); 81 | } 82 | 83 | public float getRevealPercent() { 84 | return mRevealPercent; 85 | } 86 | 87 | public float getSlideRevealX() { 88 | return mSlidingView.getWidth() - Math.abs(mSlidingView.getTranslationX()) 89 | - (mCoverView.getWidth() - mSpaceView.getWidth()); 90 | } 91 | 92 | public float getSlideHideX() { 93 | return getWidth() - mCoverView.getWidth(); 94 | } 95 | 96 | private void updateSlide() { 97 | int width = getWidth(); 98 | if (width == 0) { 99 | // We haven't measured yet; wait until measurement to slide 100 | return; 101 | } 102 | 103 | // Hide part of the cover, based on the spacer 104 | int widthDiff = mCoverView.getWidth() - mSpaceView.getWidth(); 105 | float coverTransX = mRevealPercent * widthDiff; 106 | mCoverView.setTranslationX(mReveal == Reveal.RIGHT ? -coverTransX : coverTransX); 107 | 108 | // Slide to reveal the View 109 | // 110 | // We need to take into account the cover sliding as well, so that the moment 111 | // you start sliding over, the cover doesn't keep hiding things 112 | float coverLeftoverX = (1 - mRevealPercent) * widthDiff; 113 | float translationX = (1 - mRevealPercent) * mSlidingView.getWidth() - coverLeftoverX; 114 | mSlidingView.setTranslationX(mReveal == Reveal.RIGHT ? -translationX : translationX); 115 | 116 | // Slide the middle so that its right edge is underneath what is revealed 117 | // Slide the right so that it's always on the right corner 118 | // TODO: This doesn't work for Reveal.LEFT 119 | mSlideOverlayMiddle.setTranslationX(-translationX); 120 | mSlideOverlayRight.setTranslationX(-translationX); 121 | 122 | onUpdateSlide(); 123 | } 124 | 125 | protected void onUpdateSlide() { 126 | // For subclasses to implement if necessary 127 | } 128 | 129 | public void setUseHardwareLayers(boolean useHardwareLayers) { 130 | // For subclasses to initialize hardware layers during animations 131 | 132 | int toLayerType = useHardwareLayers ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE; 133 | mSlideOverlayLeft.setLayerType(toLayerType, null); 134 | mSlideOverlayMiddle.setLayerType(toLayerType, null); 135 | mSlideOverlayRight.setLayerType(toLayerType, null); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /MoviesDemoProject/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/res/layout/include_movie_row.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 26 | 27 | 33 | 34 | 35 | 43 | 44 | 52 | 53 | 54 | 55 | 56 | 64 | 65 | 74 | 75 | 84 | 85 | 86 | 87 | 98 | 99 | 108 | 109 | 110 | 111 | 112 | 113 | 117 | 118 | 123 | 124 | 137 | 138 | 153 | 154 | 155 | 156 | 157 | 162 | 163 | 168 | 169 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/view/MovieRowView.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.view; 2 | 3 | import android.content.Context; 4 | import android.text.Spannable; 5 | import android.text.SpannableString; 6 | import android.text.TextUtils; 7 | import android.text.style.ForegroundColorSpan; 8 | import android.text.style.RelativeSizeSpan; 9 | import android.util.AttributeSet; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.ImageView; 13 | import android.widget.RatingBar; 14 | import android.widget.TextView; 15 | 16 | import com.idunnolol.moviesdemo.R; 17 | import com.idunnolol.moviesdemo.data.Movie; 18 | import com.idunnolol.moviesdemo.util.BitmapCache; 19 | import com.idunnolol.moviesdemo.util.FontCache; 20 | 21 | import org.joda.time.DateTimeZone; 22 | import org.joda.time.LocalDate; 23 | 24 | import java.text.DateFormat; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | public class MovieRowView extends SlidingRevealViewGroup { 29 | 30 | private ViewGroup mCoverContainer; 31 | private ImageView mPosterView; 32 | private TextView mTitleView; 33 | private TextView mSubtitleView; 34 | 35 | private ViewGroup mContentContainer; 36 | private TextView mContentTitleView; 37 | private ViewGroup mShowtimesContainer; 38 | private TextView mShowtimesTextView; 39 | private ViewGroup mUpcomingContainer; 40 | private TextView mUpcomingDateTextView; 41 | private TextView mUpcomingDaysTextView; 42 | private RatingBar mRatingBar; 43 | private TextView mFilmRatingTextView; 44 | 45 | // Cached for faster binding 46 | private List mStrArr = new ArrayList(); 47 | private DateFormat mDateFormat; 48 | private ForegroundColorSpan mGreenTextColorSpan; 49 | private RelativeSizeSpan mTwoLineSizeSpan; 50 | 51 | public MovieRowView(Context context) { 52 | this(context, null); 53 | } 54 | 55 | public MovieRowView(Context context, AttributeSet attrs) { 56 | this(context, attrs, 0); 57 | } 58 | 59 | public MovieRowView(Context context, AttributeSet attrs, int defStyle) { 60 | super(context, attrs, defStyle); 61 | 62 | mDateFormat = android.text.format.DateFormat.getTimeFormat(context); 63 | mDateFormat.setTimeZone(DateTimeZone.UTC.toTimeZone()); 64 | 65 | mGreenTextColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.greenText)); 66 | 67 | mTwoLineSizeSpan = new RelativeSizeSpan(2f); 68 | } 69 | 70 | @Override 71 | protected void onFinishInflate() { 72 | super.onFinishInflate(); 73 | 74 | mCoverContainer = (ViewGroup) findViewById(R.id.cover_container); 75 | mPosterView = (ImageView) findViewById(R.id.poster_view); 76 | mTitleView = (TextView) findViewById(R.id.title_view); 77 | mSubtitleView = (TextView) findViewById(R.id.subtitle_view); 78 | mContentContainer = (ViewGroup) findViewById(R.id.content_container); 79 | mContentTitleView = (TextView) findViewById(R.id.content_title_view); 80 | mShowtimesContainer = (ViewGroup) findViewById(R.id.showtimes_container); 81 | mShowtimesTextView = (TextView) findViewById(R.id.showtimes_text_view); 82 | mUpcomingContainer = (ViewGroup) findViewById(R.id.upcoming_container); 83 | mUpcomingDateTextView = (TextView) findViewById(R.id.upcoming_date_text_view); 84 | mUpcomingDaysTextView = (TextView) findViewById(R.id.upcoming_days_text_view); 85 | mRatingBar = (RatingBar) findViewById(R.id.rating_bar); 86 | mFilmRatingTextView = (TextView) findViewById(R.id.film_rating_text_view); 87 | 88 | mFilmRatingTextView.setTypeface(FontCache.getTypeface(getContext(), "fonts/RobotoCondensed-Bold.ttf")); 89 | } 90 | 91 | public void setCellSize(int cellSize) { 92 | getLayoutParams().height = cellSize; 93 | mCoverContainer.getLayoutParams().width = cellSize; 94 | } 95 | 96 | public void bind(Movie movie) { 97 | mPosterView.setImageBitmap(BitmapCache.getBitmap(movie.getPosterResId())); 98 | mTitleView.setText(movie.getTitle()); 99 | 100 | SpannableString ss; 101 | int highlightStart = 0; 102 | int highlightEnd; 103 | if (movie.getShowTimes() != null) { 104 | mStrArr.clear(); 105 | for (long utcMillis : movie.getShowTimesInUtcMillis()) { 106 | mStrArr.add(mDateFormat.format(utcMillis)); 107 | } 108 | highlightEnd = mStrArr.get(0).length(); 109 | ss = new SpannableString(TextUtils.join(", ", mStrArr)); 110 | } 111 | else { 112 | int numDays = movie.getDaysTillRelease(); 113 | highlightEnd = Integer.toString(numDays).length(); 114 | ss = new SpannableString(getResources().getQuantityString(R.plurals.numberOfDays, numDays, numDays)); 115 | } 116 | 117 | // Setup highlight for subtitle 118 | ss.setSpan(mGreenTextColorSpan, highlightStart, highlightEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 119 | 120 | mSubtitleView.setText(ss); 121 | 122 | mContentTitleView.setText(movie.getTitle()); 123 | 124 | if (movie.getShowTimes() == null) { 125 | mShowtimesContainer.setVisibility(View.GONE); 126 | } 127 | else { 128 | mShowtimesContainer.setVisibility(View.VISIBLE); 129 | 130 | // Show our previous showtimes text 131 | mShowtimesTextView.setText(ss.toString()); 132 | } 133 | 134 | if (movie.getDaysTillRelease() == 0) { 135 | mUpcomingContainer.setVisibility(View.GONE); 136 | } 137 | else { 138 | mUpcomingContainer.setVisibility(View.VISIBLE); 139 | 140 | LocalDate date = LocalDate.now().plusDays(movie.getDaysTillRelease()); 141 | mUpcomingDateTextView.setText(makeTwoLineText(Integer.toString(date.getDayOfMonth()), date 142 | .monthOfYear().getAsShortText().toUpperCase())); 143 | 144 | // Split up our previous # of days text, as a convenience (not very robust I admit) 145 | String[] split = ss.toString().split(" "); 146 | mUpcomingDaysTextView.setText(makeTwoLineText(split[0], split[1])); 147 | } 148 | 149 | if (movie.getScore() == 0) { 150 | mRatingBar.setVisibility(View.GONE); 151 | } 152 | else { 153 | mRatingBar.setVisibility(View.VISIBLE); 154 | mRatingBar.setRating(movie.getScore()); 155 | } 156 | 157 | mFilmRatingTextView.setText(movie.getFilmRating()); 158 | } 159 | 160 | private CharSequence makeTwoLineText(String lineOne, String lineTwo) { 161 | SpannableString ss = new SpannableString(lineOne + "\n" + lineTwo); 162 | ss.setSpan(mTwoLineSizeSpan, 0, lineOne.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 163 | return ss; 164 | } 165 | 166 | @Override 167 | protected void onUpdateSlide() { 168 | super.onUpdateSlide(); 169 | 170 | float revealPercent = getRevealPercent(); 171 | boolean isSliding = revealPercent != 0 && revealPercent != 1; 172 | 173 | mTitleView.setAlpha(1 - revealPercent); 174 | mSubtitleView.setAlpha(1 - revealPercent); 175 | } 176 | 177 | @Override 178 | public void setUseHardwareLayers(boolean useHardwareLayers) { 179 | // TODO: While this helps performance, it does result in a spike when you first start sliding 180 | // Maybe there's some solution where some of the Views remain in a HW layer throughout? 181 | 182 | int toLayerType = useHardwareLayers ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE; 183 | if (mPosterView.getLayerType() != toLayerType) { 184 | mPosterView.setLayerType(toLayerType, null); 185 | mTitleView.setLayerType(toLayerType, null); 186 | mSubtitleView.setLayerType(toLayerType, null); 187 | mContentContainer.setLayerType(toLayerType, null); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/ui/MoviesActivity.java: -------------------------------------------------------------------------------- 1 | package com.idunnolol.moviesdemo.ui; 2 | 3 | import android.app.ActionBar; 4 | import android.app.FragmentTransaction; 5 | import android.graphics.Point; 6 | import android.os.Bundle; 7 | import android.support.v4.app.DialogFragment; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v4.app.FragmentActivity; 10 | import android.support.v4.app.FragmentManager; 11 | import android.support.v4.app.FragmentPagerAdapter; 12 | import android.view.LayoutInflater; 13 | import android.view.Menu; 14 | import android.view.MenuItem; 15 | import android.view.View; 16 | import android.view.ViewGroup; 17 | import android.widget.Toast; 18 | 19 | import com.idunnolol.moviesdemo.R; 20 | import com.idunnolol.moviesdemo.data.Movie; 21 | import com.idunnolol.moviesdemo.view.SlidingListView; 22 | import com.idunnolol.moviesdemo.view.ViewPager; 23 | import com.idunnolol.moviesdemo.widget.MovieAdapter; 24 | 25 | import java.util.Locale; 26 | 27 | public class MoviesActivity extends FragmentActivity implements ActionBar.TabListener, 28 | MovieAdapter.MovieAdapterListener { 29 | 30 | /** 31 | * The {@link android.support.v4.view.PagerAdapter} that will provide 32 | * fragments for each of the sections. We use a 33 | * {@link android.support.v4.app.FragmentPagerAdapter} derivative, which 34 | * will keep every loaded fragment in memory. If this becomes too memory 35 | * intensive, it may be best to switch to a 36 | * {@link android.support.v4.app.FragmentStatePagerAdapter}. 37 | */ 38 | SectionsPagerAdapter mSectionsPagerAdapter; 39 | 40 | /** 41 | * The {@link ViewPager} that will host the section contents. 42 | */ 43 | ViewPager mViewPager; 44 | 45 | // ListView on screen 46 | private SlidingListView mListView; 47 | private MovieAdapter mAdapter; 48 | 49 | @Override 50 | protected void onCreate(Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | setContentView(R.layout.activity_movies); 53 | 54 | // Set up the action bar. 55 | final ActionBar actionBar = getActionBar(); 56 | actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); 57 | 58 | // Create the adapter that will return a fragment for each of the three 59 | // primary sections of the app. 60 | mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); 61 | 62 | // Set up the ViewPager with the sections adapter. 63 | mViewPager = (ViewPager) findViewById(R.id.pager); 64 | mViewPager.setAdapter(mSectionsPagerAdapter); 65 | mViewPager.setOffscreenPageLimit(3); 66 | 67 | // For each of the sections in the app, add a tab to the action bar. 68 | for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) { 69 | // Create a tab with text corresponding to the page title defined by 70 | // the adapter. Also specify this Activity object, which implements 71 | // the TabListener interface, as the callback (listener) for when 72 | // this tab is selected. 73 | actionBar.addTab( 74 | actionBar.newTab() 75 | .setText(mSectionsPagerAdapter.getPageTitle(i)) 76 | .setTabListener(this)); 77 | } 78 | 79 | mViewPager.setCurrentItem(1); 80 | actionBar.setSelectedNavigationItem(1); 81 | 82 | mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { 83 | private int mLastState; 84 | 85 | @Override 86 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 87 | if (position == 0) { 88 | setSlide(positionOffset - 1); 89 | } 90 | else if (position == 1) { 91 | setSlide(positionOffset); 92 | } 93 | else if (position == 2) { 94 | setSlide(1); 95 | } 96 | } 97 | 98 | @Override 99 | public void onPageSelected(int position) { 100 | actionBar.setSelectedNavigationItem(position); 101 | } 102 | 103 | @Override 104 | public void onPageScrollStateChanged(int state) { 105 | if (state == ViewPager.SCROLL_STATE_IDLE) { 106 | mListView.setUseHardwareLayers(false); 107 | } 108 | else if (mLastState == ViewPager.SCROLL_STATE_IDLE) { 109 | mListView.setUseHardwareLayers(true); 110 | } 111 | 112 | mLastState = state; 113 | } 114 | }); 115 | 116 | mListView = (SlidingListView) findViewById(R.id.sliding_list_view); 117 | 118 | // Add some spacing views above/below the rest of the rows; this keeps us from having 119 | // to customize the first/last rows to add some extra padding 120 | LayoutInflater inflater = LayoutInflater.from(this); 121 | mListView.addHeaderView(inflater.inflate(R.layout.include_header_footer_space, mListView, false)); 122 | mListView.addFooterView(inflater.inflate(R.layout.include_header_footer_space, mListView, false)); 123 | 124 | // We want the total width == 3*cell padding + 2*cell size 125 | int cellPadding = getResources().getDimensionPixelSize(R.dimen.cell_padding); 126 | Point size = new Point(); 127 | getWindowManager().getDefaultDisplay().getSize(size); 128 | int cellSize = (size.x - 3 * cellPadding) / 2; 129 | 130 | MoviesApplication app = (MoviesApplication) getApplication(); 131 | mAdapter = new MovieAdapter(this, app.getDemoData(), this, cellSize); 132 | mListView.setAdapter(mAdapter); 133 | } 134 | 135 | private void setSlide(float slide) { 136 | mAdapter.setSlide(slide); 137 | mListView.setSlide(slide); 138 | } 139 | 140 | @Override 141 | public boolean onCreateOptionsMenu(Menu menu) { 142 | // Inflate the menu; this adds items to the action bar if it is present. 143 | getMenuInflater().inflate(R.menu.movies, menu); 144 | return true; 145 | } 146 | 147 | @Override 148 | public boolean onOptionsItemSelected(MenuItem item) { 149 | switch (item.getItemId()) { 150 | case R.id.action_search: 151 | Toast.makeText(this, "Search not implemented for demo", Toast.LENGTH_SHORT).show(); 152 | return true; 153 | case R.id.action_about: 154 | DialogFragment df = new AboutDialogFragment(); 155 | df.show(getSupportFragmentManager(), "aboutDf"); 156 | return true; 157 | } 158 | return super.onOptionsItemSelected(item); 159 | } 160 | 161 | @Override 162 | public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { 163 | // When the given tab is selected, switch to the corresponding page in 164 | // the ViewPager. 165 | mViewPager.setCurrentItem(tab.getPosition()); 166 | } 167 | 168 | @Override 169 | public void onBackPressed() { 170 | // If the user is looking at detailed rows, put them back to the other screen instead 171 | // of leaving the page entirely 172 | if (mViewPager.getCurrentItem() != 1) { 173 | mViewPager.setCurrentItem(1, true); 174 | } 175 | else { 176 | super.onBackPressed(); 177 | } 178 | } 179 | 180 | @Override 181 | public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { 182 | } 183 | 184 | @Override 185 | public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) { 186 | } 187 | 188 | /** 189 | * A {@link FragmentPagerAdapter} that returns a fragment corresponding to 190 | * one of the sections/tabs/pages. 191 | */ 192 | public class SectionsPagerAdapter extends FragmentPagerAdapter { 193 | 194 | public SectionsPagerAdapter(FragmentManager fm) { 195 | super(fm); 196 | } 197 | 198 | @Override 199 | public Fragment getItem(int position) { 200 | return new SpaceFragment(); 201 | } 202 | 203 | @Override 204 | public int getCount() { 205 | // Show 3 total pages. 206 | return 3; 207 | } 208 | 209 | @Override 210 | public long getItemId(int position) { 211 | return super.getItemId(position); 212 | } 213 | 214 | @Override 215 | public CharSequence getPageTitle(int position) { 216 | Locale l = Locale.getDefault(); 217 | switch (position) { 218 | case 0: 219 | return getString(R.string.title_section1).toUpperCase(l); 220 | case 1: 221 | return getString(R.string.title_section2).toUpperCase(l); 222 | case 2: 223 | return getString(R.string.title_section3).toUpperCase(l); 224 | } 225 | return null; 226 | } 227 | } 228 | 229 | /** 230 | * A simple Fragment that just takes up space; We just want to use 231 | * the Decor View on top for display. 232 | */ 233 | public static class SpaceFragment extends Fragment { 234 | @Override 235 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 236 | Bundle savedInstanceState) { 237 | return inflater.inflate(R.layout.fragment_space, container, false); 238 | } 239 | } 240 | 241 | ////////////////////////////////////////////////////////////////////////// 242 | // MovieAdapterListener 243 | 244 | @Override 245 | public void onMovieClicked(Movie movie, boolean isOnLeft) { 246 | int targetItem = isOnLeft ? 0 : 2; 247 | if (mViewPager.getCurrentItem() != targetItem) { 248 | mViewPager.setCurrentItem(targetItem, true); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /MoviesDemoProject/MoviesDemo/src/main/java/com/idunnolol/moviesdemo/view/ViewPager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.idunnolol.moviesdemo.view; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.content.res.TypedArray; 22 | import android.database.DataSetObserver; 23 | import android.graphics.Canvas; 24 | import android.graphics.Rect; 25 | import android.graphics.drawable.Drawable; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import android.os.Parcel; 29 | import android.os.Parcelable; 30 | import android.os.SystemClock; 31 | import android.support.v4.os.ParcelableCompat; 32 | import android.support.v4.os.ParcelableCompatCreatorCallbacks; 33 | import android.support.v4.view.AccessibilityDelegateCompat; 34 | import android.support.v4.view.KeyEventCompat; 35 | import android.support.v4.view.MotionEventCompat; 36 | import android.support.v4.view.PagerAdapter; 37 | import android.support.v4.view.VelocityTrackerCompat; 38 | import android.support.v4.view.ViewCompat; 39 | import android.support.v4.view.ViewConfigurationCompat; 40 | import android.support.v4.view.accessibility.AccessibilityEventCompat; 41 | import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 42 | import android.support.v4.view.accessibility.AccessibilityRecordCompat; 43 | import android.support.v4.widget.EdgeEffectCompat; 44 | import android.util.AttributeSet; 45 | import android.util.Log; 46 | import android.view.FocusFinder; 47 | import android.view.Gravity; 48 | import android.view.KeyEvent; 49 | import android.view.MotionEvent; 50 | import android.view.SoundEffectConstants; 51 | import android.view.VelocityTracker; 52 | import android.view.View; 53 | import android.view.ViewConfiguration; 54 | import android.view.ViewGroup; 55 | import android.view.ViewParent; 56 | import android.view.accessibility.AccessibilityEvent; 57 | import android.view.animation.Interpolator; 58 | import android.widget.Scroller; 59 | 60 | import java.lang.reflect.Method; 61 | import java.util.ArrayList; 62 | import java.util.Collections; 63 | import java.util.Comparator; 64 | 65 | /** 66 | * We have our own internal version of ViewPager so that we can have 67 | * access to the Decor interface. This lets us overlay the UI on top 68 | * of a ViewPager. 69 | */ 70 | public class ViewPager extends ViewGroup { 71 | private static final String TAG = "ViewPager"; 72 | private static final boolean DEBUG = false; 73 | 74 | private static final boolean USE_CACHE = false; 75 | 76 | private static final int DEFAULT_OFFSCREEN_PAGES = 1; 77 | private static final int MAX_SETTLE_DURATION = 600; // ms 78 | private static final int MIN_DISTANCE_FOR_FLING = 25; // dips 79 | 80 | private static final int DEFAULT_GUTTER_SIZE = 16; // dips 81 | 82 | private static final int MIN_FLING_VELOCITY = 400; // dips 83 | 84 | private static final int[] LAYOUT_ATTRS = new int[] { 85 | android.R.attr.layout_gravity 86 | }; 87 | 88 | /** 89 | * Used to track what the expected number of items in the adapter should be. 90 | * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. 91 | */ 92 | private int mExpectedAdapterCount; 93 | 94 | static class ItemInfo { 95 | Object object; 96 | int position; 97 | boolean scrolling; 98 | float widthFactor; 99 | float offset; 100 | } 101 | 102 | private static final Comparator COMPARATOR = new Comparator(){ 103 | @Override 104 | public int compare(ItemInfo lhs, ItemInfo rhs) { 105 | return lhs.position - rhs.position; 106 | } 107 | }; 108 | 109 | private static final Interpolator sInterpolator = new Interpolator() { 110 | public float getInterpolation(float t) { 111 | t -= 1.0f; 112 | return t * t * t * t * t + 1.0f; 113 | } 114 | }; 115 | 116 | private final ArrayList mItems = new ArrayList(); 117 | private final ItemInfo mTempItem = new ItemInfo(); 118 | 119 | private final Rect mTempRect = new Rect(); 120 | 121 | private PagerAdapter mAdapter; 122 | private int mCurItem; // Index of currently displayed page. 123 | private int mRestoredCurItem = -1; 124 | private Parcelable mRestoredAdapterState = null; 125 | private ClassLoader mRestoredClassLoader = null; 126 | private Scroller mScroller; 127 | private PagerObserver mObserver; 128 | 129 | private int mPageMargin; 130 | private Drawable mMarginDrawable; 131 | private int mTopPageBounds; 132 | private int mBottomPageBounds; 133 | 134 | // Offsets of the first and last items, if known. 135 | // Set during population, used to determine if we are at the beginning 136 | // or end of the pager data set during touch scrolling. 137 | private float mFirstOffset = -Float.MAX_VALUE; 138 | private float mLastOffset = Float.MAX_VALUE; 139 | 140 | private int mChildWidthMeasureSpec; 141 | private int mChildHeightMeasureSpec; 142 | private boolean mInLayout; 143 | 144 | private boolean mScrollingCacheEnabled; 145 | 146 | private boolean mPopulatePending; 147 | private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; 148 | 149 | private boolean mIsBeingDragged; 150 | private boolean mIsUnableToDrag; 151 | private boolean mIgnoreGutter; 152 | private int mDefaultGutterSize; 153 | private int mGutterSize; 154 | private int mTouchSlop; 155 | /** 156 | * Position of the last motion event. 157 | */ 158 | private float mLastMotionX; 159 | private float mLastMotionY; 160 | private float mInitialMotionX; 161 | private float mInitialMotionY; 162 | /** 163 | * ID of the active pointer. This is used to retain consistency during 164 | * drags/flings if multiple pointers are used. 165 | */ 166 | private int mActivePointerId = INVALID_POINTER; 167 | /** 168 | * Sentinel value for no current active pointer. 169 | * Used by {@link #mActivePointerId}. 170 | */ 171 | private static final int INVALID_POINTER = -1; 172 | 173 | /** 174 | * Determines speed during touch scrolling 175 | */ 176 | private VelocityTracker mVelocityTracker; 177 | private int mMinimumVelocity; 178 | private int mMaximumVelocity; 179 | private int mFlingDistance; 180 | private int mCloseEnough; 181 | 182 | // If the pager is at least this close to its final position, complete the scroll 183 | // on touch down and let the user interact with the content inside instead of 184 | // "catching" the flinging pager. 185 | private static final int CLOSE_ENOUGH = 2; // dp 186 | 187 | private boolean mFakeDragging; 188 | private long mFakeDragBeginTime; 189 | 190 | private EdgeEffectCompat mLeftEdge; 191 | private EdgeEffectCompat mRightEdge; 192 | 193 | private boolean mFirstLayout = true; 194 | private boolean mNeedCalculatePageOffsets = false; 195 | private boolean mCalledSuper; 196 | private int mDecorChildCount; 197 | 198 | private OnPageChangeListener mOnPageChangeListener; 199 | private OnPageChangeListener mInternalPageChangeListener; 200 | private OnAdapterChangeListener mAdapterChangeListener; 201 | private PageTransformer mPageTransformer; 202 | private Method mSetChildrenDrawingOrderEnabled; 203 | 204 | private static final int DRAW_ORDER_DEFAULT = 0; 205 | private static final int DRAW_ORDER_FORWARD = 1; 206 | private static final int DRAW_ORDER_REVERSE = 2; 207 | private int mDrawingOrder; 208 | private ArrayList mDrawingOrderedChildren; 209 | private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); 210 | 211 | /** 212 | * Indicates that the pager is in an idle, settled state. The current page 213 | * is fully in view and no animation is in progress. 214 | */ 215 | public static final int SCROLL_STATE_IDLE = 0; 216 | 217 | /** 218 | * Indicates that the pager is currently being dragged by the user. 219 | */ 220 | public static final int SCROLL_STATE_DRAGGING = 1; 221 | 222 | /** 223 | * Indicates that the pager is in the process of settling to a final position. 224 | */ 225 | public static final int SCROLL_STATE_SETTLING = 2; 226 | 227 | private final Runnable mEndScrollRunnable = new Runnable() { 228 | public void run() { 229 | setScrollState(SCROLL_STATE_IDLE); 230 | populate(); 231 | } 232 | }; 233 | 234 | private int mScrollState = SCROLL_STATE_IDLE; 235 | 236 | /** 237 | * Callback interface for responding to changing state of the selected page. 238 | */ 239 | public interface OnPageChangeListener { 240 | 241 | /** 242 | * This method will be invoked when the current page is scrolled, either as part 243 | * of a programmatically initiated smooth scroll or a user initiated touch scroll. 244 | * 245 | * @param position Position index of the first page currently being displayed. 246 | * Page position+1 will be visible if positionOffset is nonzero. 247 | * @param positionOffset Value from [0, 1) indicating the offset from the page at position. 248 | * @param positionOffsetPixels Value in pixels indicating the offset from position. 249 | */ 250 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); 251 | 252 | /** 253 | * This method will be invoked when a new page becomes selected. Animation is not 254 | * necessarily complete. 255 | * 256 | * @param position Position index of the new selected page. 257 | */ 258 | public void onPageSelected(int position); 259 | 260 | /** 261 | * Called when the scroll state changes. Useful for discovering when the user 262 | * begins dragging, when the pager is automatically settling to the current page, 263 | * or when it is fully stopped/idle. 264 | * 265 | * @param state The new scroll state. 266 | * @see com.idunnolol.moviesdemo.view.ViewPager#SCROLL_STATE_IDLE 267 | * @see com.idunnolol.moviesdemo.view.ViewPager#SCROLL_STATE_DRAGGING 268 | * @see com.idunnolol.moviesdemo.view.ViewPager#SCROLL_STATE_SETTLING 269 | */ 270 | public void onPageScrollStateChanged(int state); 271 | } 272 | 273 | /** 274 | * Simple implementation of the {@link com.idunnolol.moviesdemo.view.ViewPager.OnPageChangeListener} interface with stub 275 | * implementations of each method. Extend this if you do not intend to override 276 | * every method of {@link com.idunnolol.moviesdemo.view.ViewPager.OnPageChangeListener}. 277 | */ 278 | public static class SimpleOnPageChangeListener implements OnPageChangeListener { 279 | @Override 280 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 281 | // This space for rent 282 | } 283 | 284 | @Override 285 | public void onPageSelected(int position) { 286 | // This space for rent 287 | } 288 | 289 | @Override 290 | public void onPageScrollStateChanged(int state) { 291 | // This space for rent 292 | } 293 | } 294 | 295 | /** 296 | * A PageTransformer is invoked whenever a visible/attached page is scrolled. 297 | * This offers an opportunity for the application to apply a custom transformation 298 | * to the page views using animation properties. 299 | * 300 | *

As property animation is only supported as of Android 3.0 and forward, 301 | * setting a PageTransformer on a ViewPager on earlier platform versions will 302 | * be ignored.

303 | */ 304 | public interface PageTransformer { 305 | /** 306 | * Apply a property transformation to the given page. 307 | * 308 | * @param page Apply the transformation to this page 309 | * @param position Position of page relative to the current front-and-center 310 | * position of the pager. 0 is front and center. 1 is one full 311 | * page position to the right, and -1 is one page position to the left. 312 | */ 313 | public void transformPage(View page, float position); 314 | } 315 | 316 | /** 317 | * Used internally to monitor when adapters are switched. 318 | */ 319 | interface OnAdapterChangeListener { 320 | public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); 321 | } 322 | 323 | /** 324 | * Used internally to tag special types of child views that should be added as 325 | * pager decorations by default. 326 | */ 327 | interface Decor {} 328 | 329 | public ViewPager(Context context) { 330 | super(context); 331 | initViewPager(); 332 | } 333 | 334 | public ViewPager(Context context, AttributeSet attrs) { 335 | super(context, attrs); 336 | initViewPager(); 337 | } 338 | 339 | void initViewPager() { 340 | setWillNotDraw(false); 341 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 342 | setFocusable(true); 343 | final Context context = getContext(); 344 | mScroller = new Scroller(context, sInterpolator); 345 | final ViewConfiguration configuration = ViewConfiguration.get(context); 346 | final float density = context.getResources().getDisplayMetrics().density; 347 | 348 | mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); 349 | mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); 350 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 351 | mLeftEdge = new EdgeEffectCompat(context); 352 | mRightEdge = new EdgeEffectCompat(context); 353 | 354 | mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); 355 | mCloseEnough = (int) (CLOSE_ENOUGH * density); 356 | mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); 357 | 358 | ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); 359 | 360 | if (ViewCompat.getImportantForAccessibility(this) 361 | == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 362 | ViewCompat.setImportantForAccessibility(this, 363 | ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 364 | } 365 | } 366 | 367 | @Override 368 | protected void onDetachedFromWindow() { 369 | removeCallbacks(mEndScrollRunnable); 370 | super.onDetachedFromWindow(); 371 | } 372 | 373 | private void setScrollState(int newState) { 374 | if (mScrollState == newState) { 375 | return; 376 | } 377 | 378 | mScrollState = newState; 379 | if (mPageTransformer != null) { 380 | // PageTransformers can do complex things that benefit from hardware layers. 381 | enableLayers(newState != SCROLL_STATE_IDLE); 382 | } 383 | if (mOnPageChangeListener != null) { 384 | mOnPageChangeListener.onPageScrollStateChanged(newState); 385 | } 386 | } 387 | 388 | /** 389 | * Set a PagerAdapter that will supply views for this pager as needed. 390 | * 391 | * @param adapter Adapter to use 392 | */ 393 | public void setAdapter(PagerAdapter adapter) { 394 | if (mAdapter != null) { 395 | mAdapter.unregisterDataSetObserver(mObserver); 396 | mAdapter.startUpdate(this); 397 | for (int i = 0; i < mItems.size(); i++) { 398 | final ItemInfo ii = mItems.get(i); 399 | mAdapter.destroyItem(this, ii.position, ii.object); 400 | } 401 | mAdapter.finishUpdate(this); 402 | mItems.clear(); 403 | removeNonDecorViews(); 404 | mCurItem = 0; 405 | scrollTo(0, 0); 406 | } 407 | 408 | final PagerAdapter oldAdapter = mAdapter; 409 | mAdapter = adapter; 410 | mExpectedAdapterCount = 0; 411 | 412 | if (mAdapter != null) { 413 | if (mObserver == null) { 414 | mObserver = new PagerObserver(); 415 | } 416 | mAdapter.registerDataSetObserver(mObserver); 417 | mPopulatePending = false; 418 | final boolean wasFirstLayout = mFirstLayout; 419 | mFirstLayout = true; 420 | mExpectedAdapterCount = mAdapter.getCount(); 421 | if (mRestoredCurItem >= 0) { 422 | mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); 423 | setCurrentItemInternal(mRestoredCurItem, false, true); 424 | mRestoredCurItem = -1; 425 | mRestoredAdapterState = null; 426 | mRestoredClassLoader = null; 427 | } else if (!wasFirstLayout) { 428 | populate(); 429 | } else { 430 | requestLayout(); 431 | } 432 | } 433 | 434 | if (mAdapterChangeListener != null && oldAdapter != adapter) { 435 | mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); 436 | } 437 | } 438 | 439 | private void removeNonDecorViews() { 440 | for (int i = 0; i < getChildCount(); i++) { 441 | final View child = getChildAt(i); 442 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 443 | if (!lp.isDecor) { 444 | removeViewAt(i); 445 | i--; 446 | } 447 | } 448 | } 449 | 450 | /** 451 | * Retrieve the current adapter supplying pages. 452 | * 453 | * @return The currently registered PagerAdapter 454 | */ 455 | public PagerAdapter getAdapter() { 456 | return mAdapter; 457 | } 458 | 459 | void setOnAdapterChangeListener(OnAdapterChangeListener listener) { 460 | mAdapterChangeListener = listener; 461 | } 462 | 463 | private int getClientWidth() { 464 | return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 465 | } 466 | 467 | /** 468 | * Set the currently selected page. If the ViewPager has already been through its first 469 | * layout with its current adapter there will be a smooth animated transition between 470 | * the current item and the specified item. 471 | * 472 | * @param item Item index to select 473 | */ 474 | public void setCurrentItem(int item) { 475 | mPopulatePending = false; 476 | setCurrentItemInternal(item, !mFirstLayout, false); 477 | } 478 | 479 | /** 480 | * Set the currently selected page. 481 | * 482 | * @param item Item index to select 483 | * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately 484 | */ 485 | public void setCurrentItem(int item, boolean smoothScroll) { 486 | mPopulatePending = false; 487 | setCurrentItemInternal(item, smoothScroll, false); 488 | } 489 | 490 | public int getCurrentItem() { 491 | return mCurItem; 492 | } 493 | 494 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { 495 | setCurrentItemInternal(item, smoothScroll, always, 0); 496 | } 497 | 498 | void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { 499 | if (mAdapter == null || mAdapter.getCount() <= 0) { 500 | setScrollingCacheEnabled(false); 501 | return; 502 | } 503 | if (!always && mCurItem == item && mItems.size() != 0) { 504 | setScrollingCacheEnabled(false); 505 | return; 506 | } 507 | 508 | if (item < 0) { 509 | item = 0; 510 | } else if (item >= mAdapter.getCount()) { 511 | item = mAdapter.getCount() - 1; 512 | } 513 | final int pageLimit = mOffscreenPageLimit; 514 | if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { 515 | // We are doing a jump by more than one page. To avoid 516 | // glitches, we want to keep all current pages in the view 517 | // until the scroll ends. 518 | for (int i=0; iNote: Prior to Android 3.0 the property animation APIs did not exist. 587 | * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.

588 | * 589 | * @param reverseDrawingOrder true if the supplied PageTransformer requires page views 590 | * to be drawn from last to first instead of first to last. 591 | * @param transformer PageTransformer that will modify each page's animation properties 592 | */ 593 | public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { 594 | if (Build.VERSION.SDK_INT >= 11) { 595 | final boolean hasTransformer = transformer != null; 596 | final boolean needsPopulate = hasTransformer != (mPageTransformer != null); 597 | mPageTransformer = transformer; 598 | setChildrenDrawingOrderEnabledCompat(hasTransformer); 599 | if (hasTransformer) { 600 | mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; 601 | } else { 602 | mDrawingOrder = DRAW_ORDER_DEFAULT; 603 | } 604 | if (needsPopulate) populate(); 605 | } 606 | } 607 | 608 | void setChildrenDrawingOrderEnabledCompat(boolean enable) { 609 | if (Build.VERSION.SDK_INT >= 7) { 610 | if (mSetChildrenDrawingOrderEnabled == null) { 611 | try { 612 | mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( 613 | "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE }); 614 | } catch (NoSuchMethodException e) { 615 | Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); 616 | } 617 | } 618 | try { 619 | mSetChildrenDrawingOrderEnabled.invoke(this, enable); 620 | } catch (Exception e) { 621 | Log.e(TAG, "Error changing children drawing order", e); 622 | } 623 | } 624 | } 625 | 626 | @Override 627 | protected int getChildDrawingOrder(int childCount, int i) { 628 | final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; 629 | final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; 630 | return result; 631 | } 632 | 633 | /** 634 | * Set a separate OnPageChangeListener for internal use by the support library. 635 | * 636 | * @param listener Listener to set 637 | * @return The old listener that was set, if any. 638 | */ 639 | OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { 640 | OnPageChangeListener oldListener = mInternalPageChangeListener; 641 | mInternalPageChangeListener = listener; 642 | return oldListener; 643 | } 644 | 645 | /** 646 | * Returns the number of pages that will be retained to either side of the 647 | * current page in the view hierarchy in an idle state. Defaults to 1. 648 | * 649 | * @return How many pages will be kept offscreen on either side 650 | * @see #setOffscreenPageLimit(int) 651 | */ 652 | public int getOffscreenPageLimit() { 653 | return mOffscreenPageLimit; 654 | } 655 | 656 | /** 657 | * Set the number of pages that should be retained to either side of the 658 | * current page in the view hierarchy in an idle state. Pages beyond this 659 | * limit will be recreated from the adapter when needed. 660 | * 661 | *

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

667 | * 668 | *

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

670 | * 671 | * @param limit How many pages will be kept offscreen in an idle state. 672 | */ 673 | public void setOffscreenPageLimit(int limit) { 674 | if (limit < DEFAULT_OFFSCREEN_PAGES) { 675 | Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + 676 | DEFAULT_OFFSCREEN_PAGES); 677 | limit = DEFAULT_OFFSCREEN_PAGES; 678 | } 679 | if (limit != mOffscreenPageLimit) { 680 | mOffscreenPageLimit = limit; 681 | populate(); 682 | } 683 | } 684 | 685 | /** 686 | * Set the margin between pages. 687 | * 688 | * @param marginPixels Distance between adjacent pages in pixels 689 | * @see #getPageMargin() 690 | * @see #setPageMarginDrawable(android.graphics.drawable.Drawable) 691 | * @see #setPageMarginDrawable(int) 692 | */ 693 | public void setPageMargin(int marginPixels) { 694 | final int oldMargin = mPageMargin; 695 | mPageMargin = marginPixels; 696 | 697 | final int width = getWidth(); 698 | recomputeScrollPosition(width, width, marginPixels, oldMargin); 699 | 700 | requestLayout(); 701 | } 702 | 703 | /** 704 | * Return the margin between pages. 705 | * 706 | * @return The size of the margin in pixels 707 | */ 708 | public int getPageMargin() { 709 | return mPageMargin; 710 | } 711 | 712 | /** 713 | * Set a drawable that will be used to fill the margin between pages. 714 | * 715 | * @param d Drawable to display between pages 716 | */ 717 | public void setPageMarginDrawable(Drawable d) { 718 | mMarginDrawable = d; 719 | if (d != null) refreshDrawableState(); 720 | setWillNotDraw(d == null); 721 | invalidate(); 722 | } 723 | 724 | /** 725 | * Set a drawable that will be used to fill the margin between pages. 726 | * 727 | * @param resId Resource ID of a drawable to display between pages 728 | */ 729 | public void setPageMarginDrawable(int resId) { 730 | setPageMarginDrawable(getContext().getResources().getDrawable(resId)); 731 | } 732 | 733 | @Override 734 | protected boolean verifyDrawable(Drawable who) { 735 | return super.verifyDrawable(who) || who == mMarginDrawable; 736 | } 737 | 738 | @Override 739 | protected void drawableStateChanged() { 740 | super.drawableStateChanged(); 741 | final Drawable d = mMarginDrawable; 742 | if (d != null && d.isStateful()) { 743 | d.setState(getDrawableState()); 744 | } 745 | } 746 | 747 | // We want the duration of the page snap animation to be influenced by the distance that 748 | // the screen has to travel, however, we don't want this duration to be effected in a 749 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance 750 | // of travel has on the overall snap duration. 751 | float distanceInfluenceForSnapDuration(float f) { 752 | f -= 0.5f; // center the values about 0. 753 | f *= 0.3f * Math.PI / 2.0f; 754 | return (float) Math.sin(f); 755 | } 756 | 757 | /** 758 | * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. 759 | * 760 | * @param x the number of pixels to scroll by on the X axis 761 | * @param y the number of pixels to scroll by on the Y axis 762 | */ 763 | void smoothScrollTo(int x, int y) { 764 | smoothScrollTo(x, y, 0); 765 | } 766 | 767 | /** 768 | * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately. 769 | * 770 | * @param x the number of pixels to scroll by on the X axis 771 | * @param y the number of pixels to scroll by on the Y axis 772 | * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) 773 | */ 774 | void smoothScrollTo(int x, int y, int velocity) { 775 | if (getChildCount() == 0) { 776 | // Nothing to do. 777 | setScrollingCacheEnabled(false); 778 | return; 779 | } 780 | int sx = getScrollX(); 781 | int sy = getScrollY(); 782 | int dx = x - sx; 783 | int dy = y - sy; 784 | if (dx == 0 && dy == 0) { 785 | completeScroll(false); 786 | populate(); 787 | setScrollState(SCROLL_STATE_IDLE); 788 | return; 789 | } 790 | 791 | setScrollingCacheEnabled(true); 792 | setScrollState(SCROLL_STATE_SETTLING); 793 | 794 | final int width = getClientWidth(); 795 | final int halfWidth = width / 2; 796 | final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); 797 | final float distance = halfWidth + halfWidth * 798 | distanceInfluenceForSnapDuration(distanceRatio); 799 | 800 | int duration = 0; 801 | velocity = Math.abs(velocity); 802 | if (velocity > 0) { 803 | duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 804 | } else { 805 | final float pageWidth = width * mAdapter.getPageWidth(mCurItem); 806 | final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); 807 | duration = (int) ((pageDelta + 1) * 300); 808 | } 809 | duration = Math.min(duration, MAX_SETTLE_DURATION); 810 | 811 | mScroller.startScroll(sx, sy, dx, dy, duration); 812 | ViewCompat.postInvalidateOnAnimation(this); 813 | } 814 | 815 | ItemInfo addNewItem(int position, int index) { 816 | ItemInfo ii = new ItemInfo(); 817 | ii.position = position; 818 | ii.object = mAdapter.instantiateItem(this, position); 819 | ii.widthFactor = mAdapter.getPageWidth(position); 820 | if (index < 0 || index >= mItems.size()) { 821 | mItems.add(ii); 822 | } else { 823 | mItems.add(index, ii); 824 | } 825 | return ii; 826 | } 827 | 828 | void dataSetChanged() { 829 | // This method only gets called if our observer is attached, so mAdapter is non-null. 830 | 831 | final int adapterCount = mAdapter.getCount(); 832 | mExpectedAdapterCount = adapterCount; 833 | boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && 834 | mItems.size() < adapterCount; 835 | int newCurrItem = mCurItem; 836 | 837 | boolean isUpdating = false; 838 | for (int i = 0; i < mItems.size(); i++) { 839 | final ItemInfo ii = mItems.get(i); 840 | final int newPos = mAdapter.getItemPosition(ii.object); 841 | 842 | if (newPos == PagerAdapter.POSITION_UNCHANGED) { 843 | continue; 844 | } 845 | 846 | if (newPos == PagerAdapter.POSITION_NONE) { 847 | mItems.remove(i); 848 | i--; 849 | 850 | if (!isUpdating) { 851 | mAdapter.startUpdate(this); 852 | isUpdating = true; 853 | } 854 | 855 | mAdapter.destroyItem(this, ii.position, ii.object); 856 | needPopulate = true; 857 | 858 | if (mCurItem == ii.position) { 859 | // Keep the current item in the valid range 860 | newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); 861 | needPopulate = true; 862 | } 863 | continue; 864 | } 865 | 866 | if (ii.position != newPos) { 867 | if (ii.position == mCurItem) { 868 | // Our current item changed position. Follow it. 869 | newCurrItem = newPos; 870 | } 871 | 872 | ii.position = newPos; 873 | needPopulate = true; 874 | } 875 | } 876 | 877 | if (isUpdating) { 878 | mAdapter.finishUpdate(this); 879 | } 880 | 881 | Collections.sort(mItems, COMPARATOR); 882 | 883 | if (needPopulate) { 884 | // Reset our known page widths; populate will recompute them. 885 | final int childCount = getChildCount(); 886 | for (int i = 0; i < childCount; i++) { 887 | final View child = getChildAt(i); 888 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 889 | if (!lp.isDecor) { 890 | lp.widthFactor = 0.f; 891 | } 892 | } 893 | 894 | setCurrentItemInternal(newCurrItem, false, true); 895 | requestLayout(); 896 | } 897 | } 898 | 899 | void populate() { 900 | populate(mCurItem); 901 | } 902 | 903 | void populate(int newCurrentItem) { 904 | ItemInfo oldCurInfo = null; 905 | int focusDirection = View.FOCUS_FORWARD; 906 | if (mCurItem != newCurrentItem) { 907 | focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 908 | oldCurInfo = infoForPosition(mCurItem); 909 | mCurItem = newCurrentItem; 910 | } 911 | 912 | if (mAdapter == null) { 913 | sortChildDrawingOrder(); 914 | return; 915 | } 916 | 917 | // Bail now if we are waiting to populate. This is to hold off 918 | // on creating views from the time the user releases their finger to 919 | // fling to a new position until we have finished the scroll to 920 | // that position, avoiding glitches from happening at that point. 921 | if (mPopulatePending) { 922 | if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); 923 | sortChildDrawingOrder(); 924 | return; 925 | } 926 | 927 | // Also, don't populate until we are attached to a window. This is to 928 | // avoid trying to populate before we have restored our view hierarchy 929 | // state and conflicting with what is restored. 930 | if (getWindowToken() == null) { 931 | return; 932 | } 933 | 934 | mAdapter.startUpdate(this); 935 | 936 | final int pageLimit = mOffscreenPageLimit; 937 | final int startPos = Math.max(0, mCurItem - pageLimit); 938 | final int N = mAdapter.getCount(); 939 | final int endPos = Math.min(N-1, mCurItem + pageLimit); 940 | 941 | if (N != mExpectedAdapterCount) { 942 | String resName; 943 | try { 944 | resName = getResources().getResourceName(getId()); 945 | } catch (Resources.NotFoundException e) { 946 | resName = Integer.toHexString(getId()); 947 | } 948 | throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + 949 | " contents without calling PagerAdapter#notifyDataSetChanged!" + 950 | " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + 951 | " Pager id: " + resName + 952 | " Pager class: " + getClass() + 953 | " Problematic adapter: " + mAdapter.getClass()); 954 | } 955 | 956 | // Locate the currently focused item or add it if needed. 957 | int curIndex = -1; 958 | ItemInfo curItem = null; 959 | for (curIndex = 0; curIndex < mItems.size(); curIndex++) { 960 | final ItemInfo ii = mItems.get(curIndex); 961 | if (ii.position >= mCurItem) { 962 | if (ii.position == mCurItem) curItem = ii; 963 | break; 964 | } 965 | } 966 | 967 | if (curItem == null && N > 0) { 968 | curItem = addNewItem(mCurItem, curIndex); 969 | } 970 | 971 | // Fill 3x the available width or up to the number of offscreen 972 | // pages requested to either side, whichever is larger. 973 | // If we have no current item we have no work to do. 974 | if (curItem != null) { 975 | float extraWidthLeft = 0.f; 976 | int itemIndex = curIndex - 1; 977 | ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 978 | final int clientWidth = getClientWidth(); 979 | final float leftWidthNeeded = clientWidth <= 0 ? 0 : 980 | 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; 981 | for (int pos = mCurItem - 1; pos >= 0; pos--) { 982 | if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { 983 | if (ii == null) { 984 | break; 985 | } 986 | if (pos == ii.position && !ii.scrolling) { 987 | mItems.remove(itemIndex); 988 | mAdapter.destroyItem(this, pos, ii.object); 989 | if (DEBUG) { 990 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 991 | " view: " + ((View) ii.object)); 992 | } 993 | itemIndex--; 994 | curIndex--; 995 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 996 | } 997 | } else if (ii != null && pos == ii.position) { 998 | extraWidthLeft += ii.widthFactor; 999 | itemIndex--; 1000 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1001 | } else { 1002 | ii = addNewItem(pos, itemIndex + 1); 1003 | extraWidthLeft += ii.widthFactor; 1004 | curIndex++; 1005 | ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 1006 | } 1007 | } 1008 | 1009 | float extraWidthRight = curItem.widthFactor; 1010 | itemIndex = curIndex + 1; 1011 | if (extraWidthRight < 2.f) { 1012 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1013 | final float rightWidthNeeded = clientWidth <= 0 ? 0 : 1014 | (float) getPaddingRight() / (float) clientWidth + 2.f; 1015 | for (int pos = mCurItem + 1; pos < N; pos++) { 1016 | if (extraWidthRight >= rightWidthNeeded && pos > endPos) { 1017 | if (ii == null) { 1018 | break; 1019 | } 1020 | if (pos == ii.position && !ii.scrolling) { 1021 | mItems.remove(itemIndex); 1022 | mAdapter.destroyItem(this, pos, ii.object); 1023 | if (DEBUG) { 1024 | Log.i(TAG, "populate() - destroyItem() with pos: " + pos + 1025 | " view: " + ((View) ii.object)); 1026 | } 1027 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1028 | } 1029 | } else if (ii != null && pos == ii.position) { 1030 | extraWidthRight += ii.widthFactor; 1031 | itemIndex++; 1032 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1033 | } else { 1034 | ii = addNewItem(pos, itemIndex); 1035 | itemIndex++; 1036 | extraWidthRight += ii.widthFactor; 1037 | ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; 1038 | } 1039 | } 1040 | } 1041 | 1042 | calculatePageOffsets(curItem, curIndex, oldCurInfo); 1043 | } 1044 | 1045 | if (DEBUG) { 1046 | Log.i(TAG, "Current page list:"); 1047 | for (int i=0; i(); 1095 | } else { 1096 | mDrawingOrderedChildren.clear(); 1097 | } 1098 | final int childCount = getChildCount(); 1099 | for (int i = 0; i < childCount; i++) { 1100 | final View child = getChildAt(i); 1101 | mDrawingOrderedChildren.add(child); 1102 | } 1103 | Collections.sort(mDrawingOrderedChildren, sPositionComparator); 1104 | } 1105 | } 1106 | 1107 | private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { 1108 | final int N = mAdapter.getCount(); 1109 | final int width = getClientWidth(); 1110 | final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 1111 | // Fix up offsets for later layout. 1112 | if (oldCurInfo != null) { 1113 | final int oldCurPosition = oldCurInfo.position; 1114 | // Base offsets off of oldCurInfo. 1115 | if (oldCurPosition < curItem.position) { 1116 | int itemIndex = 0; 1117 | ItemInfo ii = null; 1118 | float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; 1119 | for (int pos = oldCurPosition + 1; 1120 | pos <= curItem.position && itemIndex < mItems.size(); pos++) { 1121 | ii = mItems.get(itemIndex); 1122 | while (pos > ii.position && itemIndex < mItems.size() - 1) { 1123 | itemIndex++; 1124 | ii = mItems.get(itemIndex); 1125 | } 1126 | while (pos < ii.position) { 1127 | // We don't have an item populated for this, 1128 | // ask the adapter for an offset. 1129 | offset += mAdapter.getPageWidth(pos) + marginOffset; 1130 | pos++; 1131 | } 1132 | ii.offset = offset; 1133 | offset += ii.widthFactor + marginOffset; 1134 | } 1135 | } else if (oldCurPosition > curItem.position) { 1136 | int itemIndex = mItems.size() - 1; 1137 | ItemInfo ii = null; 1138 | float offset = oldCurInfo.offset; 1139 | for (int pos = oldCurPosition - 1; 1140 | pos >= curItem.position && itemIndex >= 0; pos--) { 1141 | ii = mItems.get(itemIndex); 1142 | while (pos < ii.position && itemIndex > 0) { 1143 | itemIndex--; 1144 | ii = mItems.get(itemIndex); 1145 | } 1146 | while (pos > ii.position) { 1147 | // We don't have an item populated for this, 1148 | // ask the adapter for an offset. 1149 | offset -= mAdapter.getPageWidth(pos) + marginOffset; 1150 | pos--; 1151 | } 1152 | offset -= ii.widthFactor + marginOffset; 1153 | ii.offset = offset; 1154 | } 1155 | } 1156 | } 1157 | 1158 | // Base all offsets off of curItem. 1159 | final int itemCount = mItems.size(); 1160 | float offset = curItem.offset; 1161 | int pos = curItem.position - 1; 1162 | mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; 1163 | mLastOffset = curItem.position == N - 1 ? 1164 | curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; 1165 | // Previous pages 1166 | for (int i = curIndex - 1; i >= 0; i--, pos--) { 1167 | final ItemInfo ii = mItems.get(i); 1168 | while (pos > ii.position) { 1169 | offset -= mAdapter.getPageWidth(pos--) + marginOffset; 1170 | } 1171 | offset -= ii.widthFactor + marginOffset; 1172 | ii.offset = offset; 1173 | if (ii.position == 0) mFirstOffset = offset; 1174 | } 1175 | offset = curItem.offset + curItem.widthFactor + marginOffset; 1176 | pos = curItem.position + 1; 1177 | // Next pages 1178 | for (int i = curIndex + 1; i < itemCount; i++, pos++) { 1179 | final ItemInfo ii = mItems.get(i); 1180 | while (pos < ii.position) { 1181 | offset += mAdapter.getPageWidth(pos++) + marginOffset; 1182 | } 1183 | if (ii.position == N - 1) { 1184 | mLastOffset = offset + ii.widthFactor - 1; 1185 | } 1186 | ii.offset = offset; 1187 | offset += ii.widthFactor + marginOffset; 1188 | } 1189 | 1190 | mNeedCalculatePageOffsets = false; 1191 | } 1192 | 1193 | /** 1194 | * This is the persistent state that is saved by ViewPager. Only needed 1195 | * if you are creating a sublass of ViewPager that must save its own 1196 | * state, in which case it should implement a subclass of this which 1197 | * contains that state. 1198 | */ 1199 | public static class SavedState extends BaseSavedState { 1200 | int position; 1201 | Parcelable adapterState; 1202 | ClassLoader loader; 1203 | 1204 | public SavedState(Parcelable superState) { 1205 | super(superState); 1206 | } 1207 | 1208 | @Override 1209 | public void writeToParcel(Parcel out, int flags) { 1210 | super.writeToParcel(out, flags); 1211 | out.writeInt(position); 1212 | out.writeParcelable(adapterState, flags); 1213 | } 1214 | 1215 | @Override 1216 | public String toString() { 1217 | return "FragmentPager.SavedState{" 1218 | + Integer.toHexString(System.identityHashCode(this)) 1219 | + " position=" + position + "}"; 1220 | } 1221 | 1222 | public static final Creator CREATOR 1223 | = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { 1224 | @Override 1225 | public SavedState createFromParcel(Parcel in, ClassLoader loader) { 1226 | return new SavedState(in, loader); 1227 | } 1228 | @Override 1229 | public SavedState[] newArray(int size) { 1230 | return new SavedState[size]; 1231 | } 1232 | }); 1233 | 1234 | SavedState(Parcel in, ClassLoader loader) { 1235 | super(in); 1236 | if (loader == null) { 1237 | loader = getClass().getClassLoader(); 1238 | } 1239 | position = in.readInt(); 1240 | adapterState = in.readParcelable(loader); 1241 | this.loader = loader; 1242 | } 1243 | } 1244 | 1245 | @Override 1246 | public Parcelable onSaveInstanceState() { 1247 | Parcelable superState = super.onSaveInstanceState(); 1248 | SavedState ss = new SavedState(superState); 1249 | ss.position = mCurItem; 1250 | if (mAdapter != null) { 1251 | ss.adapterState = mAdapter.saveState(); 1252 | } 1253 | return ss; 1254 | } 1255 | 1256 | @Override 1257 | public void onRestoreInstanceState(Parcelable state) { 1258 | if (!(state instanceof SavedState)) { 1259 | super.onRestoreInstanceState(state); 1260 | return; 1261 | } 1262 | 1263 | SavedState ss = (SavedState)state; 1264 | super.onRestoreInstanceState(ss.getSuperState()); 1265 | 1266 | if (mAdapter != null) { 1267 | mAdapter.restoreState(ss.adapterState, ss.loader); 1268 | setCurrentItemInternal(ss.position, false, true); 1269 | } else { 1270 | mRestoredCurItem = ss.position; 1271 | mRestoredAdapterState = ss.adapterState; 1272 | mRestoredClassLoader = ss.loader; 1273 | } 1274 | } 1275 | 1276 | @Override 1277 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 1278 | if (!checkLayoutParams(params)) { 1279 | params = generateLayoutParams(params); 1280 | } 1281 | final LayoutParams lp = (LayoutParams) params; 1282 | lp.isDecor |= child instanceof Decor; 1283 | if (mInLayout) { 1284 | if (lp != null && lp.isDecor) { 1285 | throw new IllegalStateException("Cannot add pager decor view during layout"); 1286 | } 1287 | lp.needsMeasure = true; 1288 | addViewInLayout(child, index, params); 1289 | } else { 1290 | super.addView(child, index, params); 1291 | } 1292 | 1293 | if (USE_CACHE) { 1294 | if (child.getVisibility() != GONE) { 1295 | child.setDrawingCacheEnabled(mScrollingCacheEnabled); 1296 | } else { 1297 | child.setDrawingCacheEnabled(false); 1298 | } 1299 | } 1300 | } 1301 | 1302 | @Override 1303 | public void removeView(View view) { 1304 | if (mInLayout) { 1305 | removeViewInLayout(view); 1306 | } else { 1307 | super.removeView(view); 1308 | } 1309 | } 1310 | 1311 | ItemInfo infoForChild(View child) { 1312 | for (int i=0; i 0 && !mItems.isEmpty()) { 1455 | final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; 1456 | final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() 1457 | + oldMargin; 1458 | final int xpos = getScrollX(); 1459 | final float pageOffset = (float) xpos / oldWidthWithMargin; 1460 | final int newOffsetPixels = (int) (pageOffset * widthWithMargin); 1461 | 1462 | scrollTo(newOffsetPixels, getScrollY()); 1463 | if (!mScroller.isFinished()) { 1464 | // We now return to your regularly scheduled scroll, already in progress. 1465 | final int newDuration = mScroller.getDuration() - mScroller.timePassed(); 1466 | ItemInfo targetInfo = infoForPosition(mCurItem); 1467 | mScroller.startScroll(newOffsetPixels, 0, 1468 | (int) (targetInfo.offset * width), 0, newDuration); 1469 | } 1470 | } else { 1471 | final ItemInfo ii = infoForPosition(mCurItem); 1472 | final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; 1473 | final int scrollPos = (int) (scrollOffset * 1474 | (width - getPaddingLeft() - getPaddingRight())); 1475 | if (scrollPos != getScrollX()) { 1476 | completeScroll(false); 1477 | scrollTo(scrollPos, getScrollY()); 1478 | } 1479 | } 1480 | } 1481 | 1482 | @Override 1483 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1484 | final int count = getChildCount(); 1485 | int width = r - l; 1486 | int height = b - t; 1487 | int paddingLeft = getPaddingLeft(); 1488 | int paddingTop = getPaddingTop(); 1489 | int paddingRight = getPaddingRight(); 1490 | int paddingBottom = getPaddingBottom(); 1491 | final int scrollX = getScrollX(); 1492 | 1493 | int decorCount = 0; 1494 | 1495 | // First pass - decor views. We need to do this in two passes so that 1496 | // we have the proper offsets for non-decor views later. 1497 | for (int i = 0; i < count; i++) { 1498 | final View child = getChildAt(i); 1499 | if (child.getVisibility() != GONE) { 1500 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1501 | int childLeft = 0; 1502 | int childTop = 0; 1503 | if (lp.isDecor) { 1504 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1505 | final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 1506 | switch (hgrav) { 1507 | default: 1508 | childLeft = paddingLeft; 1509 | break; 1510 | case Gravity.LEFT: 1511 | childLeft = paddingLeft; 1512 | paddingLeft += child.getMeasuredWidth(); 1513 | break; 1514 | case Gravity.CENTER_HORIZONTAL: 1515 | childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1516 | paddingLeft); 1517 | break; 1518 | case Gravity.RIGHT: 1519 | childLeft = width - paddingRight - child.getMeasuredWidth(); 1520 | paddingRight += child.getMeasuredWidth(); 1521 | break; 1522 | } 1523 | switch (vgrav) { 1524 | default: 1525 | childTop = paddingTop; 1526 | break; 1527 | case Gravity.TOP: 1528 | childTop = paddingTop; 1529 | paddingTop += child.getMeasuredHeight(); 1530 | break; 1531 | case Gravity.CENTER_VERTICAL: 1532 | childTop = Math.max((height - child.getMeasuredHeight()) / 2, 1533 | paddingTop); 1534 | break; 1535 | case Gravity.BOTTOM: 1536 | childTop = height - paddingBottom - child.getMeasuredHeight(); 1537 | paddingBottom += child.getMeasuredHeight(); 1538 | break; 1539 | } 1540 | childLeft += scrollX; 1541 | child.layout(childLeft, childTop, 1542 | childLeft + child.getMeasuredWidth(), 1543 | childTop + child.getMeasuredHeight()); 1544 | decorCount++; 1545 | } 1546 | } 1547 | } 1548 | 1549 | final int childWidth = width - paddingLeft - paddingRight; 1550 | // Page views. Do this once we have the right padding offsets from above. 1551 | for (int i = 0; i < count; i++) { 1552 | final View child = getChildAt(i); 1553 | if (child.getVisibility() != GONE) { 1554 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1555 | ItemInfo ii; 1556 | if (!lp.isDecor && (ii = infoForChild(child)) != null) { 1557 | int loff = (int) (childWidth * ii.offset); 1558 | int childLeft = paddingLeft + loff; 1559 | int childTop = paddingTop; 1560 | if (lp.needsMeasure) { 1561 | // This was added during layout and needs measurement. 1562 | // Do it now that we know what we're working with. 1563 | lp.needsMeasure = false; 1564 | final int widthSpec = MeasureSpec.makeMeasureSpec( 1565 | (int) (childWidth * lp.widthFactor), 1566 | MeasureSpec.EXACTLY); 1567 | final int heightSpec = MeasureSpec.makeMeasureSpec( 1568 | (int) (height - paddingTop - paddingBottom), 1569 | MeasureSpec.EXACTLY); 1570 | child.measure(widthSpec, heightSpec); 1571 | } 1572 | if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object 1573 | + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() 1574 | + "x" + child.getMeasuredHeight()); 1575 | child.layout(childLeft, childTop, 1576 | childLeft + child.getMeasuredWidth(), 1577 | childTop + child.getMeasuredHeight()); 1578 | } 1579 | } 1580 | } 1581 | mTopPageBounds = paddingTop; 1582 | mBottomPageBounds = height - paddingBottom; 1583 | mDecorChildCount = decorCount; 1584 | 1585 | if (mFirstLayout) { 1586 | scrollToItem(mCurItem, false, 0, false); 1587 | } 1588 | mFirstLayout = false; 1589 | } 1590 | 1591 | @Override 1592 | public void computeScroll() { 1593 | if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 1594 | int oldX = getScrollX(); 1595 | int oldY = getScrollY(); 1596 | int x = mScroller.getCurrX(); 1597 | int y = mScroller.getCurrY(); 1598 | 1599 | if (oldX != x || oldY != y) { 1600 | scrollTo(x, y); 1601 | if (!pageScrolled(x)) { 1602 | mScroller.abortAnimation(); 1603 | scrollTo(0, y); 1604 | } 1605 | } 1606 | 1607 | // Keep on drawing until the animation has finished. 1608 | ViewCompat.postInvalidateOnAnimation(this); 1609 | return; 1610 | } 1611 | 1612 | // Done with scroll, clean up state. 1613 | completeScroll(true); 1614 | } 1615 | 1616 | private boolean pageScrolled(int xpos) { 1617 | if (mItems.size() == 0) { 1618 | mCalledSuper = false; 1619 | onPageScrolled(0, 0, 0); 1620 | if (!mCalledSuper) { 1621 | throw new IllegalStateException( 1622 | "onPageScrolled did not call superclass implementation"); 1623 | } 1624 | return false; 1625 | } 1626 | final ItemInfo ii = infoForCurrentScrollPosition(); 1627 | final int width = getClientWidth(); 1628 | final int widthWithMargin = width + mPageMargin; 1629 | final float marginOffset = (float) mPageMargin / width; 1630 | final int currentPage = ii.position; 1631 | final float pageOffset = (((float) xpos / width) - ii.offset) / 1632 | (ii.widthFactor + marginOffset); 1633 | final int offsetPixels = (int) (pageOffset * widthWithMargin); 1634 | 1635 | mCalledSuper = false; 1636 | onPageScrolled(currentPage, pageOffset, offsetPixels); 1637 | if (!mCalledSuper) { 1638 | throw new IllegalStateException( 1639 | "onPageScrolled did not call superclass implementation"); 1640 | } 1641 | return true; 1642 | } 1643 | 1644 | /** 1645 | * This method will be invoked when the current page is scrolled, either as part 1646 | * of a programmatically initiated smooth scroll or a user initiated touch scroll. 1647 | * If you override this method you must call through to the superclass implementation 1648 | * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled 1649 | * returns. 1650 | * 1651 | * @param position Position index of the first page currently being displayed. 1652 | * Page position+1 will be visible if positionOffset is nonzero. 1653 | * @param offset Value from [0, 1) indicating the offset from the page at position. 1654 | * @param offsetPixels Value in pixels indicating the offset from position. 1655 | */ 1656 | protected void onPageScrolled(int position, float offset, int offsetPixels) { 1657 | // Offset any decor views if needed - keep them on-screen at all times. 1658 | if (mDecorChildCount > 0) { 1659 | final int scrollX = getScrollX(); 1660 | int paddingLeft = getPaddingLeft(); 1661 | int paddingRight = getPaddingRight(); 1662 | final int width = getWidth(); 1663 | final int childCount = getChildCount(); 1664 | for (int i = 0; i < childCount; i++) { 1665 | final View child = getChildAt(i); 1666 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1667 | if (!lp.isDecor) continue; 1668 | 1669 | final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1670 | int childLeft = 0; 1671 | switch (hgrav) { 1672 | default: 1673 | childLeft = paddingLeft; 1674 | break; 1675 | case Gravity.LEFT: 1676 | childLeft = paddingLeft; 1677 | paddingLeft += child.getWidth(); 1678 | break; 1679 | case Gravity.CENTER_HORIZONTAL: 1680 | childLeft = Math.max((width - child.getMeasuredWidth()) / 2, 1681 | paddingLeft); 1682 | break; 1683 | case Gravity.RIGHT: 1684 | childLeft = width - paddingRight - child.getMeasuredWidth(); 1685 | paddingRight += child.getMeasuredWidth(); 1686 | break; 1687 | } 1688 | childLeft += scrollX; 1689 | 1690 | final int childOffset = childLeft - child.getLeft(); 1691 | if (childOffset != 0) { 1692 | child.offsetLeftAndRight(childOffset); 1693 | } 1694 | } 1695 | } 1696 | 1697 | if (mOnPageChangeListener != null) { 1698 | mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1699 | } 1700 | if (mInternalPageChangeListener != null) { 1701 | mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); 1702 | } 1703 | 1704 | if (mPageTransformer != null) { 1705 | final int scrollX = getScrollX(); 1706 | final int childCount = getChildCount(); 1707 | for (int i = 0; i < childCount; i++) { 1708 | final View child = getChildAt(i); 1709 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1710 | 1711 | if (lp.isDecor) continue; 1712 | 1713 | final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); 1714 | mPageTransformer.transformPage(child, transformPos); 1715 | } 1716 | } 1717 | 1718 | mCalledSuper = true; 1719 | } 1720 | 1721 | private void completeScroll(boolean postEvents) { 1722 | boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; 1723 | if (needPopulate) { 1724 | // Done with scroll, no longer want to cache view drawing. 1725 | setScrollingCacheEnabled(false); 1726 | mScroller.abortAnimation(); 1727 | int oldX = getScrollX(); 1728 | int oldY = getScrollY(); 1729 | int x = mScroller.getCurrX(); 1730 | int y = mScroller.getCurrY(); 1731 | if (oldX != x || oldY != y) { 1732 | scrollTo(x, y); 1733 | } 1734 | } 1735 | mPopulatePending = false; 1736 | for (int i=0; i 0) || (x > getWidth() - mGutterSize && dx < 0); 1754 | } 1755 | 1756 | private void enableLayers(boolean enable) { 1757 | final int childCount = getChildCount(); 1758 | for (int i = 0; i < childCount; i++) { 1759 | final int layerType = enable ? 1760 | ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; 1761 | ViewCompat.setLayerType(getChildAt(i), layerType, null); 1762 | } 1763 | } 1764 | 1765 | @Override 1766 | public boolean onInterceptTouchEvent(MotionEvent ev) { 1767 | /* 1768 | * This method JUST determines whether we want to intercept the motion. 1769 | * If we return true, onMotionEvent will be called and we do the actual 1770 | * scrolling there. 1771 | */ 1772 | 1773 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 1774 | 1775 | // Always take care of the touch gesture being complete. 1776 | if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 1777 | // Release the drag. 1778 | if (DEBUG) Log.v(TAG, "Intercept done!"); 1779 | mIsBeingDragged = false; 1780 | mIsUnableToDrag = false; 1781 | mActivePointerId = INVALID_POINTER; 1782 | if (mVelocityTracker != null) { 1783 | mVelocityTracker.recycle(); 1784 | mVelocityTracker = null; 1785 | } 1786 | return false; 1787 | } 1788 | 1789 | // Nothing more to do here if we have decided whether or not we 1790 | // are dragging. 1791 | if (action != MotionEvent.ACTION_DOWN) { 1792 | if (mIsBeingDragged) { 1793 | if (DEBUG) Log.v(TAG, "Intercept returning true!"); 1794 | return true; 1795 | } 1796 | if (mIsUnableToDrag) { 1797 | if (DEBUG) Log.v(TAG, "Intercept returning false!"); 1798 | return false; 1799 | } 1800 | } 1801 | 1802 | switch (action) { 1803 | case MotionEvent.ACTION_MOVE: { 1804 | /* 1805 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1806 | * whether the user has moved far enough from his original down touch. 1807 | */ 1808 | 1809 | /* 1810 | * Locally do absolute value. mLastMotionY is set to the y value 1811 | * of the down event. 1812 | */ 1813 | final int activePointerId = mActivePointerId; 1814 | if (activePointerId == INVALID_POINTER) { 1815 | // If we don't have a valid id, the touch down wasn't on content. 1816 | break; 1817 | } 1818 | 1819 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1820 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1821 | final float dx = x - mLastMotionX; 1822 | final float xDiff = Math.abs(dx); 1823 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1824 | final float yDiff = Math.abs(y - mInitialMotionY); 1825 | if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1826 | 1827 | if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && 1828 | canScroll(this, false, (int) dx, (int) x, (int) y)) { 1829 | // Nested view has scrollable area under this point. Let it be handled there. 1830 | mLastMotionX = x; 1831 | mLastMotionY = y; 1832 | mIsUnableToDrag = true; 1833 | return false; 1834 | } 1835 | if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { 1836 | if (DEBUG) Log.v(TAG, "Starting drag!"); 1837 | mIsBeingDragged = true; 1838 | setScrollState(SCROLL_STATE_DRAGGING); 1839 | mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : 1840 | mInitialMotionX - mTouchSlop; 1841 | mLastMotionY = y; 1842 | setScrollingCacheEnabled(true); 1843 | } else if (yDiff > mTouchSlop) { 1844 | // The finger has moved enough in the vertical 1845 | // direction to be counted as a drag... abort 1846 | // any attempt to drag horizontally, to work correctly 1847 | // with children that have scrolling containers. 1848 | if (DEBUG) Log.v(TAG, "Starting unable to drag!"); 1849 | mIsUnableToDrag = true; 1850 | } 1851 | if (mIsBeingDragged) { 1852 | // Scroll to follow the motion event 1853 | if (performDrag(x)) { 1854 | ViewCompat.postInvalidateOnAnimation(this); 1855 | } 1856 | } 1857 | break; 1858 | } 1859 | 1860 | case MotionEvent.ACTION_DOWN: { 1861 | /* 1862 | * Remember location of down touch. 1863 | * ACTION_DOWN always refers to pointer index 0. 1864 | */ 1865 | mLastMotionX = mInitialMotionX = ev.getX(); 1866 | mLastMotionY = mInitialMotionY = ev.getY(); 1867 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1868 | mIsUnableToDrag = false; 1869 | 1870 | mScroller.computeScrollOffset(); 1871 | if (mScrollState == SCROLL_STATE_SETTLING && 1872 | Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { 1873 | // Let the user 'catch' the pager as it animates. 1874 | mScroller.abortAnimation(); 1875 | mPopulatePending = false; 1876 | populate(); 1877 | mIsBeingDragged = true; 1878 | setScrollState(SCROLL_STATE_DRAGGING); 1879 | } else { 1880 | completeScroll(false); 1881 | mIsBeingDragged = false; 1882 | } 1883 | 1884 | if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY 1885 | + " mIsBeingDragged=" + mIsBeingDragged 1886 | + "mIsUnableToDrag=" + mIsUnableToDrag); 1887 | break; 1888 | } 1889 | 1890 | case MotionEventCompat.ACTION_POINTER_UP: 1891 | onSecondaryPointerUp(ev); 1892 | break; 1893 | } 1894 | 1895 | if (mVelocityTracker == null) { 1896 | mVelocityTracker = VelocityTracker.obtain(); 1897 | } 1898 | mVelocityTracker.addMovement(ev); 1899 | 1900 | /* 1901 | * The only time we want to intercept motion events is if we are in the 1902 | * drag mode. 1903 | */ 1904 | return mIsBeingDragged; 1905 | } 1906 | 1907 | @Override 1908 | public boolean onTouchEvent(MotionEvent ev) { 1909 | if (mFakeDragging) { 1910 | // A fake drag is in progress already, ignore this real one 1911 | // but still eat the touch events. 1912 | // (It is likely that the user is multi-touching the screen.) 1913 | return true; 1914 | } 1915 | 1916 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 1917 | // Don't handle edge touches immediately -- they may actually belong to one of our 1918 | // descendants. 1919 | return false; 1920 | } 1921 | 1922 | if (mAdapter == null || mAdapter.getCount() == 0) { 1923 | // Nothing to present or scroll; nothing to touch. 1924 | return false; 1925 | } 1926 | 1927 | if (mVelocityTracker == null) { 1928 | mVelocityTracker = VelocityTracker.obtain(); 1929 | } 1930 | mVelocityTracker.addMovement(ev); 1931 | 1932 | final int action = ev.getAction(); 1933 | boolean needsInvalidate = false; 1934 | 1935 | switch (action & MotionEventCompat.ACTION_MASK) { 1936 | case MotionEvent.ACTION_DOWN: { 1937 | mScroller.abortAnimation(); 1938 | mPopulatePending = false; 1939 | populate(); 1940 | mIsBeingDragged = true; 1941 | setScrollState(SCROLL_STATE_DRAGGING); 1942 | 1943 | // Remember where the motion event started 1944 | mLastMotionX = mInitialMotionX = ev.getX(); 1945 | mLastMotionY = mInitialMotionY = ev.getY(); 1946 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 1947 | break; 1948 | } 1949 | case MotionEvent.ACTION_MOVE: 1950 | if (!mIsBeingDragged) { 1951 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1952 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1953 | final float xDiff = Math.abs(x - mLastMotionX); 1954 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1955 | final float yDiff = Math.abs(y - mLastMotionY); 1956 | if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); 1957 | if (xDiff > mTouchSlop && xDiff > yDiff) { 1958 | if (DEBUG) Log.v(TAG, "Starting drag!"); 1959 | mIsBeingDragged = true; 1960 | mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : 1961 | mInitialMotionX - mTouchSlop; 1962 | mLastMotionY = y; 1963 | setScrollState(SCROLL_STATE_DRAGGING); 1964 | setScrollingCacheEnabled(true); 1965 | } 1966 | } 1967 | // Not else! Note that mIsBeingDragged can be set above. 1968 | if (mIsBeingDragged) { 1969 | // Scroll to follow the motion event 1970 | final int activePointerIndex = MotionEventCompat.findPointerIndex( 1971 | ev, mActivePointerId); 1972 | final float x = MotionEventCompat.getX(ev, activePointerIndex); 1973 | needsInvalidate |= performDrag(x); 1974 | } 1975 | break; 1976 | case MotionEvent.ACTION_UP: 1977 | if (mIsBeingDragged) { 1978 | final VelocityTracker velocityTracker = mVelocityTracker; 1979 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1980 | int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 1981 | velocityTracker, mActivePointerId); 1982 | mPopulatePending = true; 1983 | final int width = getClientWidth(); 1984 | final int scrollX = getScrollX(); 1985 | final ItemInfo ii = infoForCurrentScrollPosition(); 1986 | final int currentPage = ii.position; 1987 | final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 1988 | final int activePointerIndex = 1989 | MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1990 | final float x = MotionEventCompat.getX(ev, activePointerIndex); 1991 | final int totalDelta = (int) (x - mInitialMotionX); 1992 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 1993 | totalDelta); 1994 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 1995 | 1996 | mActivePointerId = INVALID_POINTER; 1997 | endDrag(); 1998 | needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 1999 | } 2000 | break; 2001 | case MotionEvent.ACTION_CANCEL: 2002 | if (mIsBeingDragged) { 2003 | scrollToItem(mCurItem, true, 0, false); 2004 | mActivePointerId = INVALID_POINTER; 2005 | endDrag(); 2006 | needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); 2007 | } 2008 | break; 2009 | case MotionEventCompat.ACTION_POINTER_DOWN: { 2010 | final int index = MotionEventCompat.getActionIndex(ev); 2011 | final float x = MotionEventCompat.getX(ev, index); 2012 | mLastMotionX = x; 2013 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 2014 | break; 2015 | } 2016 | case MotionEventCompat.ACTION_POINTER_UP: 2017 | onSecondaryPointerUp(ev); 2018 | mLastMotionX = MotionEventCompat.getX(ev, 2019 | MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 2020 | break; 2021 | } 2022 | if (needsInvalidate) { 2023 | ViewCompat.postInvalidateOnAnimation(this); 2024 | } 2025 | return true; 2026 | } 2027 | 2028 | private boolean performDrag(float x) { 2029 | boolean needsInvalidate = false; 2030 | 2031 | final float deltaX = mLastMotionX - x; 2032 | mLastMotionX = x; 2033 | 2034 | float oldScrollX = getScrollX(); 2035 | float scrollX = oldScrollX + deltaX; 2036 | final int width = getClientWidth(); 2037 | 2038 | float leftBound = width * mFirstOffset; 2039 | float rightBound = width * mLastOffset; 2040 | boolean leftAbsolute = true; 2041 | boolean rightAbsolute = true; 2042 | 2043 | final ItemInfo firstItem = mItems.get(0); 2044 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2045 | if (firstItem.position != 0) { 2046 | leftAbsolute = false; 2047 | leftBound = firstItem.offset * width; 2048 | } 2049 | if (lastItem.position != mAdapter.getCount() - 1) { 2050 | rightAbsolute = false; 2051 | rightBound = lastItem.offset * width; 2052 | } 2053 | 2054 | if (scrollX < leftBound) { 2055 | if (leftAbsolute) { 2056 | float over = leftBound - scrollX; 2057 | needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); 2058 | } 2059 | scrollX = leftBound; 2060 | } else if (scrollX > rightBound) { 2061 | if (rightAbsolute) { 2062 | float over = scrollX - rightBound; 2063 | needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); 2064 | } 2065 | scrollX = rightBound; 2066 | } 2067 | // Don't lose the rounded component 2068 | mLastMotionX += scrollX - (int) scrollX; 2069 | scrollTo((int) scrollX, getScrollY()); 2070 | pageScrolled((int) scrollX); 2071 | 2072 | return needsInvalidate; 2073 | } 2074 | 2075 | /** 2076 | * @return Info about the page at the current scroll position. 2077 | * This can be synthetic for a missing middle page; the 'object' field can be null. 2078 | */ 2079 | private ItemInfo infoForCurrentScrollPosition() { 2080 | final int width = getClientWidth(); 2081 | final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; 2082 | final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; 2083 | int lastPos = -1; 2084 | float lastOffset = 0.f; 2085 | float lastWidth = 0.f; 2086 | boolean first = true; 2087 | 2088 | ItemInfo lastItem = null; 2089 | for (int i = 0; i < mItems.size(); i++) { 2090 | ItemInfo ii = mItems.get(i); 2091 | float offset; 2092 | if (!first && ii.position != lastPos + 1) { 2093 | // Create a synthetic item for a missing page. 2094 | ii = mTempItem; 2095 | ii.offset = lastOffset + lastWidth + marginOffset; 2096 | ii.position = lastPos + 1; 2097 | ii.widthFactor = mAdapter.getPageWidth(ii.position); 2098 | i--; 2099 | } 2100 | offset = ii.offset; 2101 | 2102 | final float leftBound = offset; 2103 | final float rightBound = offset + ii.widthFactor + marginOffset; 2104 | if (first || scrollOffset >= leftBound) { 2105 | if (scrollOffset < rightBound || i == mItems.size() - 1) { 2106 | return ii; 2107 | } 2108 | } else { 2109 | return lastItem; 2110 | } 2111 | first = false; 2112 | lastPos = ii.position; 2113 | lastOffset = offset; 2114 | lastWidth = ii.widthFactor; 2115 | lastItem = ii; 2116 | } 2117 | 2118 | return lastItem; 2119 | } 2120 | 2121 | private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { 2122 | int targetPage; 2123 | if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { 2124 | targetPage = velocity > 0 ? currentPage : currentPage + 1; 2125 | } else { 2126 | final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; 2127 | targetPage = (int) (currentPage + pageOffset + truncator); 2128 | } 2129 | 2130 | if (mItems.size() > 0) { 2131 | final ItemInfo firstItem = mItems.get(0); 2132 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2133 | 2134 | // Only let the user target pages we have items for 2135 | targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); 2136 | } 2137 | 2138 | return targetPage; 2139 | } 2140 | 2141 | @Override 2142 | public void draw(Canvas canvas) { 2143 | super.draw(canvas); 2144 | boolean needsInvalidate = false; 2145 | 2146 | final int overScrollMode = ViewCompat.getOverScrollMode(this); 2147 | if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 2148 | (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && 2149 | mAdapter != null && mAdapter.getCount() > 1)) { 2150 | if (!mLeftEdge.isFinished()) { 2151 | final int restoreCount = canvas.save(); 2152 | final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 2153 | final int width = getWidth(); 2154 | 2155 | canvas.rotate(270); 2156 | canvas.translate(-height + getPaddingTop(), mFirstOffset * width); 2157 | mLeftEdge.setSize(height, width); 2158 | needsInvalidate |= mLeftEdge.draw(canvas); 2159 | canvas.restoreToCount(restoreCount); 2160 | } 2161 | if (!mRightEdge.isFinished()) { 2162 | final int restoreCount = canvas.save(); 2163 | final int width = getWidth(); 2164 | final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 2165 | 2166 | canvas.rotate(90); 2167 | canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); 2168 | mRightEdge.setSize(height, width); 2169 | needsInvalidate |= mRightEdge.draw(canvas); 2170 | canvas.restoreToCount(restoreCount); 2171 | } 2172 | } else { 2173 | mLeftEdge.finish(); 2174 | mRightEdge.finish(); 2175 | } 2176 | 2177 | if (needsInvalidate) { 2178 | // Keep animating 2179 | ViewCompat.postInvalidateOnAnimation(this); 2180 | } 2181 | } 2182 | 2183 | @Override 2184 | protected void onDraw(Canvas canvas) { 2185 | super.onDraw(canvas); 2186 | 2187 | // Draw the margin drawable between pages if needed. 2188 | if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { 2189 | final int scrollX = getScrollX(); 2190 | final int width = getWidth(); 2191 | 2192 | final float marginOffset = (float) mPageMargin / width; 2193 | int itemIndex = 0; 2194 | ItemInfo ii = mItems.get(0); 2195 | float offset = ii.offset; 2196 | final int itemCount = mItems.size(); 2197 | final int firstPos = ii.position; 2198 | final int lastPos = mItems.get(itemCount - 1).position; 2199 | for (int pos = firstPos; pos < lastPos; pos++) { 2200 | while (pos > ii.position && itemIndex < itemCount) { 2201 | ii = mItems.get(++itemIndex); 2202 | } 2203 | 2204 | float drawAt; 2205 | if (pos == ii.position) { 2206 | drawAt = (ii.offset + ii.widthFactor) * width; 2207 | offset = ii.offset + ii.widthFactor + marginOffset; 2208 | } else { 2209 | float widthFactor = mAdapter.getPageWidth(pos); 2210 | drawAt = (offset + widthFactor) * width; 2211 | offset += widthFactor + marginOffset; 2212 | } 2213 | 2214 | if (drawAt + mPageMargin > scrollX) { 2215 | mMarginDrawable.setBounds((int) drawAt, mTopPageBounds, 2216 | (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); 2217 | mMarginDrawable.draw(canvas); 2218 | } 2219 | 2220 | if (drawAt > scrollX + width) { 2221 | break; // No more visible, no sense in continuing 2222 | } 2223 | } 2224 | } 2225 | } 2226 | 2227 | /** 2228 | * Start a fake drag of the pager. 2229 | * 2230 | *

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

During a fake drag the ViewPager will ignore all touch events. If a real drag 2237 | * is already in progress, this method will return false. 2238 | * 2239 | * @return true if the fake drag began successfully, false if it could not be started. 2240 | * 2241 | * @see #fakeDragBy(float) 2242 | * @see #endFakeDrag() 2243 | */ 2244 | public boolean beginFakeDrag() { 2245 | if (mIsBeingDragged) { 2246 | return false; 2247 | } 2248 | mFakeDragging = true; 2249 | setScrollState(SCROLL_STATE_DRAGGING); 2250 | mInitialMotionX = mLastMotionX = 0; 2251 | if (mVelocityTracker == null) { 2252 | mVelocityTracker = VelocityTracker.obtain(); 2253 | } else { 2254 | mVelocityTracker.clear(); 2255 | } 2256 | final long time = SystemClock.uptimeMillis(); 2257 | final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); 2258 | mVelocityTracker.addMovement(ev); 2259 | ev.recycle(); 2260 | mFakeDragBeginTime = time; 2261 | return true; 2262 | } 2263 | 2264 | /** 2265 | * End a fake drag of the pager. 2266 | * 2267 | * @see #beginFakeDrag() 2268 | * @see #fakeDragBy(float) 2269 | */ 2270 | public void endFakeDrag() { 2271 | if (!mFakeDragging) { 2272 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2273 | } 2274 | 2275 | final VelocityTracker velocityTracker = mVelocityTracker; 2276 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2277 | int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( 2278 | velocityTracker, mActivePointerId); 2279 | mPopulatePending = true; 2280 | final int width = getClientWidth(); 2281 | final int scrollX = getScrollX(); 2282 | final ItemInfo ii = infoForCurrentScrollPosition(); 2283 | final int currentPage = ii.position; 2284 | final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; 2285 | final int totalDelta = (int) (mLastMotionX - mInitialMotionX); 2286 | int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, 2287 | totalDelta); 2288 | setCurrentItemInternal(nextPage, true, true, initialVelocity); 2289 | endDrag(); 2290 | 2291 | mFakeDragging = false; 2292 | } 2293 | 2294 | /** 2295 | * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. 2296 | * 2297 | * @param xOffset Offset in pixels to drag by. 2298 | * @see #beginFakeDrag() 2299 | * @see #endFakeDrag() 2300 | */ 2301 | public void fakeDragBy(float xOffset) { 2302 | if (!mFakeDragging) { 2303 | throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); 2304 | } 2305 | 2306 | mLastMotionX += xOffset; 2307 | 2308 | float oldScrollX = getScrollX(); 2309 | float scrollX = oldScrollX - xOffset; 2310 | final int width = getClientWidth(); 2311 | 2312 | float leftBound = width * mFirstOffset; 2313 | float rightBound = width * mLastOffset; 2314 | 2315 | final ItemInfo firstItem = mItems.get(0); 2316 | final ItemInfo lastItem = mItems.get(mItems.size() - 1); 2317 | if (firstItem.position != 0) { 2318 | leftBound = firstItem.offset * width; 2319 | } 2320 | if (lastItem.position != mAdapter.getCount() - 1) { 2321 | rightBound = lastItem.offset * width; 2322 | } 2323 | 2324 | if (scrollX < leftBound) { 2325 | scrollX = leftBound; 2326 | } else if (scrollX > rightBound) { 2327 | scrollX = rightBound; 2328 | } 2329 | // Don't lose the rounded component 2330 | mLastMotionX += scrollX - (int) scrollX; 2331 | scrollTo((int) scrollX, getScrollY()); 2332 | pageScrolled((int) scrollX); 2333 | 2334 | // Synthesize an event for the VelocityTracker. 2335 | final long time = SystemClock.uptimeMillis(); 2336 | final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, 2337 | mLastMotionX, 0, 0); 2338 | mVelocityTracker.addMovement(ev); 2339 | ev.recycle(); 2340 | } 2341 | 2342 | /** 2343 | * Returns true if a fake drag is in progress. 2344 | * 2345 | * @return true if currently in a fake drag, false otherwise. 2346 | * 2347 | * @see #beginFakeDrag() 2348 | * @see #fakeDragBy(float) 2349 | * @see #endFakeDrag() 2350 | */ 2351 | public boolean isFakeDragging() { 2352 | return mFakeDragging; 2353 | } 2354 | 2355 | private void onSecondaryPointerUp(MotionEvent ev) { 2356 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 2357 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 2358 | if (pointerId == mActivePointerId) { 2359 | // This was our active pointer going up. Choose a new 2360 | // active pointer and adjust accordingly. 2361 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2362 | mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 2363 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 2364 | if (mVelocityTracker != null) { 2365 | mVelocityTracker.clear(); 2366 | } 2367 | } 2368 | } 2369 | 2370 | private void endDrag() { 2371 | mIsBeingDragged = false; 2372 | mIsUnableToDrag = false; 2373 | 2374 | if (mVelocityTracker != null) { 2375 | mVelocityTracker.recycle(); 2376 | mVelocityTracker = null; 2377 | } 2378 | } 2379 | 2380 | private void setScrollingCacheEnabled(boolean enabled) { 2381 | if (mScrollingCacheEnabled != enabled) { 2382 | mScrollingCacheEnabled = enabled; 2383 | if (USE_CACHE) { 2384 | final int size = getChildCount(); 2385 | for (int i = 0; i < size; ++i) { 2386 | final View child = getChildAt(i); 2387 | if (child.getVisibility() != GONE) { 2388 | child.setDrawingCacheEnabled(enabled); 2389 | } 2390 | } 2391 | } 2392 | } 2393 | } 2394 | 2395 | public boolean canScrollHorizontally(int direction) { 2396 | if (mAdapter == null) { 2397 | return false; 2398 | } 2399 | 2400 | final int width = getClientWidth(); 2401 | final int scrollX = getScrollX(); 2402 | if (direction < 0) { 2403 | return (scrollX > (int) (width * mFirstOffset)); 2404 | } else if (direction > 0) { 2405 | return (scrollX < (int) (width * mLastOffset)); 2406 | } else { 2407 | return false; 2408 | } 2409 | } 2410 | 2411 | /** 2412 | * Tests scrollability within child views of v given a delta of dx. 2413 | * 2414 | * @param v View to test for horizontal scrollability 2415 | * @param checkV Whether the view v passed should itself be checked for scrollability (true), 2416 | * or just its children (false). 2417 | * @param dx Delta scrolled in pixels 2418 | * @param x X coordinate of the active touch point 2419 | * @param y Y coordinate of the active touch point 2420 | * @return true if child views of v can be scrolled by delta of dx. 2421 | */ 2422 | protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 2423 | if (v instanceof ViewGroup) { 2424 | final ViewGroup group = (ViewGroup) v; 2425 | final int scrollX = v.getScrollX(); 2426 | final int scrollY = v.getScrollY(); 2427 | final int count = group.getChildCount(); 2428 | // Count backwards - let topmost views consume scroll distance first. 2429 | for (int i = count - 1; i >= 0; i--) { 2430 | // TODO: Add versioned support here for transformed views. 2431 | // This will not work for transformed views in Honeycomb+ 2432 | final View child = group.getChildAt(i); 2433 | if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 2434 | y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 2435 | canScroll(child, true, dx, x + scrollX - child.getLeft(), 2436 | y + scrollY - child.getTop())) { 2437 | return true; 2438 | } 2439 | } 2440 | } 2441 | 2442 | return checkV && ViewCompat.canScrollHorizontally(v, -dx); 2443 | } 2444 | 2445 | @Override 2446 | public boolean dispatchKeyEvent(KeyEvent event) { 2447 | // Let the focused view and/or our descendants get the key first 2448 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 2449 | } 2450 | 2451 | /** 2452 | * You can call this function yourself to have the scroll view perform 2453 | * scrolling from a key event, just as if the event had been dispatched to 2454 | * it by the view hierarchy. 2455 | * 2456 | * @param event The key event to execute. 2457 | * @return Return true if the event was handled, else false. 2458 | */ 2459 | public boolean executeKeyEvent(KeyEvent event) { 2460 | boolean handled = false; 2461 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 2462 | switch (event.getKeyCode()) { 2463 | case KeyEvent.KEYCODE_DPAD_LEFT: 2464 | handled = arrowScroll(FOCUS_LEFT); 2465 | break; 2466 | case KeyEvent.KEYCODE_DPAD_RIGHT: 2467 | handled = arrowScroll(FOCUS_RIGHT); 2468 | break; 2469 | case KeyEvent.KEYCODE_TAB: 2470 | if (Build.VERSION.SDK_INT >= 11) { 2471 | // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD 2472 | // before Android 3.0. Ignore the tab key on those devices. 2473 | if (KeyEventCompat.hasNoModifiers(event)) { 2474 | handled = arrowScroll(FOCUS_FORWARD); 2475 | } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { 2476 | handled = arrowScroll(FOCUS_BACKWARD); 2477 | } 2478 | } 2479 | break; 2480 | } 2481 | } 2482 | return handled; 2483 | } 2484 | 2485 | public boolean arrowScroll(int direction) { 2486 | View currentFocused = findFocus(); 2487 | if (currentFocused == this) { 2488 | currentFocused = null; 2489 | } else if (currentFocused != null) { 2490 | boolean isChild = false; 2491 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2492 | parent = parent.getParent()) { 2493 | if (parent == this) { 2494 | isChild = true; 2495 | break; 2496 | } 2497 | } 2498 | if (!isChild) { 2499 | // This would cause the focus search down below to fail in fun ways. 2500 | final StringBuilder sb = new StringBuilder(); 2501 | sb.append(currentFocused.getClass().getSimpleName()); 2502 | for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; 2503 | parent = parent.getParent()) { 2504 | sb.append(" => ").append(parent.getClass().getSimpleName()); 2505 | } 2506 | Log.e(TAG, "arrowScroll tried to find focus based on non-child " + 2507 | "current focused view " + sb.toString()); 2508 | currentFocused = null; 2509 | } 2510 | } 2511 | 2512 | boolean handled = false; 2513 | 2514 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, 2515 | direction); 2516 | if (nextFocused != null && nextFocused != currentFocused) { 2517 | if (direction == View.FOCUS_LEFT) { 2518 | // If there is nothing to the left, or this is causing us to 2519 | // jump to the right, then what we really want to do is page left. 2520 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; 2521 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; 2522 | if (currentFocused != null && nextLeft >= currLeft) { 2523 | handled = pageLeft(); 2524 | } else { 2525 | handled = nextFocused.requestFocus(); 2526 | } 2527 | } else if (direction == View.FOCUS_RIGHT) { 2528 | // If there is nothing to the right, or this is causing us to 2529 | // jump to the left, then what we really want to do is page right. 2530 | final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; 2531 | final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; 2532 | if (currentFocused != null && nextLeft <= currLeft) { 2533 | handled = pageRight(); 2534 | } else { 2535 | handled = nextFocused.requestFocus(); 2536 | } 2537 | } 2538 | } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { 2539 | // Trying to move left and nothing there; try to page. 2540 | handled = pageLeft(); 2541 | } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { 2542 | // Trying to move right and nothing there; try to page. 2543 | handled = pageRight(); 2544 | } 2545 | if (handled) { 2546 | playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2547 | } 2548 | return handled; 2549 | } 2550 | 2551 | private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { 2552 | if (outRect == null) { 2553 | outRect = new Rect(); 2554 | } 2555 | if (child == null) { 2556 | outRect.set(0, 0, 0, 0); 2557 | return outRect; 2558 | } 2559 | outRect.left = child.getLeft(); 2560 | outRect.right = child.getRight(); 2561 | outRect.top = child.getTop(); 2562 | outRect.bottom = child.getBottom(); 2563 | 2564 | ViewParent parent = child.getParent(); 2565 | while (parent instanceof ViewGroup && parent != this) { 2566 | final ViewGroup group = (ViewGroup) parent; 2567 | outRect.left += group.getLeft(); 2568 | outRect.right += group.getRight(); 2569 | outRect.top += group.getTop(); 2570 | outRect.bottom += group.getBottom(); 2571 | 2572 | parent = group.getParent(); 2573 | } 2574 | return outRect; 2575 | } 2576 | 2577 | boolean pageLeft() { 2578 | if (mCurItem > 0) { 2579 | setCurrentItem(mCurItem-1, true); 2580 | return true; 2581 | } 2582 | return false; 2583 | } 2584 | 2585 | boolean pageRight() { 2586 | if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { 2587 | setCurrentItem(mCurItem+1, true); 2588 | return true; 2589 | } 2590 | return false; 2591 | } 2592 | 2593 | /** 2594 | * We only want the current page that is being shown to be focusable. 2595 | */ 2596 | @Override 2597 | public void addFocusables(ArrayList views, int direction, int focusableMode) { 2598 | final int focusableCount = views.size(); 2599 | 2600 | final int descendantFocusability = getDescendantFocusability(); 2601 | 2602 | if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { 2603 | for (int i = 0; i < getChildCount(); i++) { 2604 | final View child = getChildAt(i); 2605 | if (child.getVisibility() == VISIBLE) { 2606 | ItemInfo ii = infoForChild(child); 2607 | if (ii != null && ii.position == mCurItem) { 2608 | child.addFocusables(views, direction, focusableMode); 2609 | } 2610 | } 2611 | } 2612 | } 2613 | 2614 | // we add ourselves (if focusable) in all cases except for when we are 2615 | // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is 2616 | // to avoid the focus search finding layouts when a more precise search 2617 | // among the focusable children would be more interesting. 2618 | if ( 2619 | descendantFocusability != FOCUS_AFTER_DESCENDANTS || 2620 | // No focusable descendants 2621 | (focusableCount == views.size())) { 2622 | // Note that we can't call the superclass here, because it will 2623 | // add all views in. So we need to do the same thing View does. 2624 | if (!isFocusable()) { 2625 | return; 2626 | } 2627 | if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && 2628 | isInTouchMode() && !isFocusableInTouchMode()) { 2629 | return; 2630 | } 2631 | if (views != null) { 2632 | views.add(this); 2633 | } 2634 | } 2635 | } 2636 | 2637 | /** 2638 | * We only want the current page that is being shown to be touchable. 2639 | */ 2640 | @Override 2641 | public void addTouchables(ArrayList views) { 2642 | // Note that we don't call super.addTouchables(), which means that 2643 | // we don't call View.addTouchables(). This is okay because a ViewPager 2644 | // is itself not touchable. 2645 | for (int i = 0; i < getChildCount(); i++) { 2646 | final View child = getChildAt(i); 2647 | if (child.getVisibility() == VISIBLE) { 2648 | ItemInfo ii = infoForChild(child); 2649 | if (ii != null && ii.position == mCurItem) { 2650 | child.addTouchables(views); 2651 | } 2652 | } 2653 | } 2654 | } 2655 | 2656 | /** 2657 | * We only want the current page that is being shown to be focusable. 2658 | */ 2659 | @Override 2660 | protected boolean onRequestFocusInDescendants(int direction, 2661 | Rect previouslyFocusedRect) { 2662 | int index; 2663 | int increment; 2664 | int end; 2665 | int count = getChildCount(); 2666 | if ((direction & FOCUS_FORWARD) != 0) { 2667 | index = 0; 2668 | increment = 1; 2669 | end = count; 2670 | } else { 2671 | index = count - 1; 2672 | increment = -1; 2673 | end = -1; 2674 | } 2675 | for (int i = index; i != end; i += increment) { 2676 | View child = getChildAt(i); 2677 | if (child.getVisibility() == VISIBLE) { 2678 | ItemInfo ii = infoForChild(child); 2679 | if (ii != null && ii.position == mCurItem) { 2680 | if (child.requestFocus(direction, previouslyFocusedRect)) { 2681 | return true; 2682 | } 2683 | } 2684 | } 2685 | } 2686 | return false; 2687 | } 2688 | 2689 | @Override 2690 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 2691 | // Dispatch scroll events from this ViewPager. 2692 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { 2693 | return super.dispatchPopulateAccessibilityEvent(event); 2694 | } 2695 | 2696 | // Dispatch all other accessibility events from the current page. 2697 | final int childCount = getChildCount(); 2698 | for (int i = 0; i < childCount; i++) { 2699 | final View child = getChildAt(i); 2700 | if (child.getVisibility() == VISIBLE) { 2701 | final ItemInfo ii = infoForChild(child); 2702 | if (ii != null && ii.position == mCurItem && 2703 | child.dispatchPopulateAccessibilityEvent(event)) { 2704 | return true; 2705 | } 2706 | } 2707 | } 2708 | 2709 | return false; 2710 | } 2711 | 2712 | @Override 2713 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 2714 | return new LayoutParams(); 2715 | } 2716 | 2717 | @Override 2718 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 2719 | return generateDefaultLayoutParams(); 2720 | } 2721 | 2722 | @Override 2723 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 2724 | return p instanceof LayoutParams && super.checkLayoutParams(p); 2725 | } 2726 | 2727 | @Override 2728 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2729 | return new LayoutParams(getContext(), attrs); 2730 | } 2731 | 2732 | class MyAccessibilityDelegate extends AccessibilityDelegateCompat { 2733 | 2734 | @Override 2735 | public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 2736 | super.onInitializeAccessibilityEvent(host, event); 2737 | event.setClassName(ViewPager.class.getName()); 2738 | final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain(); 2739 | recordCompat.setScrollable(canScroll()); 2740 | if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED 2741 | && mAdapter != null) { 2742 | recordCompat.setItemCount(mAdapter.getCount()); 2743 | recordCompat.setFromIndex(mCurItem); 2744 | recordCompat.setToIndex(mCurItem); 2745 | } 2746 | } 2747 | 2748 | @Override 2749 | public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 2750 | super.onInitializeAccessibilityNodeInfo(host, info); 2751 | info.setClassName(ViewPager.class.getName()); 2752 | info.setScrollable(canScroll()); 2753 | if (canScrollHorizontally(1)) { 2754 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2755 | } 2756 | if (canScrollHorizontally(-1)) { 2757 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2758 | } 2759 | } 2760 | 2761 | @Override 2762 | public boolean performAccessibilityAction(View host, int action, Bundle args) { 2763 | if (super.performAccessibilityAction(host, action, args)) { 2764 | return true; 2765 | } 2766 | switch (action) { 2767 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { 2768 | if (canScrollHorizontally(1)) { 2769 | setCurrentItem(mCurItem + 1); 2770 | return true; 2771 | } 2772 | } return false; 2773 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { 2774 | if (canScrollHorizontally(-1)) { 2775 | setCurrentItem(mCurItem - 1); 2776 | return true; 2777 | } 2778 | } return false; 2779 | } 2780 | return false; 2781 | } 2782 | 2783 | private boolean canScroll() { 2784 | return (mAdapter != null) && (mAdapter.getCount() > 1); 2785 | } 2786 | } 2787 | 2788 | private class PagerObserver extends DataSetObserver { 2789 | @Override 2790 | public void onChanged() { 2791 | dataSetChanged(); 2792 | } 2793 | @Override 2794 | public void onInvalidated() { 2795 | dataSetChanged(); 2796 | } 2797 | } 2798 | 2799 | /** 2800 | * Layout parameters that should be supplied for views added to a 2801 | * ViewPager. 2802 | */ 2803 | public static class LayoutParams extends ViewGroup.LayoutParams { 2804 | /** 2805 | * true if this view is a decoration on the pager itself and not 2806 | * a view supplied by the adapter. 2807 | */ 2808 | public boolean isDecor; 2809 | 2810 | /** 2811 | * Gravity setting for use on decor views only: 2812 | * Where to position the view page within the overall ViewPager 2813 | * container; constants are defined in {@link android.view.Gravity}. 2814 | */ 2815 | public int gravity; 2816 | 2817 | /** 2818 | * Width as a 0-1 multiplier of the measured pager width 2819 | */ 2820 | float widthFactor = 0.f; 2821 | 2822 | /** 2823 | * true if this view was added during layout and needs to be measured 2824 | * before being positioned. 2825 | */ 2826 | boolean needsMeasure; 2827 | 2828 | /** 2829 | * Adapter position this view is for if !isDecor 2830 | */ 2831 | int position; 2832 | 2833 | /** 2834 | * Current child index within the ViewPager that this view occupies 2835 | */ 2836 | int childIndex; 2837 | 2838 | public LayoutParams() { 2839 | super(FILL_PARENT, FILL_PARENT); 2840 | } 2841 | 2842 | public LayoutParams(Context context, AttributeSet attrs) { 2843 | super(context, attrs); 2844 | 2845 | final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 2846 | gravity = a.getInteger(0, Gravity.TOP); 2847 | a.recycle(); 2848 | } 2849 | } 2850 | 2851 | static class ViewPositionComparator implements Comparator { 2852 | @Override 2853 | public int compare(View lhs, View rhs) { 2854 | final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); 2855 | final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); 2856 | if (llp.isDecor != rlp.isDecor) { 2857 | return llp.isDecor ? 1 : -1; 2858 | } 2859 | return llp.position - rlp.position; 2860 | } 2861 | } 2862 | } 2863 | --------------------------------------------------------------------------------