aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml3
-rw-r--r--res/drawable-hdpi/img_oobe_files.pngbin0 -> 5505 bytes
-rw-r--r--res/drawable-hdpi/img_oobe_privacy.pngbin0 -> 11425 bytes
-rw-r--r--res/drawable-hdpi/img_oobe_root.pngbin0 -> 3965 bytes
-rw-r--r--res/drawable-mdpi/img_oobe_files.pngbin0 -> 3563 bytes
-rw-r--r--res/drawable-mdpi/img_oobe_privacy.pngbin0 -> 7395 bytes
-rw-r--r--res/drawable-mdpi/img_oobe_root.pngbin0 -> 2916 bytes
-rw-r--r--res/drawable-xhdpi/img_oobe_files.pngbin0 -> 7510 bytes
-rw-r--r--res/drawable-xhdpi/img_oobe_privacy.pngbin0 -> 15881 bytes
-rw-r--r--res/drawable-xhdpi/img_oobe_root.pngbin0 -> 5551 bytes
-rw-r--r--res/drawable-xxhdpi/img_oobe_files.pngbin0 -> 12323 bytes
-rw-r--r--res/drawable-xxhdpi/img_oobe_privacy.pngbin0 -> 24964 bytes
-rw-r--r--res/drawable-xxhdpi/img_oobe_root.pngbin0 -> 9117 bytes
-rw-r--r--res/drawable-xxxhdpi/img_oobe_files.pngbin0 -> 12323 bytes
-rw-r--r--res/drawable-xxxhdpi/img_oobe_privacy.pngbin0 -> 24964 bytes
-rw-r--r--res/drawable-xxxhdpi/img_oobe_root.pngbin0 -> 9117 bytes
-rw-r--r--res/layout/fragment_intro_content.xml42
-rw-r--r--res/layout/welcome.xml238
-rw-r--r--res/values/attrs.xml8
-rw-r--r--res/values/colors.xml3
-rw-r--r--res/values/dimen.xml3
-rw-r--r--res/values/strings.xml24
-rw-r--r--res/values/styles.xml8
-rwxr-xr-xsrc/com/cyanogenmod/filemanager/activities/WelcomeActivity.java165
-rw-r--r--src/com/cyanogenmod/filemanager/adapters/WelcomeAdapter.java40
-rw-r--r--src/com/cyanogenmod/filemanager/ui/fragments/WelcomeFragment.java70
-rw-r--r--src/com/cyanogenmod/filemanager/views/InkPageIndicator.java872
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
new file mode 100644
index 00000000..0fb1b511
--- /dev/null
+++ b/res/drawable-hdpi/img_oobe_files.png
Binary files differ
diff --git a/res/drawable-hdpi/img_oobe_privacy.png b/res/drawable-hdpi/img_oobe_privacy.png
new file mode 100644
index 00000000..715077cb
--- /dev/null
+++ b/res/drawable-hdpi/img_oobe_privacy.png
Binary files differ
diff --git a/res/drawable-hdpi/img_oobe_root.png b/res/drawable-hdpi/img_oobe_root.png
new file mode 100644
index 00000000..7a721f79
--- /dev/null
+++ b/res/drawable-hdpi/img_oobe_root.png
Binary files differ
diff --git a/res/drawable-mdpi/img_oobe_files.png b/res/drawable-mdpi/img_oobe_files.png
new file mode 100644
index 00000000..3be40a6f
--- /dev/null
+++ b/res/drawable-mdpi/img_oobe_files.png
Binary files differ
diff --git a/res/drawable-mdpi/img_oobe_privacy.png b/res/drawable-mdpi/img_oobe_privacy.png
new file mode 100644
index 00000000..c8a6275d
--- /dev/null
+++ b/res/drawable-mdpi/img_oobe_privacy.png
Binary files differ
diff --git a/res/drawable-mdpi/img_oobe_root.png b/res/drawable-mdpi/img_oobe_root.png
new file mode 100644
index 00000000..48940d74
--- /dev/null
+++ b/res/drawable-mdpi/img_oobe_root.png
Binary files differ
diff --git a/res/drawable-xhdpi/img_oobe_files.png b/res/drawable-xhdpi/img_oobe_files.png
new file mode 100644
index 00000000..0f89ef2d
--- /dev/null
+++ b/res/drawable-xhdpi/img_oobe_files.png
Binary files differ
diff --git a/res/drawable-xhdpi/img_oobe_privacy.png b/res/drawable-xhdpi/img_oobe_privacy.png
new file mode 100644
index 00000000..5dea4a0b
--- /dev/null
+++ b/res/drawable-xhdpi/img_oobe_privacy.png
Binary files differ
diff --git a/res/drawable-xhdpi/img_oobe_root.png b/res/drawable-xhdpi/img_oobe_root.png
new file mode 100644
index 00000000..7242de6f
--- /dev/null
+++ b/res/drawable-xhdpi/img_oobe_root.png
Binary files differ
diff --git a/res/drawable-xxhdpi/img_oobe_files.png b/res/drawable-xxhdpi/img_oobe_files.png
new file mode 100644
index 00000000..4f38a750
--- /dev/null
+++ b/res/drawable-xxhdpi/img_oobe_files.png
Binary files differ
diff --git a/res/drawable-xxhdpi/img_oobe_privacy.png b/res/drawable-xxhdpi/img_oobe_privacy.png
new file mode 100644
index 00000000..abe8612e
--- /dev/null
+++ b/res/drawable-xxhdpi/img_oobe_privacy.png
Binary files differ
diff --git a/res/drawable-xxhdpi/img_oobe_root.png b/res/drawable-xxhdpi/img_oobe_root.png
new file mode 100644
index 00000000..dd7a60dd
--- /dev/null
+++ b/res/drawable-xxhdpi/img_oobe_root.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/img_oobe_files.png b/res/drawable-xxxhdpi/img_oobe_files.png
new file mode 100644
index 00000000..4f38a750
--- /dev/null
+++ b/res/drawable-xxxhdpi/img_oobe_files.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/img_oobe_privacy.png b/res/drawable-xxxhdpi/img_oobe_privacy.png
new file mode 100644
index 00000000..abe8612e
--- /dev/null
+++ b/res/drawable-xxxhdpi/img_oobe_privacy.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/img_oobe_root.png b/res/drawable-xxxhdpi/img_oobe_root.png
new file mode 100644
index 00000000..dd7a60dd
--- /dev/null
+++ b/res/drawable-xxxhdpi/img_oobe_root.png
Binary files differ
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);
+ }
+ });
+ }
+ }
+}