diff options
27 files changed, 1096 insertions, 380 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e0b04339..f06b2107 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -201,7 +201,8 @@ android:name=".activities.WelcomeActivity" android:label="@string/app_name" android:launchMode="singleTop" - android:configChanges="keyboardHidden|screenSize"> + android:configChanges="keyboardHidden|screenSize" + android:theme="@style/FileManager.Theme.Welcome"> </activity> <activity diff --git a/res/drawable-hdpi/img_oobe_files.png b/res/drawable-hdpi/img_oobe_files.png Binary files differnew file mode 100644 index 00000000..0fb1b511 --- /dev/null +++ b/res/drawable-hdpi/img_oobe_files.png diff --git a/res/drawable-hdpi/img_oobe_privacy.png b/res/drawable-hdpi/img_oobe_privacy.png Binary files differnew file mode 100644 index 00000000..715077cb --- /dev/null +++ b/res/drawable-hdpi/img_oobe_privacy.png diff --git a/res/drawable-hdpi/img_oobe_root.png b/res/drawable-hdpi/img_oobe_root.png Binary files differnew file mode 100644 index 00000000..7a721f79 --- /dev/null +++ b/res/drawable-hdpi/img_oobe_root.png diff --git a/res/drawable-mdpi/img_oobe_files.png b/res/drawable-mdpi/img_oobe_files.png Binary files differnew file mode 100644 index 00000000..3be40a6f --- /dev/null +++ b/res/drawable-mdpi/img_oobe_files.png diff --git a/res/drawable-mdpi/img_oobe_privacy.png b/res/drawable-mdpi/img_oobe_privacy.png Binary files differnew file mode 100644 index 00000000..c8a6275d --- /dev/null +++ b/res/drawable-mdpi/img_oobe_privacy.png diff --git a/res/drawable-mdpi/img_oobe_root.png b/res/drawable-mdpi/img_oobe_root.png Binary files differnew file mode 100644 index 00000000..48940d74 --- /dev/null +++ b/res/drawable-mdpi/img_oobe_root.png diff --git a/res/drawable-xhdpi/img_oobe_files.png b/res/drawable-xhdpi/img_oobe_files.png Binary files differnew file mode 100644 index 00000000..0f89ef2d --- /dev/null +++ b/res/drawable-xhdpi/img_oobe_files.png diff --git a/res/drawable-xhdpi/img_oobe_privacy.png b/res/drawable-xhdpi/img_oobe_privacy.png Binary files differnew file mode 100644 index 00000000..5dea4a0b --- /dev/null +++ b/res/drawable-xhdpi/img_oobe_privacy.png diff --git a/res/drawable-xhdpi/img_oobe_root.png b/res/drawable-xhdpi/img_oobe_root.png Binary files differnew file mode 100644 index 00000000..7242de6f --- /dev/null +++ b/res/drawable-xhdpi/img_oobe_root.png diff --git a/res/drawable-xxhdpi/img_oobe_files.png b/res/drawable-xxhdpi/img_oobe_files.png Binary files differnew file mode 100644 index 00000000..4f38a750 --- /dev/null +++ b/res/drawable-xxhdpi/img_oobe_files.png diff --git a/res/drawable-xxhdpi/img_oobe_privacy.png b/res/drawable-xxhdpi/img_oobe_privacy.png Binary files differnew file mode 100644 index 00000000..abe8612e --- /dev/null +++ b/res/drawable-xxhdpi/img_oobe_privacy.png diff --git a/res/drawable-xxhdpi/img_oobe_root.png b/res/drawable-xxhdpi/img_oobe_root.png Binary files differnew file mode 100644 index 00000000..dd7a60dd --- /dev/null +++ b/res/drawable-xxhdpi/img_oobe_root.png diff --git a/res/drawable-xxxhdpi/img_oobe_files.png b/res/drawable-xxxhdpi/img_oobe_files.png Binary files differnew file mode 100644 index 00000000..4f38a750 --- /dev/null +++ b/res/drawable-xxxhdpi/img_oobe_files.png diff --git a/res/drawable-xxxhdpi/img_oobe_privacy.png b/res/drawable-xxxhdpi/img_oobe_privacy.png Binary files differnew file mode 100644 index 00000000..abe8612e --- /dev/null +++ b/res/drawable-xxxhdpi/img_oobe_privacy.png diff --git a/res/drawable-xxxhdpi/img_oobe_root.png b/res/drawable-xxxhdpi/img_oobe_root.png Binary files differnew file mode 100644 index 00000000..dd7a60dd --- /dev/null +++ b/res/drawable-xxxhdpi/img_oobe_root.png diff --git a/res/layout/fragment_intro_content.xml b/res/layout/fragment_intro_content.xml new file mode 100644 index 00000000..cae3d02f --- /dev/null +++ b/res/layout/fragment_intro_content.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="@dimen/intro_margin"> + + <ImageView + android:id="@+id/benefits_img" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:scaleType="fitXY" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:baselineAligned="true" + android:gravity="center" + android:orientation="vertical" + android:padding="@dimen/intro_margin"> + + <TextView + android:id="@+id/benefits_title" + style="@style/TextAppearance.AppCompat.Headline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/white" /> + + <TextView + android:id="@+id/benefits_message" + style="@style/TextAppearance.AppCompat.Body1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/intro_margin" + android:layout_marginTop="@dimen/intro_margin" + android:gravity="center" + android:paddingBottom="16dp" + android:textColor="@color/white" /> + </LinearLayout> +</FrameLayout> diff --git a/res/layout/welcome.xml b/res/layout/welcome.xml index 029025e4..05f7ea6a 100644 --- a/res/layout/welcome.xml +++ b/res/layout/welcome.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2012 The CyanogenMod Project +<!-- Copyright (C) 2016 The CyanogenMod Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,215 +14,45 @@ limitations under the License. --> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<android.support.design.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="@color/slide_color" + android:fitsSystemWindows="true"> <android.support.v4.view.ViewPager + android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - android:id="@+id/intro_pager" - android:background="@drawable/bg_material_statusbar"> + android:paddingBottom="?attr/actionBarSize" /> - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingTop="16dp" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:id="@+id/itemOne"> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/welcome_title" - android:textSize="28sp" - android:id="@+id/titleMessageOne" - android:textColor="@android:color/white"/> - - <View - android:id="@+id/generic_square" - android:layout_width="fill_parent" - android:layout_height="match_parent" - android:layout_above="@+id/bottomMessageOne" - android:layout_below="@+id/titleMessageOne" - android:layout_marginTop="16dp" - android:layout_marginBottom="16dp" - android:background="@drawable/rectangle"/> - - <TextView - android:id="@+id/bottomMessageOne" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/first_message" - android:layout_alignParentBottom="true" - android:paddingBottom="16dp" - android:layout_marginBottom="@dimen/default_row_height" - android:textSize="16sp" - android:textColor="@android:color/white"/> - - </RelativeLayout> - - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingTop="16dp" - android:paddingLeft="16dp" - android:id="@+id/itemTwo"> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/second_title" - android:textSize="28sp" - android:textColor="@android:color/white"/> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/second_message" - android:layout_alignParentBottom="true" - android:paddingBottom="16dp" - android:layout_marginBottom="@dimen/default_row_height" - android:textSize="16sp" - android:textColor="@android:color/white"/> - - </RelativeLayout> - - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingTop="16dp" - android:paddingLeft="16dp" - android:id="@+id/itemThree"> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/second_title" - android:textSize="28sp" - android:textColor="@android:color/white"/> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/second_message" - android:layout_alignParentBottom="true" - android:paddingBottom="16dp" - android:layout_marginBottom="@dimen/default_row_height" - android:textSize="16sp" - android:textColor="@android:color/white"/> - - </RelativeLayout> - - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:id="@+id/itemFour" - android:background="@color/cloud_fill"> - - <ImageView android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/flying_folder" - android:layout_above="@+id/clouds_intro" - android:id="@+id/flying_folder" - android:layout_marginBottom="78dp" - android:layout_centerHorizontal="true" - android:layout_gravity="center|center_vertical|center_horizontal" /> - - <!-- the cloud drawable has some weird spacing around it --> - <ImageView android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@drawable/cloud_line" - android:layout_marginLeft="-50dp" - android:layout_marginBottom="-13dp" - android:layout_centerVertical="true" - android:id="@+id/clouds_intro" - android:layout_gravity="center|center_vertical|center_horizontal"/> - - <View - android:id="@+id/generic_square" - android:layout_width="fill_parent" - android:layout_height="match_parent" - android:layout_below="@+id/clouds_intro" - android:background="@drawable/rectangle"/> - - <LinearLayout - android:id="@+id/cloud_text" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:layout_below="@+id/clouds_intro" - android:layout_marginStart="24dp"> - - <TextView - android:text="@string/add_cloud_storage" - android:id="@+id/cardHeaderText" - android:textColor="@android:color/white" - android:textSize="20sp" - android:textStyle="bold" - android:layout_width="wrap_content" - android:fontFamily="sans-serif-medium" - android:layout_height="wrap_content"/> - - <TextView - android:text="@string/oobe_add_cloud_storage_desc" - android:id="@+id/cardChildText" - android:textColor="@android:color/white" - android:textSize="16sp" - android:layout_marginTop="20dp" - android:layout_width="wrap_content" - android:layout_height="match_parent" /> - - </LinearLayout> - - <Button - android:text="@string/connect_now" - android:id="@+id/dismiss_card" - android:layout_width="120dp" - android:layout_height="36dp" - android:layout_alignParentBottom="true" - android:layout_centerHorizontal="true" - android:layout_marginBottom="124dp" - android:textColor="@android:color/white" - android:background="@color/dark_button_blue"/> - - </RelativeLayout> - - </android.support.v4.view.ViewPager> - - <RelativeLayout - android:id="@+id/footer" - android:layout_height="@dimen/default_row_height" - android:layout_width="fill_parent" - android:layout_alignParentBottom="true" - android:layout_alignParentStart="true"> - - <ImageView - android:id="@+id/prevButton" - style="?android:attr/buttonBarButtonStyle" + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom|center" + android:orientation="vertical" + android:padding="@dimen/intro_margin" + android:paddingLeft="@dimen/intro_margin"> + + <android.support.v7.widget.AppCompatButton + android:id="@+id/intro_btn_finish" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/ic_oobe_back" - android:layout_alignParentLeft="true" - android:textColor="@android:color/white"/> - - <com.cyanogenmod.filemanager.views.CirclePageIndicator - android:id="@+id/pagination" - android:layout_height="@dimen/half_row_height" - android:layout_width="fill_parent" - android:layout_alignParentBottom="true" - android:layout_alignParentStart="true" /> - - <ImageView - android:id="@+id/nextButton" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="wrap_content" + android:layout_gravity="center" + android:backgroundTint="@color/white" + android:elevation="8dp" + android:text="@string/slide_button" + android:textColor="@color/black" /> + + <com.cyanogenmod.filemanager.views.InkPageIndicator + android:id="@+id/indicator" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:src="@drawable/ic_oobe_forward" - android:layout_alignParentRight="true" - android:textColor="@android:color/white"/> - - </RelativeLayout> - -</RelativeLayout> + android:layout_gravity="center" + android:gravity="center" + android:paddingBottom="8dp" + android:paddingTop="8dp" /> + </LinearLayout> +</android.support.design.widget.CoordinatorLayout> diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 9b406036..0970c4db 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -87,4 +87,12 @@ <!-- View background --> <attr name="android:background"/> </declare-styleable> + + <declare-styleable name="InkPageIndicator"> + <attr name="dotDiameter" format="dimension" /> + <attr name="dotGap" format="dimension" /> + <attr name="animationDuration" format="integer" /> + <attr name="pageIndicatorColor" format="color" /> + <attr name="currentPageIndicatorColor" format="color" /> + </declare-styleable> </resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index 07a65024..a16e6c63 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -76,6 +76,7 @@ <color name="black_46">#75000000</color> <color name="black_26">#42000000</color> <color name="black_11">#1C000000</color> + <color name="black">#000000</color> <!-- Default navigation drawer colors --> <color name="navigation_drawer_title_default">#df000000</color> @@ -205,4 +206,6 @@ <color name="open_file_progress_dialog_message_color">#8b000000</color> <color name="picker_activity_window_bg_color">#99000000</color> + + <color name="slide_color">@color/default_primary</color> </resources> diff --git a/res/values/dimen.xml b/res/values/dimen.xml index eecd314a..7efc0388 100644 --- a/res/values/dimen.xml +++ b/res/values/dimen.xml @@ -240,5 +240,6 @@ <dimen name="open_dialog_text_margin_top">25dp</dimen> <!-- Open dialog text padding bottom --> <dimen name="open_dialog_text_padding_bottom">21dp</dimen> - + <!-- Intro margin --> + <dimen name="intro_margin">16dp</dimen> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index f134b656..20cf6a5d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -879,23 +879,13 @@ <!-- Welcome Dialog - Title --> <string name="welcome_title">Welcome to File Manager</string> - - <string name="skip_text">Skip</string> - <string name="next_text">Next</string> - - <!-- Welcome Dialog - Message --> - <string name="first_message">This app allows you to explore the file system and do operations - that could break your device. To prevent damage, the app will start in a safe,low-privileged - mode. - </string> - - <string name="second_title">Placeholder Intro Title </string> - - <string name="second_message"> - \nYou can access the advanced, full-privileged mode via Settings. It\'s your - responsibility to ensure that an operation doesn\'t break your system. - \nThe CyanogenMod Team - </string> + <string name="slide0_title">Your files</string> + <string name="slide0_message">Easily copy, move, share and move your files</string> + <string name="slide1_title">Secure your private stuff</string> + <string name="slide1_message">You can move your files into a secure folder which is protected with a password</string> + <string name="slide2_title">Advanced features</string> + <string name="slide2_message">You can access system files with root Mode.\nRemember: "with great power comes great responsibility"</string> + <string name="slide_button">Get started</string> <string name="activity_not_found_exception">Couldn\'t find an app to open this file</string> diff --git a/res/values/styles.xml b/res/values/styles.xml index 6649864e..0a85c7c7 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -140,6 +140,13 @@ <item name="android:backgroundDimAmount">0.6</item> </style> + <style name="FileManager.Theme.Welcome" parent="Theme.AppCompat.Light"> + <item name="android:colorPrimary">@color/default_primary</item> + <item name="android:colorPrimaryDark">@color/default_primary_dark</item> + <item name="windowActionBar">false</item> + <item name="windowNoTitle">true</item> + </style> + <!-- Action bar --> <style name="FileManager.Widget.ActionBar.White" parent="@android:style/Widget.Material.Light.ActionBar"> <item name="android:textColorPrimary">@android:color/white</item> @@ -155,6 +162,7 @@ <item name="android:colorControlNormal">@android:color/white</item> </style> + <!-- Title --> <style name="title_text_appearance"> <item name="android:textSize">@dimen/title_text_size</item> diff --git a/src/com/cyanogenmod/filemanager/activities/WelcomeActivity.java b/src/com/cyanogenmod/filemanager/activities/WelcomeActivity.java index 7a88babd..96faf89c 100755 --- a/src/com/cyanogenmod/filemanager/activities/WelcomeActivity.java +++ b/src/com/cyanogenmod/filemanager/activities/WelcomeActivity.java @@ -16,153 +16,60 @@ package com.cyanogenmod.filemanager.activities; -import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; import android.support.v4.view.ViewPager; -import android.support.v4.view.ViewPager.OnPageChangeListener; -import android.util.Log; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.AppCompatButton; import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ImageView; + import com.cyanogenmod.filemanager.R; import com.cyanogenmod.filemanager.adapters.WelcomeAdapter; import com.cyanogenmod.filemanager.views.CirclePageIndicator; import com.cyanogenmod.filemanager.views.PageIndicator; +import com.cyanogenmod.filemanager.views.InkPageIndicator; +public class WelcomeActivity extends AppCompatActivity { -/** - * An activity for search files and folders. - */ -public class WelcomeActivity extends Activity { - - private static final String TAG = "WelcomeActivity"; //$NON-NLS-1$ - - private static boolean DEBUG = false; - - ImageView mNextButton; - ViewPager vp; - WelcomeAdapter adapter; - ImageView mPrevButton; - - /** - * {@inheritDoc} - */ @Override - protected void onCreate(android.os.Bundle state) { - if (DEBUG) { - android.util.Log.d(TAG, "WelcomeActivity.onCreate"); //$NON-NLS-1$ - } - //Set the main layout of the activity + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); setContentView(R.layout.welcome); - - adapter = new WelcomeAdapter(); - vp = (ViewPager) findViewById(R.id.intro_pager); - mNextButton = (ImageView) findViewById(R.id.nextButton); - PageIndicator indicator = (CirclePageIndicator)findViewById(R.id.pagination); - mPrevButton = (ImageView) findViewById(R.id.prevButton); - - vp.setAdapter(adapter); - vp.setOffscreenPageLimit(3); - - indicator.setViewPager(vp); - - pagePrepare(vp.getCurrentItem()); - - indicator.setOnPageChangeListener(new OnPageChangeListener() { - @Override - public void onPageScrolled(int i, float v, int i1) { - } - - @Override - public void onPageSelected(int i) { - pagePrepare(i); - } - - @Override - public void onPageScrollStateChanged(int i) { - } - }); - - //Save state - super.onCreate(state); - } - - private void endButton(ImageView b) { - b.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - finish(); - } - }); - } - - private void pagePrepare(int currentPage) { - int maxCount = adapter.getCount(); - if (maxCount == currentPage + 1) { - mNextButton.setImageDrawable(getResources().getDrawable(R.drawable.ic_oobe_finish)); - endButton(mNextButton); - } else { - mNextButton.setImageDrawable(getResources().getDrawable(R.drawable.ic_oobe_forward)); - mNextButton.setOnClickListener(new OnClickListener() { + final WelcomeAdapter mSectionsPagerAdapter = + new WelcomeAdapter(getSupportFragmentManager()); + AppCompatButton mFinishBtn = (AppCompatButton) findViewById(R.id.intro_btn_finish); + InkPageIndicator inkPageIndicator = (InkPageIndicator) findViewById(R.id.indicator); + ViewPager mViewPager = (ViewPager) findViewById(R.id.container); + + if (mViewPager != null && inkPageIndicator != null) { + mViewPager.setAdapter(mSectionsPagerAdapter); + mViewPager.setCurrentItem(0); + mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override - public void onClick(View view) { - int current = vp.getCurrentItem(); - vp.setCurrentItem(current + 1); + public void onPageScrolled(int position, float positionOffset, + int positionOffsetPixels) { + } + @Override + public void onPageSelected(int position) { } - }); - } - if (currentPage == 0) { - mPrevButton.setVisibility(View.INVISIBLE); - } else { - mPrevButton.setVisibility(View.VISIBLE); - mPrevButton.setOnClickListener(new OnClickListener() { @Override - public void onClick(View view) { - int current = vp.getCurrentItem(); - vp.setCurrentItem(current - 1); + public void onPageScrollStateChanged(int state) { } }); + inkPageIndicator.setViewPager(mViewPager); + if (mFinishBtn != null) { + mFinishBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + } } } - /** - * {@inheritDoc} - */ @Override - protected void onDestroy() { - if (DEBUG) { - Log.d(TAG, "WelcomeActivity.onDestroy"); //$NON-NLS-1$ - } - - //All destroy. Continue - super.onDestroy(); - } - - /** - * {@inheritDoc} - */ - @Override - public void onConfigurationChanged(android.content.res.Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } - - - /** - * {@inheritDoc} - */ - @Override - protected void onPause() { - super.onPause(); - } - - /** - * {@inheritDoc} - */ - @Override - protected void onSaveInstanceState(android.os.Bundle outState) { - if (DEBUG) { - Log.d(TAG, "SearchActivity.onSaveInstanceState"); //$NON-NLS-1$ - } - super.onSaveInstanceState(outState); + public void onBackPressed() { + // Do nothing } } - diff --git a/src/com/cyanogenmod/filemanager/adapters/WelcomeAdapter.java b/src/com/cyanogenmod/filemanager/adapters/WelcomeAdapter.java index e0dce4aa..d7e0c239 100644 --- a/src/com/cyanogenmod/filemanager/adapters/WelcomeAdapter.java +++ b/src/com/cyanogenmod/filemanager/adapters/WelcomeAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The CyanogenMod Project + * Copyright (C) 2016 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,41 +16,25 @@ package com.cyanogenmod.filemanager.adapters; -import android.support.v4.view.PagerAdapter; -import android.view.View; -import com.cyanogenmod.filemanager.R; -import android.view.ViewGroup; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; -public class WelcomeAdapter extends PagerAdapter { +import com.cyanogenmod.filemanager.ui.fragments.WelcomeFragment; - private final static int COUNT_OF_INTRO_PAGES = 4; +public class WelcomeAdapter extends FragmentPagerAdapter { - public Object instantiateItem(ViewGroup collection, int position) { - - int resId = 0; - switch (position) { - case 0: - resId = R.id.itemOne; - break; - case 1: - resId = R.id.itemTwo; - break; - case 2: - resId = R.id.itemThree; - break; - case 3: - resId = R.id.itemFour; - } - return collection.findViewById(resId); + public WelcomeAdapter(FragmentManager mFragmentManager) { + super(mFragmentManager); } @Override - public int getCount() { - return COUNT_OF_INTRO_PAGES; + public Fragment getItem(int mPostion) { + return WelcomeFragment.newInstance(mPostion + 1); } @Override - public boolean isViewFromObject(View view, Object o) { - return view == ((View) o); + public int getCount() { + return 3; } } diff --git a/src/com/cyanogenmod/filemanager/ui/fragments/WelcomeFragment.java b/src/com/cyanogenmod/filemanager/ui/fragments/WelcomeFragment.java new file mode 100644 index 00000000..fc0cf58c --- /dev/null +++ b/src/com/cyanogenmod/filemanager/ui/fragments/WelcomeFragment.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.ui.fragments; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.cyanogenmod.filemanager.R; + +public class WelcomeFragment extends Fragment { + + public WelcomeFragment() { + } + + public static WelcomeFragment newInstance(int sectionNumber) { + WelcomeFragment fragment = new WelcomeFragment(); + Bundle args = new Bundle(); + args.putInt("section_number", sectionNumber); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater mInflater, ViewGroup mContainer, + Bundle mSavedInstance) { + int mPosition = getArguments().getInt("section_number"); + View mView = mInflater.inflate(R.layout.fragment_intro_content, mContainer, false); + TextView mTitle = (TextView) mView.findViewById(R.id.benefits_title); + TextView mDescription = (TextView) mView.findViewById(R.id.benefits_message); + ImageView mImage = (ImageView) mView.findViewById(R.id.benefits_img); + + switch (mPosition) { + case 1: + mTitle.setText(getString(R.string.slide0_title)); + mDescription.setText(getString(R.string.slide0_message)); + mImage.setImageResource(R.drawable.img_oobe_files); + break; + case 2: + mTitle.setText(getString(R.string.slide1_title)); + mDescription.setText(getString(R.string.slide1_message)); + mImage.setImageResource(R.drawable.img_oobe_privacy); + break; + case 3: + mTitle.setText(getString(R.string.slide2_title)); + mDescription.setText(getString(R.string.slide2_message)); + mImage.setImageResource(R.drawable.img_oobe_root); + break; + } + return mView; + } +} diff --git a/src/com/cyanogenmod/filemanager/views/InkPageIndicator.java b/src/com/cyanogenmod/filemanager/views/InkPageIndicator.java new file mode 100644 index 00000000..b9dc77c2 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/views/InkPageIndicator.java @@ -0,0 +1,872 @@ +package com.cyanogenmod.filemanager.views; + +/* + * Copyright 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPager; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.Interpolator; + +import java.util.Arrays; + +import com.cyanogenmod.filemanager.R; + +public class InkPageIndicator extends View implements ViewPager.OnPageChangeListener, + View.OnAttachStateChangeListener { + + // defaults + private static final int DEFAULT_DOT_SIZE = 8; // dp + private static final int DEFAULT_GAP = 12; // dp + private static final int DEFAULT_ANIM_DURATION = 400; // ms + private static final int DEFAULT_UNSELECTED_COLOUR = 0x80ffffff; // 50% white + private static final int DEFAULT_SELECTED_COLOUR = 0xffffffff; // 100% white + + // constants + private static final float INVALID_FRACTION = -1f; + private static final float MINIMAL_REVEAL = 0.00001f; + + // configurable attributes + private final int dotDiameter; + private final int gap; + private final long animDuration; + + // derived from attributes + private final float dotRadius; + private final float halfDotRadius; + private final long animHalfDuration; + // drawing + private final Paint unselectedPaint; + private final Paint selectedPaint; + private final Path combinedUnselectedPath; + private final Path unselectedDotPath; + private final Path unselectedDotLeftPath; + private final Path unselectedDotRightPath; + private final RectF rectF; + private final Interpolator interpolator; + private float dotTopY; + private float dotCenterY; + private float dotBottomY; + // ViewPager + private ViewPager viewPager; + // state + private int pageCount; + private int currentPage; + private int previousPage; + private float selectedDotX; + private boolean selectedDotInPosition; + private float[] dotCenterX; + private float[] joiningFractions; + private float retreatingJoinX1; + private float retreatingJoinX2; + private float[] dotRevealFractions; + private boolean isAttachedToWindow; + private boolean pageChanging; + // animation + private ValueAnimator moveAnimation; + private PendingRetreatAnimator retreatAnimation; + private PendingRevealAnimator[] revealAnimations; + + public InkPageIndicator(Context context) { + this(context, null, 0); + } + + public InkPageIndicator(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public InkPageIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + int density = (int) context.getResources().getDisplayMetrics().density; + + // Load attributes + TypedArray a = getContext().obtainStyledAttributes( + attrs, R.styleable.InkPageIndicator, defStyle, 0); + + dotDiameter = a.getDimensionPixelSize(R.styleable.InkPageIndicator_dotDiameter, + DEFAULT_DOT_SIZE * density); + dotRadius = dotDiameter / 2; + halfDotRadius = dotRadius / 2; + gap = a.getDimensionPixelSize(R.styleable.InkPageIndicator_dotGap, + DEFAULT_GAP * density); + animDuration = (long) a.getInteger(R.styleable.InkPageIndicator_animationDuration, + DEFAULT_ANIM_DURATION); + animHalfDuration = animDuration / 2; + int unselectedColour = a.getColor(R.styleable.InkPageIndicator_pageIndicatorColor, + DEFAULT_UNSELECTED_COLOUR); + int selectedColour = a.getColor(R.styleable.InkPageIndicator_currentPageIndicatorColor, + DEFAULT_SELECTED_COLOUR); + + a.recycle(); + + unselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + unselectedPaint.setColor(unselectedColour); + selectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + selectedPaint.setColor(selectedColour); + interpolator = new FastOutSlowInInterpolator(); + + // create paths & rect now – reuse & rewind later + combinedUnselectedPath = new Path(); + unselectedDotPath = new Path(); + unselectedDotLeftPath = new Path(); + unselectedDotRightPath = new Path(); + rectF = new RectF(); + + addOnAttachStateChangeListener(this); + } + + public void setViewPager(ViewPager viewPager) { + this.viewPager = viewPager; + viewPager.addOnPageChangeListener(this); + setPageCount(viewPager.getAdapter().getCount()); + viewPager.getAdapter().registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + setPageCount(InkPageIndicator.this.viewPager.getAdapter().getCount()); + } + }); + setCurrentPageImmediate(); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (isAttachedToWindow) { + float fraction = positionOffset; + int currentPosition = pageChanging ? previousPage : currentPage; + int leftDotPosition = position; + // when swiping from #2 to #1 ViewPager reports position as 1 and a descending offset + // need to convert this into our left-dot-based 'coordinate space' + if (currentPosition != position) { + fraction = 1f - positionOffset; + + // if user scrolls completely to next page then the position param updates to that + // new page but we're not ready to switch our 'current' page yet so adjust for that + if (fraction == 1f) { + leftDotPosition = Math.min(currentPosition, position); + } + } + setJoiningFraction(leftDotPosition, fraction); + } + } + + @Override + public void onPageSelected(int position) { + if (isAttachedToWindow) { + // this is the main event we're interested in! + setSelectedPage(position); + } else { + // when not attached, don't animate the move, just store immediately + setCurrentPageImmediate(); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + } + + private void setPageCount(int pages) { + pageCount = pages; + resetState(); + requestLayout(); + } + + private void calculateDotPositions(int width) { + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = width - getPaddingRight(); + + int requiredWidth = getRequiredWidth(); + float startLeft = left + (right - left - requiredWidth) / 2 + dotRadius; + + dotCenterX = new float[pageCount]; + for (int i = 0; i < pageCount; i++) { + dotCenterX[i] = startLeft + i * (dotDiameter + gap); + } + dotTopY = top; + dotCenterY = top + dotRadius; + dotBottomY = top + dotDiameter; + + setCurrentPageImmediate(); + } + + private void setCurrentPageImmediate() { + currentPage = viewPager != null ? viewPager.getCurrentItem() : 0; + if (dotCenterX != null && dotCenterX.length > 0 && + (moveAnimation == null || !moveAnimation.isStarted())) { + selectedDotX = dotCenterX[currentPage]; + } + } + + private void resetState() { + joiningFractions = new float[pageCount - 1]; + Arrays.fill(joiningFractions, 0f); + dotRevealFractions = new float[pageCount]; + Arrays.fill(dotRevealFractions, 0f); + retreatingJoinX1 = INVALID_FRACTION; + retreatingJoinX2 = INVALID_FRACTION; + selectedDotInPosition = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int desiredHeight = getDesiredHeight(); + int height; + switch (View.MeasureSpec.getMode(heightMeasureSpec)) { + case View.MeasureSpec.EXACTLY: + height = View.MeasureSpec.getSize(heightMeasureSpec); + break; + case View.MeasureSpec.AT_MOST: + height = Math.min(desiredHeight, View.MeasureSpec.getSize(heightMeasureSpec)); + break; + case View.MeasureSpec.UNSPECIFIED: + default: + height = desiredHeight; + break; + } + + int desiredWidth = getDesiredWidth(); + int width; + switch (View.MeasureSpec.getMode(widthMeasureSpec)) { + case View.MeasureSpec.EXACTLY: + width = View.MeasureSpec.getSize(widthMeasureSpec); + break; + case View.MeasureSpec.AT_MOST: + width = Math.min(desiredWidth, View.MeasureSpec.getSize(widthMeasureSpec)); + break; + case View.MeasureSpec.UNSPECIFIED: + default: + width = desiredWidth; + break; + } + setMeasuredDimension(width, height); + calculateDotPositions(width); + } + + private int getDesiredHeight() { + return getPaddingTop() + dotDiameter + getPaddingBottom(); + } + + private int getRequiredWidth() { + return pageCount * dotDiameter + (pageCount - 1) * gap; + } + + private int getDesiredWidth() { + return getPaddingLeft() + getRequiredWidth() + getPaddingRight(); + } + + @Override + public void onViewAttachedToWindow(View view) { + isAttachedToWindow = true; + } + + @Override + public void onViewDetachedFromWindow(View view) { + isAttachedToWindow = false; + } + + @Override + protected void onDraw(Canvas canvas) { + if (viewPager == null || pageCount == 0) return; + drawUnselected(canvas); + drawSelected(canvas); + } + + private void drawUnselected(Canvas canvas) { + + combinedUnselectedPath.rewind(); + + // draw any settled, revealing or joining dots + for (int page = 0; page < pageCount; page++) { + int nextXIndex = page == pageCount - 1 ? page : page + 1; + Path unselectedPath = getUnselectedPath(page, + dotCenterX[page], + dotCenterX[nextXIndex], + page == pageCount - 1 ? INVALID_FRACTION : joiningFractions[page], + dotRevealFractions[page]); + unselectedPath.addPath(combinedUnselectedPath); + combinedUnselectedPath.addPath(unselectedPath); + } + // draw any retreating joins + if (retreatingJoinX1 != INVALID_FRACTION) { + Path retreatingJoinPath = getRetreatingJoinPath(); + combinedUnselectedPath.addPath(retreatingJoinPath); + } + + canvas.drawPath(combinedUnselectedPath, unselectedPaint); + } + + /** + * Unselected dots can be in 6 states: + * <p> + * #1 At rest + * #2 Joining neighbour, still separate + * #3 Joining neighbour, combined curved + * #4 Joining neighbour, combined straight + * #5 Join retreating + * #6 Dot re-showing / revealing + * <p> + * It can also be in a combination of these states e.g. joining one neighbour while + * retreating from another. We therefore create a Path so that we can examine each + * dot pair separately and later take the union for these cases. + * <p> + * This function returns a path for the given dot **and any action to it's right** e.g. joining + * or retreating from it's neighbour + * + * @return path + */ + private Path getUnselectedPath(int page, float centerX, float nextCenterX, + float joiningFraction, float dotRevealFraction) { + + unselectedDotPath.rewind(); + + if ((joiningFraction == 0f || joiningFraction == INVALID_FRACTION) + && dotRevealFraction == 0f + && !(page == currentPage && selectedDotInPosition)) { + + // case #1 – At rest + unselectedDotPath.addCircle(dotCenterX[page], dotCenterY, dotRadius, Path.Direction.CW); + } + + float endX1; + float endY1; + float endX2; + float endY2; + float controlX1; + float controlY1; + float controlX2; + float controlY2; + if (joiningFraction > 0f && joiningFraction <= 0.5f + && retreatingJoinX1 == INVALID_FRACTION) { + + // case #2 – Joining neighbour, still separate + + // start with the left dot + unselectedDotLeftPath.rewind(); + + // start at the bottom center + unselectedDotLeftPath.moveTo(centerX, dotBottomY); + + // semi circle to the top center + rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY); + unselectedDotLeftPath.arcTo(rectF, 90, 180, true); + + // cubic to the right middle + endX1 = centerX + dotRadius + joiningFraction * gap; + endY1 = dotCenterY; + controlX1 = centerX + halfDotRadius; + controlY1 = dotTopY; + controlX2 = endX1; + controlY2 = endY1 - halfDotRadius; + unselectedDotLeftPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX1, endY1); + + // cubic back to the bottom center + endX2 = centerX; + endY2 = dotBottomY; + controlX1 = endX1; + controlY1 = endY1 + halfDotRadius; + controlX2 = centerX + halfDotRadius; + controlY2 = dotBottomY; + unselectedDotLeftPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX2, endY2); + + unselectedDotPath.addPath(unselectedDotLeftPath); + + // now do the next dot to the right + unselectedDotRightPath.rewind(); + + // start at the bottom center + unselectedDotRightPath.moveTo(nextCenterX, dotBottomY); + + // semi circle to the top center + rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY); + unselectedDotRightPath.arcTo(rectF, 90, -180, true); + + // cubic to the left middle + endX1 = nextCenterX - dotRadius - joiningFraction * gap; + endY1 = dotCenterY; + controlX1 = nextCenterX - halfDotRadius; + controlY1 = dotTopY; + controlX2 = endX1; + controlY2 = endY1 - halfDotRadius; + unselectedDotRightPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX1, endY1); + + // cubic back to the bottom center + endX2 = nextCenterX; + endY2 = dotBottomY; + controlX1 = endX1; + controlY1 = endY1 + halfDotRadius; + controlX2 = endX2 - halfDotRadius; + controlY2 = dotBottomY; + unselectedDotRightPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX2, endY2); + unselectedDotPath.addPath(unselectedDotRightPath); + } + + if (joiningFraction > 0.5f && joiningFraction < 1f + && retreatingJoinX1 == INVALID_FRACTION) { + + // case #3 – Joining neighbour, combined curved + + // adjust the fraction so that it goes from 0.3 -> 1 to produce a more realistic 'join' + float adjustedFraction = (joiningFraction - 0.2f) * 1.25f; + + // start in the bottom left + unselectedDotPath.moveTo(centerX, dotBottomY); + + // semi-circle to the top left + rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY); + unselectedDotPath.arcTo(rectF, 90, 180, true); + + // bezier to the middle top of the join + endX1 = centerX + dotRadius + gap / 2; + endY1 = dotCenterY - adjustedFraction * dotRadius; + controlX1 = endX1 - adjustedFraction * dotRadius; + controlY1 = dotTopY; + controlX2 = endX1 - (1 - adjustedFraction) * dotRadius; + controlY2 = endY1; + unselectedDotPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX1, endY1); + + // bezier to the top right of the join + endX2 = nextCenterX; + endY2 = dotTopY; + controlX1 = endX1 + (1 - adjustedFraction) * dotRadius; + controlY1 = endY1; + controlX2 = endX1 + adjustedFraction * dotRadius; + controlY2 = dotTopY; + unselectedDotPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX2, endY2); + + // semi-circle to the bottom right + rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY); + unselectedDotPath.arcTo(rectF, 270, 180, true); + + // bezier to the middle bottom of the join + // endX1 stays the same + endY1 = dotCenterY + adjustedFraction * dotRadius; + controlX1 = endX1 + adjustedFraction * dotRadius; + controlY1 = dotBottomY; + controlX2 = endX1 + (1 - adjustedFraction) * dotRadius; + controlY2 = endY1; + unselectedDotPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX1, endY1); + + // bezier back to the start point in the bottom left + endX2 = centerX; + endY2 = dotBottomY; + controlX1 = endX1 - (1 - adjustedFraction) * dotRadius; + controlY1 = endY1; + controlX2 = endX1 - adjustedFraction * dotRadius; + controlY2 = endY2; + unselectedDotPath.cubicTo(controlX1, controlY1, + controlX2, controlY2, + endX2, endY2); + } + if (joiningFraction == 1 && retreatingJoinX1 == INVALID_FRACTION) { + + // case #4 Joining neighbour, combined straight technically we could use case 3 for this + // situation as well but assume that this is an optimization rather than faffing around + // with beziers just to draw a rounded rect + rectF.set(centerX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY); + unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW); + } + + // case #5 is handled by #getRetreatingJoinPath() + // this is done separately so that we can have a single retreating path spanning + // multiple dots and therefore animate it's movement smoothly + + if (dotRevealFraction > MINIMAL_REVEAL) { + + // case #6 – previously hidden dot revealing + unselectedDotPath.addCircle(centerX, dotCenterY, dotRevealFraction * dotRadius, + Path.Direction.CW); + } + + return unselectedDotPath; + } + + private Path getRetreatingJoinPath() { + unselectedDotPath.rewind(); + rectF.set(retreatingJoinX1, dotTopY, retreatingJoinX2, dotBottomY); + unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW); + return unselectedDotPath; + } + + private void drawSelected(Canvas canvas) { + canvas.drawCircle(selectedDotX, dotCenterY, dotRadius, selectedPaint); + } + + private void setSelectedPage(int now) { + if (now == currentPage) return; + + pageChanging = true; + previousPage = currentPage; + currentPage = now; + int steps = Math.abs(now - previousPage); + + if (steps > 1) { + if (now > previousPage) { + for (int i = 0; i < steps; i++) { + setJoiningFraction(previousPage + i, 1f); + } + } else { + for (int i = -1; i > -steps; i--) { + setJoiningFraction(previousPage + i, 1f); + } + } + } + + // create the anim to move the selected dot – this animator will kick off + // retreat animations when it has moved 75% of the way. + // The retreat animation in turn will kick of reveal anims when the + // retreat has passed any dots to be revealed + moveAnimation = createMoveSelectedAnimator(dotCenterX[now], previousPage, now, steps); + moveAnimation.start(); + } + + private ValueAnimator createMoveSelectedAnimator( + float moveTo, int was, int now, int steps) { + + // create the actual move animator + ValueAnimator moveSelected = ValueAnimator.ofFloat(selectedDotX, moveTo); + + // also set up a pending retreat anim – this starts when the move is 75% complete + retreatAnimation = new InkPageIndicator.PendingRetreatAnimator(was, now, steps, + now > was ? + new InkPageIndicator.RightwardStartPredicate( + moveTo - (moveTo - selectedDotX) * 0.25f) : + new InkPageIndicator.LeftwardStartPredicate( + moveTo + (selectedDotX - moveTo) * 0.25f)); + retreatAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + resetState(); + pageChanging = false; + } + }); + moveSelected.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + selectedDotX = (Float) valueAnimator.getAnimatedValue(); + retreatAnimation.startIfNecessary(selectedDotX); + ViewCompat.postInvalidateOnAnimation(InkPageIndicator.this); + } + }); + moveSelected.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // set a flag so that we continue to draw the unselected dot in the target position + // until the selected dot has finished moving into place + selectedDotInPosition = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + // set a flag when anim finishes so that we don't draw both selected & unselected + // page dots + selectedDotInPosition = true; + } + }); + // slightly delay the start to give the joins a chance to run + // unless dot isn't in position yet – then don't delay! + moveSelected.setStartDelay(selectedDotInPosition ? animDuration / 4L : 0L); + moveSelected.setDuration(animDuration * 3L / 4L); + moveSelected.setInterpolator(interpolator); + return moveSelected; + } + + private void setJoiningFraction(int leftDot, float fraction) { + if (leftDot < joiningFractions.length) { + + joiningFractions[leftDot] = fraction; + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void clearJoiningFractions() { + Arrays.fill(joiningFractions, 0f); + ViewCompat.postInvalidateOnAnimation(this); + } + + private void setDotRevealFraction(int dot, float fraction) { + if (dot < dotRevealFractions.length) { + dotRevealFractions[dot] = fraction; + } + ViewCompat.postInvalidateOnAnimation(this); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + InkPageIndicator.SavedState savedState = (InkPageIndicator.SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + currentPage = savedState.currentPage; + requestLayout(); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + InkPageIndicator.SavedState savedState = new InkPageIndicator.SavedState(superState); + savedState.currentPage = currentPage; + return savedState; + } + + /** + * A predicate used to start an animation when a test passes + */ + abstract static class StartPredicate { + + final float thresholdValue; + + StartPredicate(float thresholdValue) { + this.thresholdValue = thresholdValue; + } + + abstract boolean shouldStart(float currentValue); + + } + + /** + * A predicate used to start an animation when a given value is greater than a threshold + */ + private static class RightwardStartPredicate extends InkPageIndicator.StartPredicate { + + RightwardStartPredicate(float thresholdValue) { + super(thresholdValue); + } + + boolean shouldStart(float currentValue) { + return currentValue > thresholdValue; + } + } + + /** + * A predicate used to start an animation then a given value is less than a threshold + */ + private static class LeftwardStartPredicate extends InkPageIndicator.StartPredicate { + + LeftwardStartPredicate(float thresholdValue) { + super(thresholdValue); + } + + boolean shouldStart(float currentValue) { + return currentValue < thresholdValue; + } + } + + private static class SavedState extends View.BaseSavedState { + public static final Parcelable.Creator<InkPageIndicator.SavedState> CREATOR = + new Parcelable.Creator<InkPageIndicator.SavedState>() { + @Override + public InkPageIndicator.SavedState createFromParcel(Parcel in) { + return new InkPageIndicator.SavedState(in); + } + + @Override + public InkPageIndicator.SavedState[] newArray(int size) { + return new InkPageIndicator.SavedState[size]; + } + }; + int currentPage; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(currentPage); + } + } + + /** + * A {@link ValueAnimator} that starts once a given predicate returns true. + */ + @SuppressWarnings({"CloneableClassInSecureContext", "NonStaticInnerClassInSecureContext"}) + abstract class PendingStartAnimator extends ValueAnimator implements Cloneable { + + final InkPageIndicator.StartPredicate predicate; + boolean hasStarted; + + PendingStartAnimator(InkPageIndicator.StartPredicate predicate) { + this.predicate = predicate; + hasStarted = false; + } + + void startIfNecessary(float currentValue) { + if (!hasStarted && predicate.shouldStart(currentValue)) { + start(); + hasStarted = true; + } + } + } + + /** + * An Animator that shows and then shrinks a retreating join between the previous and newly + * selected pages. This also sets up some pending dot reveals – to be started when the retreat + * has passed the dot to be revealed. + */ + @SuppressWarnings({"CloneableClassInSecureContext", "NonStaticInnerClassInSecureContext"}) + private class PendingRetreatAnimator extends PendingStartAnimator { + + PendingRetreatAnimator(int was, int now, int steps, StartPredicate predicate) { + super(predicate); + setDuration(animHalfDuration); + setInterpolator(interpolator); + + // work out the start/end values of the retreating join from the direction we're + // travelling in. Also look at the current selected dot position, i.e. we're moving on + // before a prior anim has finished. + final float initialX1 = now > was ? Math.min(dotCenterX[was], selectedDotX) - dotRadius + : dotCenterX[now] - dotRadius; + float finalX1 = now > was ? dotCenterX[now] - dotRadius + : dotCenterX[now] - dotRadius; + final float initialX2 = now > was ? dotCenterX[now] + dotRadius + : Math.max(dotCenterX[was], selectedDotX) + dotRadius; + float finalX2 = now > was ? dotCenterX[now] + dotRadius + : dotCenterX[now] + dotRadius; + + revealAnimations = new InkPageIndicator.PendingRevealAnimator[steps]; + // hold on to the indexes of the dots that will be hidden by the retreat so that + // we can initialize their revealFraction's i.e. make sure they're hidden while the + // reveal animation runs + final int[] dotsToHide = new int[steps]; + if (initialX1 == finalX1) { // (initialX2 != finalX2) leftward retreat + setFloatValues(initialX2, finalX2); + // create the reveal animations that will run when the retreat passes them + for (int i = 0; i < steps; i++) { + revealAnimations[i] = new PendingRevealAnimator(was - i, + new LeftwardStartPredicate(dotCenterX[was - i])); + dotsToHide[i] = was - i; + } + addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + retreatingJoinX2 = (Float) valueAnimator.getAnimatedValue(); + ViewCompat.postInvalidateOnAnimation(InkPageIndicator.this); + // start any reveal animations if we've passed them + for (PendingRevealAnimator pendingReveal : revealAnimations) { + pendingReveal.startIfNecessary(retreatingJoinX2); + } + } + }); + } else { // rightward retreat + setFloatValues(initialX1, finalX1); + // create the reveal animations that will run when the retreat passes them + for (int i = 0; i < steps; i++) { + revealAnimations[i] = new PendingRevealAnimator(was + i, + new RightwardStartPredicate(dotCenterX[was + i])); + dotsToHide[i] = was + i; + } + addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + retreatingJoinX1 = (Float) valueAnimator.getAnimatedValue(); + ViewCompat.postInvalidateOnAnimation(InkPageIndicator.this); + // start any reveal animations if we've passed them + for (PendingRevealAnimator pendingReveal : revealAnimations) { + pendingReveal.startIfNecessary(retreatingJoinX1); + } + } + }); + } + + addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + clearJoiningFractions(); + // we need to set this so that the dots are hidden until the reveal anim runs + for (int dot : dotsToHide) { + setDotRevealFraction(dot, MINIMAL_REVEAL); + } + retreatingJoinX1 = initialX1; + retreatingJoinX2 = initialX2; + ViewCompat.postInvalidateOnAnimation(InkPageIndicator.this); + } + + @Override + public void onAnimationEnd(Animator animation) { + retreatingJoinX1 = INVALID_FRACTION; + retreatingJoinX2 = INVALID_FRACTION; + ViewCompat.postInvalidateOnAnimation(InkPageIndicator.this); + } + }); + } + } + + /** + * An Animator that animates a given dot's revealFraction i.e. scales it up + */ + @SuppressWarnings({"CloneableClassInSecureContext", "NonStaticInnerClassInSecureContext"}) + private class PendingRevealAnimator extends PendingStartAnimator { + + private final int dot; + + PendingRevealAnimator(int dot, StartPredicate predicate) { + super(predicate); + setFloatValues(MINIMAL_REVEAL, 1f); + this.dot = dot; + setDuration(animHalfDuration); + setInterpolator(interpolator); + addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + setDotRevealFraction(PendingRevealAnimator.this.dot, + (Float) valueAnimator.getAnimatedValue()); + } + }); + addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setDotRevealFraction(PendingRevealAnimator.this.dot, 0f); + ViewCompat.postInvalidateOnAnimation(InkPageIndicator.this); + } + }); + } + } +} |