summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main/src/main/AndroidManifest.xml2
-rw-r--r--main/src/main/java/android/support/v4n/app/FragmentStatePagerAdapter.java228
-rw-r--r--main/src/main/java/android/support/v4n/view/PagerAdapter.java320
-rw-r--r--main/src/main/java/android/support/v4n/view/ViewPager.java2902
-rw-r--r--main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java143
-rw-r--r--main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java1
-rw-r--r--main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java1
-rw-r--r--main/src/main/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.java7
-rw-r--r--main/src/main/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java732
-rw-r--r--main/src/main/java/de/blinkt/openvpn/views/SlidingTabLayout.java314
-rw-r--r--main/src/main/java/de/blinkt/openvpn/views/SlidingTabStrip.java208
-rw-r--r--main/src/main/java/de/blinkt/openvpn/views/TabBarView.java16
-rw-r--r--main/src/main/res/drawable/bg_tabs.xml9
-rw-r--r--main/src/main/res/drawable/slidingtab_background.xml8
-rw-r--r--main/src/main/res/layout-v21/tabs.xml13
-rw-r--r--main/src/main/res/layout/allowed_vpn_apps.xml2
-rw-r--r--main/src/main/res/layout/main_activity.xml19
-rw-r--r--main/src/main/res/layout/padersliding_tab.xml8
-rw-r--r--main/src/main/res/layout/tabs.xml12
-rw-r--r--main/src/main/res/values-v21/styles.xml5
-rw-r--r--main/src/main/res/values/attrs.xml41
-rw-r--r--main/src/main/res/values/colours.xml3
-rw-r--r--main/src/main/res/values/styles.xml7
-rw-r--r--remoteExample/build.gradle4
-rw-r--r--vpndialogxposed/build.gradle4
25 files changed, 4935 insertions, 74 deletions
diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml
index ff99f22b..4e6ca126 100644
--- a/main/src/main/AndroidManifest.xml
+++ b/main/src/main/AndroidManifest.xml
@@ -28,7 +28,7 @@
<application
android:allowBackup="true"
- android:theme="@style/appstyle"
+ android:theme="@style/blinkt"
android:icon="@drawable/icon"
android:label="@string/app"
android:name=".core.ICSOpenVPNApplication"
diff --git a/main/src/main/java/android/support/v4n/app/FragmentStatePagerAdapter.java b/main/src/main/java/android/support/v4n/app/FragmentStatePagerAdapter.java
new file mode 100644
index 00000000..07810935
--- /dev/null
+++ b/main/src/main/java/android/support/v4n/app/FragmentStatePagerAdapter.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2011 The Android Open Source 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 android.support.v4n.app;
+
+import java.util.ArrayList;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4n.view.PagerAdapter;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Implementation of {@link android.support.v4.view.PagerAdapter} that
+ * uses a {@link Fragment} to manage each page. This class also handles
+ * saving and restoring of fragment's state.
+ *
+ * <p>This version of the pager is more useful when there are a large number
+ * of pages, working more like a list view. When pages are not visible to
+ * the user, their entire fragment may be destroyed, only keeping the saved
+ * state of that fragment. This allows the pager to hold on to much less
+ * memory associated with each visited page as compared to
+ * {@link FragmentPagerAdapter} at the cost of potentially more overhead when
+ * switching between pages.
+ *
+ * <p>When using FragmentPagerAdapter the host ViewPager must have a
+ * valid ID set.</p>
+ *
+ * <p>Subclasses only need to implement {@link #getItem(int)}
+ * and {@link #getCount()} to have a working adapter.
+ *
+ * <p>Here is an example implementation of a pager containing fragments of
+ * lists:
+ *
+ * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java
+ * complete}
+ *
+ * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is:
+ *
+ * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml
+ * complete}
+ *
+ * <p>The <code>R.layout.fragment_pager_list</code> resource containing each
+ * individual fragment's layout is:
+ *
+ * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml
+ * complete}
+ */
+public abstract class FragmentStatePagerAdapter extends PagerAdapter {
+ private static final String TAG = "FragmentStatePagerAdapter";
+ private static final boolean DEBUG = false;
+
+ private final FragmentManager mFragmentManager;
+ private FragmentTransaction mCurTransaction = null;
+
+ private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
+ private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
+ private Fragment mCurrentPrimaryItem = null;
+
+ public FragmentStatePagerAdapter(FragmentManager fm) {
+ mFragmentManager = fm;
+ }
+
+ /**
+ * Return the Fragment associated with a specified position.
+ */
+ public abstract Fragment getItem(int position);
+
+ @Override
+ public void startUpdate(ViewGroup container) {
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ // If we already have this item instantiated, there is nothing
+ // to do. This can happen when we are restoring the entire pager
+ // from its saved state, where the fragment manager has already
+ // taken care of restoring the fragments we previously had instantiated.
+ if (mFragments.size() > position) {
+ Fragment f = mFragments.get(position);
+ if (f != null) {
+ return f;
+ }
+ }
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+
+ Fragment fragment = getItem(position);
+ if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
+ if (mSavedState.size() > position) {
+ Fragment.SavedState fss = mSavedState.get(position);
+ if (fss != null) {
+ fragment.setInitialSavedState(fss);
+ }
+ }
+ while (mFragments.size() <= position) {
+ mFragments.add(null);
+ }
+ fragment.setMenuVisibility(false);
+ fragment.setUserVisibleHint(false);
+ mFragments.set(position, fragment);
+ mCurTransaction.add(container.getId(), fragment);
+
+ return fragment;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ Fragment fragment = (Fragment)object;
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+ if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ + " v=" + ((Fragment)object).getView());
+ while (mSavedState.size() <= position) {
+ mSavedState.add(null);
+ }
+ mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
+ mFragments.set(position, null);
+
+ mCurTransaction.remove(fragment);
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ Fragment fragment = (Fragment)object;
+ if (fragment != mCurrentPrimaryItem) {
+ if (mCurrentPrimaryItem != null) {
+ mCurrentPrimaryItem.setMenuVisibility(false);
+ mCurrentPrimaryItem.setUserVisibleHint(false);
+ }
+ if (fragment != null) {
+ fragment.setMenuVisibility(true);
+ fragment.setUserVisibleHint(true);
+ }
+ mCurrentPrimaryItem = fragment;
+ }
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ if (mCurTransaction != null) {
+ mCurTransaction.commitAllowingStateLoss();
+ mCurTransaction = null;
+ mFragmentManager.executePendingTransactions();
+ }
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return ((Fragment)object).getView() == view;
+ }
+
+ @Override
+ public Parcelable saveState() {
+ Bundle state = null;
+ if (mSavedState.size() > 0) {
+ state = new Bundle();
+ Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
+ mSavedState.toArray(fss);
+ state.putParcelableArray("states", fss);
+ }
+ for (int i=0; i<mFragments.size(); i++) {
+ Fragment f = mFragments.get(i);
+ if (f != null) {
+ if (state == null) {
+ state = new Bundle();
+ }
+ String key = "f" + i;
+ mFragmentManager.putFragment(state, key, f);
+ }
+ }
+ return state;
+ }
+
+ @Override
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ if (state != null) {
+ Bundle bundle = (Bundle)state;
+ bundle.setClassLoader(loader);
+ Parcelable[] fss = bundle.getParcelableArray("states");
+ mSavedState.clear();
+ mFragments.clear();
+ if (fss != null) {
+ for (int i=0; i<fss.length; i++) {
+ mSavedState.add((Fragment.SavedState)fss[i]);
+ }
+ }
+ Iterable<String> keys = bundle.keySet();
+ for (String key: keys) {
+ if (key.startsWith("f")) {
+ int index = Integer.parseInt(key.substring(1));
+ Fragment f = mFragmentManager.getFragment(bundle, key);
+ if (f != null) {
+ while (mFragments.size() <= index) {
+ mFragments.add(null);
+ }
+ f.setMenuVisibility(false);
+ mFragments.set(index, f);
+ } else {
+ Log.w(TAG, "Bad fragment at key " + key);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/main/src/main/java/android/support/v4n/view/PagerAdapter.java b/main/src/main/java/android/support/v4n/view/PagerAdapter.java
new file mode 100644
index 00000000..70ed75f3
--- /dev/null
+++ b/main/src/main/java/android/support/v4n/view/PagerAdapter.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2011 The Android Open Source 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 android.support.v4n.view;
+
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Base class providing the adapter to populate pages inside of
+ * a {@link ViewPager}. You will most likely want to use a more
+ * specific implementation of this, such as
+ * {@link android.support.v4n.app.FragmentPagerAdapter} or
+ * {@link android.support.v4.app.FragmentStatePagerAdapter}.
+ *
+ * <p>When you implement a PagerAdapter, you must override the following methods
+ * at minimum:</p>
+ * <ul>
+ * <li>{@link #instantiateItem(ViewGroup, int)}</li>
+ * <li>{@link #destroyItem(ViewGroup, int, Object)}</li>
+ * <li>{@link #getCount()}</li>
+ * <li>{@link #isViewFromObject(View, Object)}</li>
+ * </ul>
+ *
+ * <p>PagerAdapter is more general than the adapters used for
+ * {@link android.widget.AdapterView AdapterViews}. Instead of providing a
+ * View recycling mechanism directly ViewPager uses callbacks to indicate the
+ * steps taken during an update. A PagerAdapter may implement a form of View
+ * recycling if desired or use a more sophisticated method of managing page
+ * Views such as Fragment transactions where each page is represented by its
+ * own Fragment.</p>
+ *
+ * <p>ViewPager associates each page with a key Object instead of working with
+ * Views directly. This key is used to track and uniquely identify a given page
+ * independent of its position in the adapter. A call to the PagerAdapter method
+ * {@link #startUpdate(ViewGroup)} indicates that the contents of the ViewPager
+ * are about to change. One or more calls to {@link #instantiateItem(ViewGroup, int)}
+ * and/or {@link #destroyItem(ViewGroup, int, Object)} will follow, and the end
+ * of an update will be signaled by a call to {@link #finishUpdate(ViewGroup)}.
+ * By the time {@link #finishUpdate(ViewGroup) finishUpdate} returns the views
+ * associated with the key objects returned by
+ * {@link #instantiateItem(ViewGroup, int) instantiateItem} should be added to
+ * the parent ViewGroup passed to these methods and the views associated with
+ * the keys passed to {@link #destroyItem(ViewGroup, int, Object) destroyItem}
+ * should be removed. The method {@link #isViewFromObject(View, Object)} identifies
+ * whether a page View is associated with a given key object.</p>
+ *
+ * <p>A very simple PagerAdapter may choose to use the page Views themselves
+ * as key objects, returning them from {@link #instantiateItem(ViewGroup, int)}
+ * after creation and adding them to the parent ViewGroup. A matching
+ * {@link #destroyItem(ViewGroup, int, Object)} implementation would remove the
+ * View from the parent ViewGroup and {@link #isViewFromObject(View, Object)}
+ * could be implemented as <code>return view == object;</code>.</p>
+ *
+ * <p>PagerAdapter supports data set changes. Data set changes must occur on the
+ * main thread and must end with a call to {@link #notifyDataSetChanged()} similar
+ * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data
+ * set change may involve pages being added, removed, or changing position. The
+ * ViewPager will keep the current page active provided the adapter implements
+ * the method {@link #getItemPosition(Object)}.</p>
+ */
+public abstract class PagerAdapter {
+ private DataSetObservable mObservable = new DataSetObservable();
+
+ public static final int POSITION_UNCHANGED = -1;
+ public static final int POSITION_NONE = -2;
+
+ /**
+ * Return the number of views available.
+ */
+ public abstract int getCount();
+
+ /**
+ * Called when a change in the shown pages is going to start being made.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ */
+ public void startUpdate(ViewGroup container) {
+ startUpdate((View) container);
+ }
+
+ /**
+ * Create the page for the given position. The adapter is responsible
+ * for adding the view to the container given here, although it only
+ * must ensure this is done by the time it returns from
+ * {@link #finishUpdate(ViewGroup)}.
+ *
+ * @param container The containing View in which the page will be shown.
+ * @param position The page position to be instantiated.
+ * @return Returns an Object representing the new page. This does not
+ * need to be a View, but can be some other container of the page.
+ */
+ public Object instantiateItem(ViewGroup container, int position) {
+ return instantiateItem((View) container, position);
+ }
+
+ /**
+ * Remove a page for the given position. The adapter is responsible
+ * for removing the view from its container, although it only must ensure
+ * this is done by the time it returns from {@link #finishUpdate(ViewGroup)}.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position to be removed.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(View, int)}.
+ */
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ destroyItem((View) container, position, object);
+ }
+
+ /**
+ * Called to inform the adapter of which item is currently considered to
+ * be the "primary", that is the one show to the user as the current page.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position that is now the primary.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(View, int)}.
+ */
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ setPrimaryItem((View) container, position, object);
+ }
+
+ /**
+ * Called when the a change in the shown pages has been completed. At this
+ * point you must ensure that all of the pages have actually been added or
+ * removed from the container as appropriate.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ */
+ public void finishUpdate(ViewGroup container) {
+ finishUpdate((View) container);
+ }
+
+ /**
+ * Called when a change in the shown pages is going to start being made.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ *
+ * @deprecated Use {@link #startUpdate(ViewGroup)}
+ */
+ public void startUpdate(View container) {
+ }
+
+ /**
+ * Create the page for the given position. The adapter is responsible
+ * for adding the view to the container given here, although it only
+ * must ensure this is done by the time it returns from
+ * {@link #finishUpdate(ViewGroup)}.
+ *
+ * @param container The containing View in which the page will be shown.
+ * @param position The page position to be instantiated.
+ * @return Returns an Object representing the new page. This does not
+ * need to be a View, but can be some other container of the page.
+ *
+ * @deprecated Use {@link #instantiateItem(ViewGroup, int)}
+ */
+ public Object instantiateItem(View container, int position) {
+ throw new UnsupportedOperationException(
+ "Required method instantiateItem was not overridden");
+ }
+
+ /**
+ * Remove a page for the given position. The adapter is responsible
+ * for removing the view from its container, although it only must ensure
+ * this is done by the time it returns from {@link #finishUpdate(View)}.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position to be removed.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(View, int)}.
+ *
+ * @deprecated Use {@link #destroyItem(ViewGroup, int, Object)}
+ */
+ public void destroyItem(View container, int position, Object object) {
+ throw new UnsupportedOperationException("Required method destroyItem was not overridden");
+ }
+
+ /**
+ * Called to inform the adapter of which item is currently considered to
+ * be the "primary", that is the one show to the user as the current page.
+ *
+ * @param container The containing View from which the page will be removed.
+ * @param position The page position that is now the primary.
+ * @param object The same object that was returned by
+ * {@link #instantiateItem(View, int)}.
+ *
+ * @deprecated Use {@link #setPrimaryItem(ViewGroup, int, Object)}
+ */
+ public void setPrimaryItem(View container, int position, Object object) {
+ }
+
+ /**
+ * Called when the a change in the shown pages has been completed. At this
+ * point you must ensure that all of the pages have actually been added or
+ * removed from the container as appropriate.
+ * @param container The containing View which is displaying this adapter's
+ * page views.
+ *
+ * @deprecated Use {@link #finishUpdate(ViewGroup)}
+ */
+ public void finishUpdate(View container) {
+ }
+
+ /**
+ * Determines whether a page View is associated with a specific key object
+ * as returned by {@link #instantiateItem(ViewGroup, int)}. This method is
+ * required for a PagerAdapter to function properly.
+ *
+ * @param view Page View to check for association with <code>object</code>
+ * @param object Object to check for association with <code>view</code>
+ * @return true if <code>view</code> is associated with the key object <code>object</code>
+ */
+ public abstract boolean isViewFromObject(View view, Object object);
+
+ /**
+ * Save any instance state associated with this adapter and its pages that should be
+ * restored if the current UI state needs to be reconstructed.
+ *
+ * @return Saved state for this adapter
+ */
+ public Parcelable saveState() {
+ return null;
+ }
+
+ /**
+ * Restore any instance state associated with this adapter and its pages
+ * that was previously saved by {@link #saveState()}.
+ *
+ * @param state State previously saved by a call to {@link #saveState()}
+ * @param loader A ClassLoader that should be used to instantiate any restored objects
+ */
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ }
+
+ /**
+ * Called when the host view is attempting to determine if an item's position
+ * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
+ * item has not changed or {@link #POSITION_NONE} if the item is no longer present
+ * in the adapter.
+ *
+ * <p>The default implementation assumes that items will never
+ * change position and always returns {@link #POSITION_UNCHANGED}.
+ *
+ * @param object Object representing an item, previously returned by a call to
+ * {@link #instantiateItem(View, int)}.
+ * @return object's new position index from [0, {@link #getCount()}),
+ * {@link #POSITION_UNCHANGED} if the object's position has not changed,
+ * or {@link #POSITION_NONE} if the item is no longer present.
+ */
+ public int getItemPosition(Object object) {
+ return POSITION_UNCHANGED;
+ }
+
+ /**
+ * This method should be called by the application if the data backing this adapter has changed
+ * and associated views should update.
+ */
+ public void notifyDataSetChanged() {
+ mObservable.notifyChanged();
+ }
+
+ /**
+ * Register an observer to receive callbacks related to the adapter's data changing.
+ *
+ * @param observer The {@link android.database.DataSetObserver} which will receive callbacks.
+ */
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mObservable.registerObserver(observer);
+ }
+
+ /**
+ * Unregister an observer from callbacks related to the adapter's data changing.
+ *
+ * @param observer The {@link android.database.DataSetObserver} which will be unregistered.
+ */
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * This method may be called by the ViewPager to obtain a title string
+ * to describe the specified page. This method may return null
+ * indicating no title for this page. The default implementation returns
+ * null.
+ *
+ * @param position The position of the title requested
+ * @return A title for the requested page
+ */
+ public CharSequence getPageTitle(int position) {
+ return null;
+ }
+
+ /**
+ * Returns the proportional width of a given page as a percentage of the
+ * ViewPager's measured width from (0.f-1.f]
+ *
+ * @param position The position of the page requested
+ * @return Proportional width for the given page position
+ */
+ public float getPageWidth(int position) {
+ return 1.f;
+ }
+}
diff --git a/main/src/main/java/android/support/v4n/view/ViewPager.java b/main/src/main/java/android/support/v4n/view/ViewPager.java
new file mode 100644
index 00000000..b2b5238e
--- /dev/null
+++ b/main/src/main/java/android/support/v4n/view/ViewPager.java
@@ -0,0 +1,2902 @@
+/*
+ * Copyright (C) 2011 The Android Open Source 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 android.support.v4n.view;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.support.annotation.DrawableRes;
+import android.support.v4.os.ParcelableCompat;
+import android.support.v4.os.ParcelableCompatCreatorCallbacks;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.KeyEventCompat;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewConfigurationCompat;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.support.v4.widget.EdgeEffectCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Interpolator;
+import android.widget.Scroller;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Layout manager that allows the user to flip left and right
+ * through pages of data. You supply an implementation of a
+ * {@link PagerAdapter} to generate the pages that the view shows.
+ *
+ * <p>Note this class is currently under early design and
+ * development. The API will likely change in later updates of
+ * the compatibility library, requiring changes to the source code
+ * of apps when they are compiled against the newer version.</p>
+ *
+ * <p>ViewPager is most often used in conjunction with {@link android.app.Fragment},
+ * which is a convenient way to supply and manage the lifecycle of each page.
+ * There are standard adapters implemented for using fragments with the ViewPager,
+ * which cover the most common use cases. These are
+ * {@link android.support.v4n.app.FragmentPagerAdapter} and
+ * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these
+ * classes have simple code showing how to build a full user interface
+ * with them.
+ *
+ * <p>For more information about how to use ViewPager, read <a
+ * href="{@docRoot}training/implementing-navigation/lateral.html">Creating Swipe Views with
+ * Tabs</a>.</p>
+ *
+ * <p>Below is a more complicated example of ViewPager, using it in conjunction
+ * with {@link android.app.ActionBar} tabs. You can find other examples of using
+ * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code.
+ *
+ * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java
+ * complete}
+ */
+public class ViewPager extends ViewGroup {
+ private static final String TAG = "ViewPager";
+ private static final boolean DEBUG = false;
+
+ private static final boolean USE_CACHE = false;
+
+ private static final int DEFAULT_OFFSCREEN_PAGES = 1;
+ private static final int MAX_SETTLE_DURATION = 600; // ms
+ private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
+
+ private static final int DEFAULT_GUTTER_SIZE = 16; // dips
+
+ private static final int MIN_FLING_VELOCITY = 400; // dips
+
+ private static final int[] LAYOUT_ATTRS = new int[] {
+ android.R.attr.layout_gravity
+ };
+
+ /**
+ * Used to track what the expected number of items in the adapter should be.
+ * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
+ */
+ private int mExpectedAdapterCount;
+
+ static class ItemInfo {
+ Object object;
+ int position;
+ boolean scrolling;
+ float widthFactor;
+ float offset;
+ }
+
+ private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){
+ @Override
+ public int compare(ItemInfo lhs, ItemInfo rhs) {
+ return lhs.position - rhs.position;
+ }
+ };
+
+ private static final Interpolator sInterpolator = new Interpolator() {
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
+ private final ItemInfo mTempItem = new ItemInfo();
+
+ private final Rect mTempRect = new Rect();
+
+ private PagerAdapter mAdapter;
+ private int mCurItem; // Index of currently displayed page.
+ private int mRestoredCurItem = -1;
+ private Parcelable mRestoredAdapterState = null;
+ private ClassLoader mRestoredClassLoader = null;
+ private Scroller mScroller;
+ private PagerObserver mObserver;
+
+ private int mPageMargin;
+ private Drawable mMarginDrawable;
+ private int mTopPageBounds;
+ private int mBottomPageBounds;
+
+ // Offsets of the first and last items, if known.
+ // Set during population, used to determine if we are at the beginning
+ // or end of the pager data set during touch scrolling.
+ private float mFirstOffset = -Float.MAX_VALUE;
+ private float mLastOffset = Float.MAX_VALUE;
+
+ private int mChildWidthMeasureSpec;
+ private int mChildHeightMeasureSpec;
+ private boolean mInLayout;
+
+ private boolean mScrollingCacheEnabled;
+
+ private boolean mPopulatePending;
+ private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
+
+ private boolean mIsBeingDragged;
+ private boolean mIsUnableToDrag;
+ private boolean mIgnoreGutter;
+ private int mDefaultGutterSize;
+ private int mGutterSize;
+ private int mTouchSlop;
+ /**
+ * Position of the last motion event.
+ */
+ private float mLastMotionX;
+ private float mLastMotionY;
+ private float mInitialMotionX;
+ private float mInitialMotionY;
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private int mActivePointerId = INVALID_POINTER;
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by {@link #mActivePointerId}.
+ */
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+ private int mMinimumVelocity;
+ private int mMaximumVelocity;
+ private int mFlingDistance;
+ private int mCloseEnough;
+
+ // If the pager is at least this close to its final position, complete the scroll
+ // on touch down and let the user interact with the content inside instead of
+ // "catching" the flinging pager.
+ private static final int CLOSE_ENOUGH = 2; // dp
+
+ private boolean mFakeDragging;
+ private long mFakeDragBeginTime;
+
+ private EdgeEffectCompat mLeftEdge;
+ private EdgeEffectCompat mRightEdge;
+
+ private boolean mFirstLayout = true;
+ private boolean mNeedCalculatePageOffsets = false;
+ private boolean mCalledSuper;
+ private int mDecorChildCount;
+
+ private OnPageChangeListener mOnPageChangeListener;
+ private OnPageChangeListener mInternalPageChangeListener;
+ private OnAdapterChangeListener mAdapterChangeListener;
+ private PageTransformer mPageTransformer;
+ private Method mSetChildrenDrawingOrderEnabled;
+
+ private static final int DRAW_ORDER_DEFAULT = 0;
+ private static final int DRAW_ORDER_FORWARD = 1;
+ private static final int DRAW_ORDER_REVERSE = 2;
+ private int mDrawingOrder;
+ private ArrayList<View> mDrawingOrderedChildren;
+ private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
+
+ /**
+ * Indicates that the pager is in an idle, settled state. The current page
+ * is fully in view and no animation is in progress.
+ */
+ public static final int SCROLL_STATE_IDLE = 0;
+
+ /**
+ * Indicates that the pager is currently being dragged by the user.
+ */
+ public static final int SCROLL_STATE_DRAGGING = 1;
+
+ /**
+ * Indicates that the pager is in the process of settling to a final position.
+ */
+ public static final int SCROLL_STATE_SETTLING = 2;
+
+ private final Runnable mEndScrollRunnable = new Runnable() {
+ public void run() {
+ setScrollState(SCROLL_STATE_IDLE);
+ populate();
+ }
+ };
+
+ private int mScrollState = SCROLL_STATE_IDLE;
+
+ /**
+ * Callback interface for responding to changing state of the selected page.
+ */
+ public interface OnPageChangeListener {
+
+ /**
+ * This method will be invoked when the current page is scrolled, either as part
+ * of a programmatically initiated smooth scroll or a user initiated touch scroll.
+ *
+ * @param position Position index of the first page currently being displayed.
+ * Page position+1 will be visible if positionOffset is nonzero.
+ * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
+ * @param positionOffsetPixels Value in pixels indicating the offset from position.
+ */
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
+
+ /**
+ * This method will be invoked when a new page becomes selected. Animation is not
+ * necessarily complete.
+ *
+ * @param position Position index of the new selected page.
+ */
+ public void onPageSelected(int position);
+
+ /**
+ * Called when the scroll state changes. Useful for discovering when the user
+ * begins dragging, when the pager is automatically settling to the current page,
+ * or when it is fully stopped/idle.
+ *
+ * @param state The new scroll state.
+ * @see ViewPager#SCROLL_STATE_IDLE
+ * @see ViewPager#SCROLL_STATE_DRAGGING
+ * @see ViewPager#SCROLL_STATE_SETTLING
+ */
+ public void onPageScrollStateChanged(int state);
+ }
+
+ /**
+ * Simple implementation of the {@link OnPageChangeListener} interface with stub
+ * implementations of each method. Extend this if you do not intend to override
+ * every method of {@link OnPageChangeListener}.
+ */
+ public static class SimpleOnPageChangeListener implements OnPageChangeListener {
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ // This space for rent
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ // This space for rent
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ // This space for rent
+ }
+ }
+
+ /**
+ * A PageTransformer is invoked whenever a visible/attached page is scrolled.
+ * This offers an opportunity for the application to apply a custom transformation
+ * to the page views using animation properties.
+ *
+ * <p>As property animation is only supported as of Android 3.0 and forward,
+ * setting a PageTransformer on a ViewPager on earlier platform versions will
+ * be ignored.</p>
+ */
+ public interface PageTransformer {
+ /**
+ * Apply a property transformation to the given page.
+ *
+ * @param page Apply the transformation to this page
+ * @param position Position of page relative to the current front-and-center
+ * position of the pager. 0 is front and center. 1 is one full
+ * page position to the right, and -1 is one page position to the left.
+ */
+ public void transformPage(View page, float position);
+ }
+
+ /**
+ * Used internally to monitor when adapters are switched.
+ */
+ interface OnAdapterChangeListener {
+ public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
+ }
+
+ /**
+ * Used internally to tag special types of child views that should be added as
+ * pager decorations by default.
+ */
+ interface Decor {}
+
+ public ViewPager(Context context) {
+ super(context);
+ initViewPager();
+ }
+
+ public ViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initViewPager();
+ }
+
+ void initViewPager() {
+ setWillNotDraw(false);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setFocusable(true);
+ final Context context = getContext();
+ mScroller = new Scroller(context, sInterpolator);
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ final float density = context.getResources().getDisplayMetrics().density;
+
+ mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
+ mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
+ mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+ mLeftEdge = new EdgeEffectCompat(context);
+ mRightEdge = new EdgeEffectCompat(context);
+
+ mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
+ mCloseEnough = (int) (CLOSE_ENOUGH * density);
+ mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
+
+ ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());
+
+ if (ViewCompat.getImportantForAccessibility(this)
+ == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ ViewCompat.setImportantForAccessibility(this,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ removeCallbacks(mEndScrollRunnable);
+ super.onDetachedFromWindow();
+ }
+
+ private void setScrollState(int newState) {
+ if (mScrollState == newState) {
+ return;
+ }
+
+ mScrollState = newState;
+ if (mPageTransformer != null) {
+ // PageTransformers can do complex things that benefit from hardware layers.
+ enableLayers(newState != SCROLL_STATE_IDLE);
+ }
+ if (mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageScrollStateChanged(newState);
+ }
+ }
+
+ /**
+ * Set a PagerAdapter that will supply views for this pager as needed.
+ *
+ * @param adapter Adapter to use
+ */
+ public void setAdapter(PagerAdapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ mAdapter.startUpdate(this);
+ for (int i = 0; i < mItems.size(); i++) {
+ final ItemInfo ii = mItems.get(i);
+ mAdapter.destroyItem(this, ii.position, ii.object);
+ }
+ mAdapter.finishUpdate(this);
+ mItems.clear();
+ removeNonDecorViews();
+ mCurItem = 0;
+ scrollTo(0, 0);
+ }
+
+ final PagerAdapter oldAdapter = mAdapter;
+ mAdapter = adapter;
+ mExpectedAdapterCount = 0;
+
+ if (mAdapter != null) {
+ if (mObserver == null) {
+ mObserver = new PagerObserver();
+ }
+ mAdapter.registerDataSetObserver(mObserver);
+ mPopulatePending = false;
+ final boolean wasFirstLayout = mFirstLayout;
+ mFirstLayout = true;
+ mExpectedAdapterCount = mAdapter.getCount();
+ if (mRestoredCurItem >= 0) {
+ mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
+ setCurrentItemInternal(mRestoredCurItem, false, true);
+ mRestoredCurItem = -1;
+ mRestoredAdapterState = null;
+ mRestoredClassLoader = null;
+ } else if (!wasFirstLayout) {
+ populate();
+ } else {
+ requestLayout();
+ }
+ }
+
+ if (mAdapterChangeListener != null && oldAdapter != adapter) {
+ mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
+ }
+ }
+
+ private void removeNonDecorViews() {
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.isDecor) {
+ removeViewAt(i);
+ i--;
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current adapter supplying pages.
+ *
+ * @return The currently registered PagerAdapter
+ */
+ public PagerAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
+ mAdapterChangeListener = listener;
+ }
+
+ private int getClientWidth() {
+ return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ }
+
+ /**
+ * Set the currently selected page. If the ViewPager has already been through its first
+ * layout with its current adapter there will be a smooth animated transition between
+ * the current item and the specified item.
+ *
+ * @param item Item index to select
+ */
+ public void setCurrentItem(int item) {
+ mPopulatePending = false;
+ setCurrentItemInternal(item, !mFirstLayout, false);
+ }
+
+ /**
+ * Set the currently selected page.
+ *
+ * @param item Item index to select
+ * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
+ */
+ public void setCurrentItem(int item, boolean smoothScroll) {
+ mPopulatePending = false;
+ setCurrentItemInternal(item, smoothScroll, false);
+ }
+
+ public int getCurrentItem() {
+ return mCurItem;
+ }
+
+ void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
+ setCurrentItemInternal(item, smoothScroll, always, 0);
+ }
+
+ void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
+ if (mAdapter == null || mAdapter.getCount() <= 0) {
+ setScrollingCacheEnabled(false);
+ return;
+ }
+ if (!always && mCurItem == item && mItems.size() != 0) {
+ setScrollingCacheEnabled(false);
+ return;
+ }
+
+ if (item < 0) {
+ item = 0;
+ } else if (item >= mAdapter.getCount()) {
+ item = mAdapter.getCount() - 1;
+ }
+ final int pageLimit = mOffscreenPageLimit;
+ if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
+ // We are doing a jump by more than one page. To avoid
+ // glitches, we want to keep all current pages in the view
+ // until the scroll ends.
+ for (int i=0; i<mItems.size(); i++) {
+ mItems.get(i).scrolling = true;
+ }
+ }
+ final boolean dispatchSelected = mCurItem != item;
+
+ if (mFirstLayout) {
+ // We don't have any idea how big we are yet and shouldn't have any pages either.
+ // Just set things up and let the pending layout handle things.
+ mCurItem = item;
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(item);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(item);
+ }
+ requestLayout();
+ } else {
+ populate(item);
+ scrollToItem(item, smoothScroll, velocity, dispatchSelected);
+ }
+ }
+
+ private void scrollToItem(int item, boolean smoothScroll, int velocity,
+ boolean dispatchSelected) {
+ final ItemInfo curInfo = infoForPosition(item);
+ int destX = 0;
+ if (curInfo != null) {
+ final int width = getClientWidth();
+ destX = (int) (width * Math.max(mFirstOffset,
+ Math.min(curInfo.offset, mLastOffset)));
+ }
+ if (smoothScroll) {
+ smoothScrollTo(destX, 0, velocity);
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(item);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(item);
+ }
+ } else {
+ if (dispatchSelected && mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageSelected(item);
+ }
+ if (dispatchSelected && mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageSelected(item);
+ }
+ completeScroll(false);
+ scrollTo(destX, 0);
+ pageScrolled(destX);
+ }
+ }
+
+ /**
+ * Set a listener that will be invoked whenever the page changes or is incrementally
+ * scrolled. See {@link OnPageChangeListener}.
+ *
+ * @param listener Listener to set
+ */
+ public void setOnPageChangeListener(OnPageChangeListener listener) {
+ mOnPageChangeListener = listener;
+ }
+
+ /**
+ * Set a {@link PageTransformer} that will be called for each attached page whenever
+ * the scroll position is changed. This allows the application to apply custom property
+ * transformations to each page, overriding the default sliding look and feel.
+ *
+ * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
+ * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>
+ *
+ * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
+ * to be drawn from last to first instead of first to last.
+ * @param transformer PageTransformer that will modify each page's animation properties
+ */
+ public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) {
+ if (Build.VERSION.SDK_INT >= 11) {
+ final boolean hasTransformer = transformer != null;
+ final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
+ mPageTransformer = transformer;
+ setChildrenDrawingOrderEnabledCompat(hasTransformer);
+ if (hasTransformer) {
+ mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
+ } else {
+ mDrawingOrder = DRAW_ORDER_DEFAULT;
+ }
+ if (needsPopulate) populate();
+ }
+ }
+
+ void setChildrenDrawingOrderEnabledCompat(boolean enable) {
+ if (Build.VERSION.SDK_INT >= 7) {
+ if (mSetChildrenDrawingOrderEnabled == null) {
+ try {
+ mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod(
+ "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE });
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e);
+ }
+ }
+ try {
+ mSetChildrenDrawingOrderEnabled.invoke(this, enable);
+ } catch (Exception e) {
+ Log.e(TAG, "Error changing children drawing order", e);
+ }
+ }
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
+ final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
+ return result;
+ }
+
+ /**
+ * Set a separate OnPageChangeListener for internal use by the support library.
+ *
+ * @param listener Listener to set
+ * @return The old listener that was set, if any.
+ */
+ OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) {
+ OnPageChangeListener oldListener = mInternalPageChangeListener;
+ mInternalPageChangeListener = listener;
+ return oldListener;
+ }
+
+ /**
+ * Returns the number of pages that will be retained to either side of the
+ * current page in the view hierarchy in an idle state. Defaults to 1.
+ *
+ * @return How many pages will be kept offscreen on either side
+ * @see #setOffscreenPageLimit(int)
+ */
+ public int getOffscreenPageLimit() {
+ return mOffscreenPageLimit;
+ }
+
+ /**
+ * Set the number of pages that should be retained to either side of the
+ * current page in the view hierarchy in an idle state. Pages beyond this
+ * limit will be recreated from the adapter when needed.
+ *
+ * <p>This is offered as an optimization. If you know in advance the number
+ * of pages you will need to support or have lazy-loading mechanisms in place
+ * on your pages, tweaking this setting can have benefits in perceived smoothness
+ * of paging animations and interaction. If you have a small number of pages (3-4)
+ * that you can keep active all at once, less time will be spent in layout for
+ * newly created view subtrees as the user pages back and forth.</p>
+ *
+ * <p>You should keep this limit low, especially if your pages have complex layouts.
+ * This setting defaults to 1.</p>
+ *
+ * @param limit How many pages will be kept offscreen in an idle state.
+ */
+ public void setOffscreenPageLimit(int limit) {
+ if (limit < DEFAULT_OFFSCREEN_PAGES) {
+ Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
+ DEFAULT_OFFSCREEN_PAGES);
+ limit = DEFAULT_OFFSCREEN_PAGES;
+ }
+ if (limit != mOffscreenPageLimit) {
+ mOffscreenPageLimit = limit;
+ populate();
+ }
+ }
+
+ /**
+ * Set the margin between pages.
+ *
+ * @param marginPixels Distance between adjacent pages in pixels
+ * @see #getPageMargin()
+ * @see #setPageMarginDrawable(Drawable)
+ * @see #setPageMarginDrawable(int)
+ */
+ public void setPageMargin(int marginPixels) {
+ final int oldMargin = mPageMargin;
+ mPageMargin = marginPixels;
+
+ final int width = getWidth();
+ recomputeScrollPosition(width, width, marginPixels, oldMargin);
+
+ requestLayout();
+ }
+
+ /**
+ * Return the margin between pages.
+ *
+ * @return The size of the margin in pixels
+ */
+ public int getPageMargin() {
+ return mPageMargin;
+ }
+
+ /**
+ * Set a drawable that will be used to fill the margin between pages.
+ *
+ * @param d Drawable to display between pages
+ */
+ public void setPageMarginDrawable(Drawable d) {
+ mMarginDrawable = d;
+ if (d != null) refreshDrawableState();
+ setWillNotDraw(d == null);
+ invalidate();
+ }
+
+ /**
+ * Set a drawable that will be used to fill the margin between pages.
+ *
+ * @param resId Resource ID of a drawable to display between pages
+ */
+ public void setPageMarginDrawable(@DrawableRes int resId) {
+ setPageMarginDrawable(getContext().getResources().getDrawable(resId));
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mMarginDrawable;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ final Drawable d = mMarginDrawable;
+ if (d != null && d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ }
+
+ // We want the duration of the page snap animation to be influenced by the distance that
+ // the screen has to travel, however, we don't want this duration to be effected in a
+ // purely linear fashion. Instead, we use this method to moderate the effect that the distance
+ // of travel has on the overall snap duration.
+ float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ /**
+ * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+ *
+ * @param x the number of pixels to scroll by on the X axis
+ * @param y the number of pixels to scroll by on the Y axis
+ */
+ void smoothScrollTo(int x, int y) {
+ smoothScrollTo(x, y, 0);
+ }
+
+ /**
+ * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+ *
+ * @param x the number of pixels to scroll by on the X axis
+ * @param y the number of pixels to scroll by on the Y axis
+ * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
+ */
+ void smoothScrollTo(int x, int y, int velocity) {
+ if (getChildCount() == 0) {
+ // Nothing to do.
+ setScrollingCacheEnabled(false);
+ return;
+ }
+ int sx = getScrollX();
+ int sy = getScrollY();
+ int dx = x - sx;
+ int dy = y - sy;
+ if (dx == 0 && dy == 0) {
+ completeScroll(false);
+ populate();
+ setScrollState(SCROLL_STATE_IDLE);
+ return;
+ }
+
+ setScrollingCacheEnabled(true);
+ setScrollState(SCROLL_STATE_SETTLING);
+
+ final int width = getClientWidth();
+ final int halfWidth = width / 2;
+ final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
+ final float distance = halfWidth + halfWidth *
+ distanceInfluenceForSnapDuration(distanceRatio);
+
+ int duration = 0;
+ velocity = Math.abs(velocity);
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
+ final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
+ duration = (int) ((pageDelta + 1) * 100);
+ }
+ duration = Math.min(duration, MAX_SETTLE_DURATION);
+
+ mScroller.startScroll(sx, sy, dx, dy, duration);
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+
+ ItemInfo addNewItem(int position, int index) {
+ ItemInfo ii = new ItemInfo();
+ ii.position = position;
+ ii.object = mAdapter.instantiateItem(this, position);
+ ii.widthFactor = mAdapter.getPageWidth(position);
+ if (index < 0 || index >= mItems.size()) {
+ mItems.add(ii);
+ } else {
+ mItems.add(index, ii);
+ }
+ return ii;
+ }
+
+ void dataSetChanged() {
+ // This method only gets called if our observer is attached, so mAdapter is non-null.
+
+ final int adapterCount = mAdapter.getCount();
+ mExpectedAdapterCount = adapterCount;
+ boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
+ mItems.size() < adapterCount;
+ int newCurrItem = mCurItem;
+
+ boolean isUpdating = false;
+ for (int i = 0; i < mItems.size(); i++) {
+ final ItemInfo ii = mItems.get(i);
+ final int newPos = mAdapter.getItemPosition(ii.object);
+
+ if (newPos == PagerAdapter.POSITION_UNCHANGED) {
+ continue;
+ }
+
+ if (newPos == PagerAdapter.POSITION_NONE) {
+ mItems.remove(i);
+ i--;
+
+ if (!isUpdating) {
+ mAdapter.startUpdate(this);
+ isUpdating = true;
+ }
+
+ mAdapter.destroyItem(this, ii.position, ii.object);
+ needPopulate = true;
+
+ if (mCurItem == ii.position) {
+ // Keep the current item in the valid range
+ newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
+ needPopulate = true;
+ }
+ continue;
+ }
+
+ if (ii.position != newPos) {
+ if (ii.position == mCurItem) {
+ // Our current item changed position. Follow it.
+ newCurrItem = newPos;
+ }
+
+ ii.position = newPos;
+ needPopulate = true;
+ }
+ }
+
+ if (isUpdating) {
+ mAdapter.finishUpdate(this);
+ }
+
+ Collections.sort(mItems, COMPARATOR);
+
+ if (needPopulate) {
+ // Reset our known page widths; populate will recompute them.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.isDecor) {
+ lp.widthFactor = 0.f;
+ }
+ }
+
+ setCurrentItemInternal(newCurrItem, false, true);
+ requestLayout();
+ }
+ }
+
+ void populate() {
+ populate(mCurItem);
+ }
+
+ void populate(int newCurrentItem) {
+ ItemInfo oldCurInfo = null;
+ int focusDirection = View.FOCUS_FORWARD;
+ if (mCurItem != newCurrentItem) {
+ focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
+ oldCurInfo = infoForPosition(mCurItem);
+ mCurItem = newCurrentItem;
+ }
+
+ if (mAdapter == null) {
+ sortChildDrawingOrder();
+ return;
+ }
+
+ // Bail now if we are waiting to populate. This is to hold off
+ // on creating views from the time the user releases their finger to
+ // fling to a new position until we have finished the scroll to
+ // that position, avoiding glitches from happening at that point.
+ if (mPopulatePending) {
+ if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
+ sortChildDrawingOrder();
+ return;
+ }
+
+ // Also, don't populate until we are attached to a window. This is to
+ // avoid trying to populate before we have restored our view hierarchy
+ // state and conflicting with what is restored.
+ if (getWindowToken() == null) {
+ return;
+ }
+
+ mAdapter.startUpdate(this);
+
+ final int pageLimit = mOffscreenPageLimit;
+ final int startPos = Math.max(0, mCurItem - pageLimit);
+ final int N = mAdapter.getCount();
+ final int endPos = Math.min(N-1, mCurItem + pageLimit);
+
+ if (N != mExpectedAdapterCount) {
+ String resName;
+ try {
+ resName = getResources().getResourceName(getId());
+ } catch (Resources.NotFoundException e) {
+ resName = Integer.toHexString(getId());
+ }
+ throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
+ " contents without calling PagerAdapter#notifyDataSetChanged!" +
+ " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
+ " Pager id: " + resName +
+ " Pager class: " + getClass() +
+ " Problematic adapter: " + mAdapter.getClass());
+ }
+
+ // Locate the currently focused item or add it if needed.
+ int curIndex = -1;
+ ItemInfo curItem = null;
+ for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
+ final ItemInfo ii = mItems.get(curIndex);
+ if (ii.position >= mCurItem) {
+ if (ii.position == mCurItem) curItem = ii;
+ break;
+ }
+ }
+
+ if (curItem == null && N > 0) {
+ curItem = addNewItem(mCurItem, curIndex);
+ }
+
+ // Fill 3x the available width or up to the number of offscreen
+ // pages requested to either side, whichever is larger.
+ // If we have no current item we have no work to do.
+ if (curItem != null) {
+ float extraWidthLeft = 0.f;
+ int itemIndex = curIndex - 1;
+ ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ final int clientWidth = getClientWidth();
+ final float leftWidthNeeded = clientWidth <= 0 ? 0 :
+ 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
+ for (int pos = mCurItem - 1; pos >= 0; pos--) {
+ if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
+ if (ii == null) {
+ break;
+ }
+ if (pos == ii.position && !ii.scrolling) {
+ mItems.remove(itemIndex);
+ mAdapter.destroyItem(this, pos, ii.object);
+ if (DEBUG) {
+ Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+ " view: " + ((View) ii.object));
+ }
+ itemIndex--;
+ curIndex--;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ }
+ } else if (ii != null && pos == ii.position) {
+ extraWidthLeft += ii.widthFactor;
+ itemIndex--;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ } else {
+ ii = addNewItem(pos, itemIndex + 1);
+ extraWidthLeft += ii.widthFactor;
+ curIndex++;
+ ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
+ }
+ }
+
+ float extraWidthRight = curItem.widthFactor;
+ itemIndex = curIndex + 1;
+ if (extraWidthRight < 2.f) {
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ final float rightWidthNeeded = clientWidth <= 0 ? 0 :
+ (float) getPaddingRight() / (float) clientWidth + 2.f;
+ for (int pos = mCurItem + 1; pos < N; pos++) {
+ if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
+ if (ii == null) {
+ break;
+ }
+ if (pos == ii.position && !ii.scrolling) {
+ mItems.remove(itemIndex);
+ mAdapter.destroyItem(this, pos, ii.object);
+ if (DEBUG) {
+ Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
+ " view: " + ((View) ii.object));
+ }
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ }
+ } else if (ii != null && pos == ii.position) {
+ extraWidthRight += ii.widthFactor;
+ itemIndex++;
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ } else {
+ ii = addNewItem(pos, itemIndex);
+ itemIndex++;
+ extraWidthRight += ii.widthFactor;
+ ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
+ }
+ }
+ }
+
+ calculatePageOffsets(curItem, curIndex, oldCurInfo);
+ }
+
+ if (DEBUG) {
+ Log.i(TAG, "Current page list:");
+ for (int i=0; i<mItems.size(); i++) {
+ Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
+ }
+ }
+
+ mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
+
+ mAdapter.finishUpdate(this);
+
+ // Check width measurement of current pages and drawing sort order.
+ // Update LayoutParams as needed.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.childIndex = i;
+ if (!lp.isDecor && lp.widthFactor == 0.f) {
+ // 0 means requery the adapter for this, it doesn't have a valid width.
+ final ItemInfo ii = infoForChild(child);
+ if (ii != null) {
+ lp.widthFactor = ii.widthFactor;
+ lp.position = ii.position;
+ }
+ }
+ }
+ sortChildDrawingOrder();
+
+ if (hasFocus()) {
+ View currentFocused = findFocus();
+ ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
+ if (ii == null || ii.position != mCurItem) {
+ for (int i=0; i<getChildCount(); i++) {
+ View child = getChildAt(i);
+ ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ if (child.requestFocus(focusDirection)) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void sortChildDrawingOrder() {
+ if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
+ if (mDrawingOrderedChildren == null) {
+ mDrawingOrderedChildren = new ArrayList<View>();
+ } else {
+ mDrawingOrderedChildren.clear();
+ }
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ mDrawingOrderedChildren.add(child);
+ }
+ Collections.sort(mDrawingOrderedChildren, sPositionComparator);
+ }
+ }
+
+ private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
+ final int N = mAdapter.getCount();
+ final int width = getClientWidth();
+ final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
+ // Fix up offsets for later layout.
+ if (oldCurInfo != null) {
+ final int oldCurPosition = oldCurInfo.position;
+ // Base offsets off of oldCurInfo.
+ if (oldCurPosition < curItem.position) {
+ int itemIndex = 0;
+ ItemInfo ii = null;
+ float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
+ for (int pos = oldCurPosition + 1;
+ pos <= curItem.position && itemIndex < mItems.size(); pos++) {
+ ii = mItems.get(itemIndex);
+ while (pos > ii.position && itemIndex < mItems.size() - 1) {
+ itemIndex++;
+ ii = mItems.get(itemIndex);
+ }
+ while (pos < ii.position) {
+ // We don't have an item populated for this,
+ // ask the adapter for an offset.
+ offset += mAdapter.getPageWidth(pos) + marginOffset;
+ pos++;
+ }
+ ii.offset = offset;
+ offset += ii.widthFactor + marginOffset;
+ }
+ } else if (oldCurPosition > curItem.position) {
+ int itemIndex = mItems.size() - 1;
+ ItemInfo ii = null;
+ float offset = oldCurInfo.offset;
+ for (int pos = oldCurPosition - 1;
+ pos >= curItem.position && itemIndex >= 0; pos--) {
+ ii = mItems.get(itemIndex);
+ while (pos < ii.position && itemIndex > 0) {
+ itemIndex--;
+ ii = mItems.get(itemIndex);
+ }
+ while (pos > ii.position) {
+ // We don't have an item populated for this,
+ // ask the adapter for an offset.
+ offset -= mAdapter.getPageWidth(pos) + marginOffset;
+ pos--;
+ }
+ offset -= ii.widthFactor + marginOffset;
+ ii.offset = offset;
+ }
+ }
+ }
+
+ // Base all offsets off of curItem.
+ final int itemCount = mItems.size();
+ float offset = curItem.offset;
+ int pos = curItem.position - 1;
+ mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
+ mLastOffset = curItem.position == N - 1 ?
+ curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
+ // Previous pages
+ for (int i = curIndex - 1; i >= 0; i--, pos--) {
+ final ItemInfo ii = mItems.get(i);
+ while (pos > ii.position) {
+ offset -= mAdapter.getPageWidth(pos--) + marginOffset;
+ }
+ offset -= ii.widthFactor + marginOffset;
+ ii.offset = offset;
+ if (ii.position == 0) mFirstOffset = offset;
+ }
+ offset = curItem.offset + curItem.widthFactor + marginOffset;
+ pos = curItem.position + 1;
+ // Next pages
+ for (int i = curIndex + 1; i < itemCount; i++, pos++) {
+ final ItemInfo ii = mItems.get(i);
+ while (pos < ii.position) {
+ offset += mAdapter.getPageWidth(pos++) + marginOffset;
+ }
+ if (ii.position == N - 1) {
+ mLastOffset = offset + ii.widthFactor - 1;
+ }
+ ii.offset = offset;
+ offset += ii.widthFactor + marginOffset;
+ }
+
+ mNeedCalculatePageOffsets = false;
+ }
+
+ /**
+ * This is the persistent state that is saved by ViewPager. Only needed
+ * if you are creating a sublass of ViewPager that must save its own
+ * state, in which case it should implement a subclass of this which
+ * contains that state.
+ */
+ public static class SavedState extends BaseSavedState {
+ int position;
+ Parcelable adapterState;
+ ClassLoader loader;
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(position);
+ out.writeParcelable(adapterState, flags);
+ }
+
+ @Override
+ public String toString() {
+ return "FragmentPager.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " position=" + position + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in, ClassLoader loader) {
+ return new SavedState(in, loader);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ });
+
+ SavedState(Parcel in, ClassLoader loader) {
+ super(in);
+ if (loader == null) {
+ loader = getClass().getClassLoader();
+ }
+ position = in.readInt();
+ adapterState = in.readParcelable(loader);
+ this.loader = loader;
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.position = mCurItem;
+ if (mAdapter != null) {
+ ss.adapterState = mAdapter.saveState();
+ }
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState ss = (SavedState)state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (mAdapter != null) {
+ mAdapter.restoreState(ss.adapterState, ss.loader);
+ setCurrentItemInternal(ss.position, false, true);
+ } else {
+ mRestoredCurItem = ss.position;
+ mRestoredAdapterState = ss.adapterState;
+ mRestoredClassLoader = ss.loader;
+ }
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (!checkLayoutParams(params)) {
+ params = generateLayoutParams(params);
+ }
+ final LayoutParams lp = (LayoutParams) params;
+ lp.isDecor |= child instanceof Decor;
+ if (mInLayout) {
+ if (lp != null && lp.isDecor) {
+ throw new IllegalStateException("Cannot add pager decor view during layout");
+ }
+ lp.needsMeasure = true;
+ addViewInLayout(child, index, params);
+ } else {
+ super.addView(child, index, params);
+ }
+
+ if (USE_CACHE) {
+ if (child.getVisibility() != GONE) {
+ child.setDrawingCacheEnabled(mScrollingCacheEnabled);
+ } else {
+ child.setDrawingCacheEnabled(false);
+ }
+ }
+ }
+
+ @Override
+ public void removeView(View view) {
+ if (mInLayout) {
+ removeViewInLayout(view);
+ } else {
+ super.removeView(view);
+ }
+ }
+
+ ItemInfo infoForChild(View child) {
+ for (int i=0; i<mItems.size(); i++) {
+ ItemInfo ii = mItems.get(i);
+ if (mAdapter.isViewFromObject(child, ii.object)) {
+ return ii;
+ }
+ }
+ return null;
+ }
+
+ ItemInfo infoForAnyChild(View child) {
+ ViewParent parent;
+ while ((parent=child.getParent()) != this) {
+ if (parent == null || !(parent instanceof View)) {
+ return null;
+ }
+ child = (View)parent;
+ }
+ return infoForChild(child);
+ }
+
+ ItemInfo infoForPosition(int position) {
+ for (int i = 0; i < mItems.size(); i++) {
+ ItemInfo ii = mItems.get(i);
+ if (ii.position == position) {
+ return ii;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // For simple implementation, our internal size is always 0.
+ // We depend on the container to specify the layout size of
+ // our view. We can't really know what it is since we will be
+ // adding and removing different arbitrary views and do not
+ // want the layout to change as this happens.
+ setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
+ getDefaultSize(0, heightMeasureSpec));
+
+ final int measuredWidth = getMeasuredWidth();
+ final int maxGutterSize = measuredWidth / 10;
+ mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
+
+ // Children are just made to fill our space.
+ int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
+ int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+
+ /*
+ * Make sure all children have been properly measured. Decor views first.
+ * Right now we cheat and make this less complicated by assuming decor
+ * views won't intersect. We will pin to edges based on gravity.
+ */
+ int size = getChildCount();
+ for (int i = 0; i < size; ++i) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp != null && lp.isDecor) {
+ final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ int widthMode = MeasureSpec.AT_MOST;
+ int heightMode = MeasureSpec.AT_MOST;
+ boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
+ boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
+
+ if (consumeVertical) {
+ widthMode = MeasureSpec.EXACTLY;
+ } else if (consumeHorizontal) {
+ heightMode = MeasureSpec.EXACTLY;
+ }
+
+ int widthSize = childWidthSize;
+ int heightSize = childHeightSize;
+ if (lp.width != LayoutParams.WRAP_CONTENT) {
+ widthMode = MeasureSpec.EXACTLY;
+ if (lp.width != LayoutParams.FILL_PARENT) {
+ widthSize = lp.width;
+ }
+ }
+ if (lp.height != LayoutParams.WRAP_CONTENT) {
+ heightMode = MeasureSpec.EXACTLY;
+ if (lp.height != LayoutParams.FILL_PARENT) {
+ heightSize = lp.height;
+ }
+ }
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
+ child.measure(widthSpec, heightSpec);
+
+ if (consumeVertical) {
+ childHeightSize -= child.getMeasuredHeight();
+ } else if (consumeHorizontal) {
+ childWidthSize -= child.getMeasuredWidth();
+ }
+ }
+ }
+ }
+
+ mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
+ mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
+
+ // Make sure we have created all fragments that we need to have shown.
+ mInLayout = true;
+ populate();
+ mInLayout = false;
+
+ // Page views next.
+ size = getChildCount();
+ for (int i = 0; i < size; ++i) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
+ + ": " + mChildWidthMeasureSpec);
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp == null || !lp.isDecor) {
+ final int widthSpec = MeasureSpec.makeMeasureSpec(
+ (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
+ child.measure(widthSpec, mChildHeightMeasureSpec);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ // Make sure scroll position is set correctly.
+ if (w != oldw) {
+ recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
+ }
+ }
+
+ private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
+ if (oldWidth > 0 && !mItems.isEmpty()) {
+ final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin;
+ final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight()
+ + oldMargin;
+ final int xpos = getScrollX();
+ final float pageOffset = (float) xpos / oldWidthWithMargin;
+ final int newOffsetPixels = (int) (pageOffset * widthWithMargin);
+
+ scrollTo(newOffsetPixels, getScrollY());
+ if (!mScroller.isFinished()) {
+ // We now return to your regularly scheduled scroll, already in progress.
+ final int newDuration = mScroller.getDuration() - mScroller.timePassed();
+ ItemInfo targetInfo = infoForPosition(mCurItem);
+ mScroller.startScroll(newOffsetPixels, 0,
+ (int) (targetInfo.offset * width), 0, newDuration);
+ }
+ } else {
+ final ItemInfo ii = infoForPosition(mCurItem);
+ final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
+ final int scrollPos = (int) (scrollOffset *
+ (width - getPaddingLeft() - getPaddingRight()));
+ if (scrollPos != getScrollX()) {
+ completeScroll(false);
+ scrollTo(scrollPos, getScrollY());
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int count = getChildCount();
+ int width = r - l;
+ int height = b - t;
+ int paddingLeft = getPaddingLeft();
+ int paddingTop = getPaddingTop();
+ int paddingRight = getPaddingRight();
+ int paddingBottom = getPaddingBottom();
+ final int scrollX = getScrollX();
+
+ int decorCount = 0;
+
+ // First pass - decor views. We need to do this in two passes so that
+ // we have the proper offsets for non-decor views later.
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ int childLeft = 0;
+ int childTop = 0;
+ if (lp.isDecor) {
+ final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ switch (hgrav) {
+ default:
+ childLeft = paddingLeft;
+ break;
+ case Gravity.LEFT:
+ childLeft = paddingLeft;
+ paddingLeft += child.getMeasuredWidth();
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
+ paddingLeft);
+ break;
+ case Gravity.RIGHT:
+ childLeft = width - paddingRight - child.getMeasuredWidth();
+ paddingRight += child.getMeasuredWidth();
+ break;
+ }
+ switch (vgrav) {
+ default:
+ childTop = paddingTop;
+ break;
+ case Gravity.TOP:
+ childTop = paddingTop;
+ paddingTop += child.getMeasuredHeight();
+ break;
+ case Gravity.CENTER_VERTICAL:
+ childTop = Math.max((height - child.getMeasuredHeight()) / 2,
+ paddingTop);
+ break;
+ case Gravity.BOTTOM:
+ childTop = height - paddingBottom - child.getMeasuredHeight();
+ paddingBottom += child.getMeasuredHeight();
+ break;
+ }
+ childLeft += scrollX;
+ child.layout(childLeft, childTop,
+ childLeft + child.getMeasuredWidth(),
+ childTop + child.getMeasuredHeight());
+ decorCount++;
+ }
+ }
+ }
+
+ final int childWidth = width - paddingLeft - paddingRight;
+ // Page views. Do this once we have the right padding offsets from above.
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ ItemInfo ii;
+ if (!lp.isDecor && (ii = infoForChild(child)) != null) {
+ int loff = (int) (childWidth * ii.offset);
+ int childLeft = paddingLeft + loff;
+ int childTop = paddingTop;
+ if (lp.needsMeasure) {
+ // This was added during layout and needs measurement.
+ // Do it now that we know what we're working with.
+ lp.needsMeasure = false;
+ final int widthSpec = MeasureSpec.makeMeasureSpec(
+ (int) (childWidth * lp.widthFactor),
+ MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(
+ (int) (height - paddingTop - paddingBottom),
+ MeasureSpec.EXACTLY);
+ child.measure(widthSpec, heightSpec);
+ }
+ if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
+ + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
+ + "x" + child.getMeasuredHeight());
+ child.layout(childLeft, childTop,
+ childLeft + child.getMeasuredWidth(),
+ childTop + child.getMeasuredHeight());
+ }
+ }
+ }
+ mTopPageBounds = paddingTop;
+ mBottomPageBounds = height - paddingBottom;
+ mDecorChildCount = decorCount;
+
+ if (mFirstLayout) {
+ scrollToItem(mCurItem, false, 0, false);
+ }
+ mFirstLayout = false;
+ }
+
+ @Override
+ public void computeScroll() {
+ if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+
+ if (oldX != x || oldY != y) {
+ scrollTo(x, y);
+ if (!pageScrolled(x)) {
+ mScroller.abortAnimation();
+ scrollTo(0, y);
+ }
+ }
+
+ // Keep on drawing until the animation has finished.
+ ViewCompat.postInvalidateOnAnimation(this);
+ return;
+ }
+
+ // Done with scroll, clean up state.
+ completeScroll(true);
+ }
+
+ private boolean pageScrolled(int xpos) {
+ if (mItems.size() == 0) {
+ mCalledSuper = false;
+ onPageScrolled(0, 0, 0);
+ if (!mCalledSuper) {
+ throw new IllegalStateException(
+ "onPageScrolled did not call superclass implementation");
+ }
+ return false;
+ }
+ final ItemInfo ii = infoForCurrentScrollPosition();
+ final int width = getClientWidth();
+ final int widthWithMargin = width + mPageMargin;
+ final float marginOffset = (float) mPageMargin / width;
+ final int currentPage = ii.position;
+ final float pageOffset = (((float) xpos / width) - ii.offset) /
+ (ii.widthFactor + marginOffset);
+ final int offsetPixels = (int) (pageOffset * widthWithMargin);
+
+ mCalledSuper = false;
+ onPageScrolled(currentPage, pageOffset, offsetPixels);
+ if (!mCalledSuper) {
+ throw new IllegalStateException(
+ "onPageScrolled did not call superclass implementation");
+ }
+ return true;
+ }
+
+ /**
+ * This method will be invoked when the current page is scrolled, either as part
+ * of a programmatically initiated smooth scroll or a user initiated touch scroll.
+ * If you override this method you must call through to the superclass implementation
+ * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
+ * returns.
+ *
+ * @param position Position index of the first page currently being displayed.
+ * Page position+1 will be visible if positionOffset is nonzero.
+ * @param offset Value from [0, 1) indicating the offset from the page at position.
+ * @param offsetPixels Value in pixels indicating the offset from position.
+ */
+ protected void onPageScrolled(int position, float offset, int offsetPixels) {
+ // Offset any decor views if needed - keep them on-screen at all times.
+ if (mDecorChildCount > 0) {
+ final int scrollX = getScrollX();
+ int paddingLeft = getPaddingLeft();
+ int paddingRight = getPaddingRight();
+ final int width = getWidth();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.isDecor) continue;
+
+ final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ int childLeft = 0;
+ switch (hgrav) {
+ default:
+ childLeft = paddingLeft;
+ break;
+ case Gravity.LEFT:
+ childLeft = paddingLeft;
+ paddingLeft += child.getWidth();
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
+ paddingLeft);
+ break;
+ case Gravity.RIGHT:
+ childLeft = width - paddingRight - child.getMeasuredWidth();
+ paddingRight += child.getMeasuredWidth();
+ break;
+ }
+ childLeft += scrollX;
+
+ final int childOffset = childLeft - child.getLeft();
+ if (childOffset != 0) {
+ child.offsetLeftAndRight(childOffset);
+ }
+ }
+ }
+
+ if (mOnPageChangeListener != null) {
+ mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+ }
+ if (mInternalPageChangeListener != null) {
+ mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
+ }
+
+ if (mPageTransformer != null) {
+ final int scrollX = getScrollX();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (lp.isDecor) continue;
+
+ final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
+ mPageTransformer.transformPage(child, transformPos);
+ }
+ }
+
+ mCalledSuper = true;
+ }
+
+ private void completeScroll(boolean postEvents) {
+ boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
+ if (needPopulate) {
+ // Done with scroll, no longer want to cache view drawing.
+ setScrollingCacheEnabled(false);
+ mScroller.abortAnimation();
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+ if (oldX != x || oldY != y) {
+ scrollTo(x, y);
+ }
+ }
+ mPopulatePending = false;
+ for (int i=0; i<mItems.size(); i++) {
+ ItemInfo ii = mItems.get(i);
+ if (ii.scrolling) {
+ needPopulate = true;
+ ii.scrolling = false;
+ }
+ }
+ if (needPopulate) {
+ if (postEvents) {
+ ViewCompat.postOnAnimation(this, mEndScrollRunnable);
+ } else {
+ mEndScrollRunnable.run();
+ }
+ }
+ }
+
+ private boolean isGutterDrag(float x, float dx) {
+ return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0);
+ }
+
+ private void enableLayers(boolean enable) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final int layerType = enable ?
+ ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
+ ViewCompat.setLayerType(getChildAt(i), layerType, null);
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ /*
+ * This method JUST determines whether we want to intercept the motion.
+ * If we return true, onMotionEvent will be called and we do the actual
+ * scrolling there.
+ */
+
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+
+ // Always take care of the touch gesture being complete.
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ // Release the drag.
+ if (DEBUG) Log.v(TAG, "Intercept done!");
+ mIsBeingDragged = false;
+ mIsUnableToDrag = false;
+ mActivePointerId = INVALID_POINTER;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ return false;
+ }
+
+ // Nothing more to do here if we have decided whether or not we
+ // are dragging.
+ if (action != MotionEvent.ACTION_DOWN) {
+ if (mIsBeingDragged) {
+ if (DEBUG) Log.v(TAG, "Intercept returning true!");
+ return true;
+ }
+ if (mIsUnableToDrag) {
+ if (DEBUG) Log.v(TAG, "Intercept returning false!");
+ return false;
+ }
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE: {
+ /*
+ * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+ * whether the user has moved far enough from his original down touch.
+ */
+
+ /*
+ * Locally do absolute value. mLastMotionY is set to the y value
+ * of the down event.
+ */
+ final int activePointerId = mActivePointerId;
+ if (activePointerId == INVALID_POINTER) {
+ // If we don't have a valid id, the touch down wasn't on content.
+ break;
+ }
+
+ final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
+ final float x = MotionEventCompat.getX(ev, pointerIndex);
+ final float dx = x - mLastMotionX;
+ final float xDiff = Math.abs(dx);
+ final float y = MotionEventCompat.getY(ev, pointerIndex);
+ final float yDiff = Math.abs(y - mInitialMotionY);
+ if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+
+ if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
+ canScroll(this, false, (int) dx, (int) x, (int) y)) {
+ // Nested view has scrollable area under this point. Let it be handled there.
+ mLastMotionX = x;
+ mLastMotionY = y;
+ mIsUnableToDrag = true;
+ return false;
+ }
+ if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
+ if (DEBUG) Log.v(TAG, "Starting drag!");
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
+ mInitialMotionX - mTouchSlop;
+ mLastMotionY = y;
+ setScrollingCacheEnabled(true);
+ } else if (yDiff > mTouchSlop) {
+ // The finger has moved enough in the vertical
+ // direction to be counted as a drag... abort
+ // any attempt to drag horizontally, to work correctly
+ // with children that have scrolling containers.
+ if (DEBUG) Log.v(TAG, "Starting unable to drag!");
+ mIsUnableToDrag = true;
+ }
+ if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ if (performDrag(x)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_DOWN: {
+ /*
+ * Remember location of down touch.
+ * ACTION_DOWN always refers to pointer index 0.
+ */
+ mLastMotionX = mInitialMotionX = ev.getX();
+ mLastMotionY = mInitialMotionY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mIsUnableToDrag = false;
+
+ mScroller.computeScrollOffset();
+ if (mScrollState == SCROLL_STATE_SETTLING &&
+ Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
+ // Let the user 'catch' the pager as it animates.
+ mScroller.abortAnimation();
+ mPopulatePending = false;
+ populate();
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ } else {
+ completeScroll(false);
+ mIsBeingDragged = false;
+ }
+
+ if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
+ + " mIsBeingDragged=" + mIsBeingDragged
+ + "mIsUnableToDrag=" + mIsUnableToDrag);
+ break;
+ }
+
+ case MotionEventCompat.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ /*
+ * The only time we want to intercept motion events is if we are in the
+ * drag mode.
+ */
+ return mIsBeingDragged;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mFakeDragging) {
+ // A fake drag is in progress already, ignore this real one
+ // but still eat the touch events.
+ // (It is likely that the user is multi-touching the screen.)
+ return true;
+ }
+
+ if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
+ // Don't handle edge touches immediately -- they may actually belong to one of our
+ // descendants.
+ return false;
+ }
+
+ if (mAdapter == null || mAdapter.getCount() == 0) {
+ // Nothing to present or scroll; nothing to touch.
+ return false;
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+ boolean needsInvalidate = false;
+
+ switch (action & MotionEventCompat.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ mScroller.abortAnimation();
+ mPopulatePending = false;
+ populate();
+
+ // Remember where the motion event started
+ mLastMotionX = mInitialMotionX = ev.getX();
+ mLastMotionY = mInitialMotionY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE:
+ if (!mIsBeingDragged) {
+ final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ final float x = MotionEventCompat.getX(ev, pointerIndex);
+ final float xDiff = Math.abs(x - mLastMotionX);
+ final float y = MotionEventCompat.getY(ev, pointerIndex);
+ final float yDiff = Math.abs(y - mLastMotionY);
+ if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
+ if (xDiff > mTouchSlop && xDiff > yDiff) {
+ if (DEBUG) Log.v(TAG, "Starting drag!");
+ mIsBeingDragged = true;
+ requestParentDisallowInterceptTouchEvent(true);
+ mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
+ mInitialMotionX - mTouchSlop;
+ mLastMotionY = y;
+ setScrollState(SCROLL_STATE_DRAGGING);
+ setScrollingCacheEnabled(true);
+
+ // Disallow Parent Intercept, just in case
+ ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+ }
+ // Not else! Note that mIsBeingDragged can be set above.
+ if (mIsBeingDragged) {
+ // Scroll to follow the motion event
+ final int activePointerIndex = MotionEventCompat.findPointerIndex(
+ ev, mActivePointerId);
+ final float x = MotionEventCompat.getX(ev, activePointerIndex);
+ needsInvalidate |= performDrag(x);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mIsBeingDragged) {
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
+ velocityTracker, mActivePointerId);
+ mPopulatePending = true;
+ final int width = getClientWidth();
+ final int scrollX = getScrollX();
+ final ItemInfo ii = infoForCurrentScrollPosition();
+ final int currentPage = ii.position;
+ final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
+ final int activePointerIndex =
+ MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ final float x = MotionEventCompat.getX(ev, activePointerIndex);
+ final int totalDelta = (int) (x - mInitialMotionX);
+ int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
+ totalDelta);
+ setCurrentItemInternal(nextPage, true, true, initialVelocity);
+
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsBeingDragged) {
+ scrollToItem(mCurItem, true, 0, false);
+ mActivePointerId = INVALID_POINTER;
+ endDrag();
+ needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
+ }
+ break;
+ case MotionEventCompat.ACTION_POINTER_DOWN: {
+ final int index = MotionEventCompat.getActionIndex(ev);
+ final float x = MotionEventCompat.getX(ev, index);
+ mLastMotionX = x;
+ mActivePointerId = MotionEventCompat.getPointerId(ev, index);
+ break;
+ }
+ case MotionEventCompat.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ mLastMotionX = MotionEventCompat.getX(ev,
+ MotionEventCompat.findPointerIndex(ev, mActivePointerId));
+ break;
+ }
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ return true;
+ }
+
+ private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+
+ private boolean performDrag(float x) {
+ boolean needsInvalidate = false;
+
+ final float deltaX = mLastMotionX - x;
+ mLastMotionX = x;
+
+ float oldScrollX = getScrollX();
+ float scrollX = oldScrollX + deltaX;
+ final int width = getClientWidth();
+
+ float leftBound = width * mFirstOffset;
+ float rightBound = width * mLastOffset;
+ boolean leftAbsolute = true;
+ boolean rightAbsolute = true;
+
+ final ItemInfo firstItem = mItems.get(0);
+ final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+ if (firstItem.position != 0) {
+ leftAbsolute = false;
+ leftBound = firstItem.offset * width;
+ }
+ if (lastItem.position != mAdapter.getCount() - 1) {
+ rightAbsolute = false;
+ rightBound = lastItem.offset * width;
+ }
+
+ if (scrollX < leftBound) {
+ if (leftAbsolute) {
+ float over = leftBound - scrollX;
+ needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width);
+ }
+ scrollX = leftBound;
+ } else if (scrollX > rightBound) {
+ if (rightAbsolute) {
+ float over = scrollX - rightBound;
+ needsInvalidate = mRightEdge.onPull(Math.abs(over) / width);
+ }
+ scrollX = rightBound;
+ }
+ // Don't lose the rounded component
+ mLastMotionX += scrollX - (int) scrollX;
+ scrollTo((int) scrollX, getScrollY());
+ pageScrolled((int) scrollX);
+
+ return needsInvalidate;
+ }
+
+ /**
+ * @return Info about the page at the current scroll position.
+ * This can be synthetic for a missing middle page; the 'object' field can be null.
+ */
+ private ItemInfo infoForCurrentScrollPosition() {
+ final int width = getClientWidth();
+ final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0;
+ final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
+ int lastPos = -1;
+ float lastOffset = 0.f;
+ float lastWidth = 0.f;
+ boolean first = true;
+
+ ItemInfo lastItem = null;
+ for (int i = 0; i < mItems.size(); i++) {
+ ItemInfo ii = mItems.get(i);
+ float offset;
+ if (!first && ii.position != lastPos + 1) {
+ // Create a synthetic item for a missing page.
+ ii = mTempItem;
+ ii.offset = lastOffset + lastWidth + marginOffset;
+ ii.position = lastPos + 1;
+ ii.widthFactor = mAdapter.getPageWidth(ii.position);
+ i--;
+ }
+ offset = ii.offset;
+
+ final float leftBound = offset;
+ final float rightBound = offset + ii.widthFactor + marginOffset;
+ if (first || scrollOffset >= leftBound) {
+ if (scrollOffset < rightBound || i == mItems.size() - 1) {
+ return ii;
+ }
+ } else {
+ return lastItem;
+ }
+ first = false;
+ lastPos = ii.position;
+ lastOffset = offset;
+ lastWidth = ii.widthFactor;
+ lastItem = ii;
+ }
+
+ return lastItem;
+ }
+
+ private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
+ int targetPage;
+ if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+ targetPage = velocity > 0 ? currentPage : currentPage + 1;
+ } else {
+ final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
+ targetPage = (int) (currentPage + pageOffset + truncator);
+ }
+
+ if (mItems.size() > 0) {
+ final ItemInfo firstItem = mItems.get(0);
+ final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+
+ // Only let the user target pages we have items for
+ targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
+ }
+
+ return targetPage;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ boolean needsInvalidate = false;
+
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+ if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
+ mAdapter != null && mAdapter.getCount() > 1)) {
+ if (!mLeftEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+ final int width = getWidth();
+
+ canvas.rotate(270);
+ canvas.translate(-height + getPaddingTop(), mFirstOffset * width);
+ mLeftEdge.setSize(height, width);
+ needsInvalidate |= mLeftEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mRightEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.rotate(90);
+ canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width);
+ mRightEdge.setSize(height, width);
+ needsInvalidate |= mRightEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ }
+ } else {
+ mLeftEdge.finish();
+ mRightEdge.finish();
+ }
+
+ if (needsInvalidate) {
+ // Keep animating
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Draw the margin drawable between pages if needed.
+ if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
+ final int scrollX = getScrollX();
+ final int width = getWidth();
+
+ final float marginOffset = (float) mPageMargin / width;
+ int itemIndex = 0;
+ ItemInfo ii = mItems.get(0);
+ float offset = ii.offset;
+ final int itemCount = mItems.size();
+ final int firstPos = ii.position;
+ final int lastPos = mItems.get(itemCount - 1).position;
+ for (int pos = firstPos; pos < lastPos; pos++) {
+ while (pos > ii.position && itemIndex < itemCount) {
+ ii = mItems.get(++itemIndex);
+ }
+
+ float drawAt;
+ if (pos == ii.position) {
+ drawAt = (ii.offset + ii.widthFactor) * width;
+ offset = ii.offset + ii.widthFactor + marginOffset;
+ } else {
+ float widthFactor = mAdapter.getPageWidth(pos);
+ drawAt = (offset + widthFactor) * width;
+ offset += widthFactor + marginOffset;
+ }
+
+ if (drawAt + mPageMargin > scrollX) {
+ mMarginDrawable.setBounds((int) drawAt, mTopPageBounds,
+ (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds);
+ mMarginDrawable.draw(canvas);
+ }
+
+ if (drawAt > scrollX + width) {
+ break; // No more visible, no sense in continuing
+ }
+ }
+ }
+ }
+
+ /**
+ * Start a fake drag of the pager.
+ *
+ * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager
+ * with the touch scrolling of another view, while still letting the ViewPager
+ * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
+ * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
+ * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
+ *
+ * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag
+ * is already in progress, this method will return false.
+ *
+ * @return true if the fake drag began successfully, false if it could not be started.
+ *
+ * @see #fakeDragBy(float)
+ * @see #endFakeDrag()
+ */
+ public boolean beginFakeDrag() {
+ if (mIsBeingDragged) {
+ return false;
+ }
+ mFakeDragging = true;
+ setScrollState(SCROLL_STATE_DRAGGING);
+ mInitialMotionX = mLastMotionX = 0;
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ final long time = SystemClock.uptimeMillis();
+ final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
+ mVelocityTracker.addMovement(ev);
+ ev.recycle();
+ mFakeDragBeginTime = time;
+ return true;
+ }
+
+ /**
+ * End a fake drag of the pager.
+ *
+ * @see #beginFakeDrag()
+ * @see #fakeDragBy(float)
+ */
+ public void endFakeDrag() {
+ if (!mFakeDragging) {
+ throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
+ }
+
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
+ velocityTracker, mActivePointerId);
+ mPopulatePending = true;
+ final int width = getClientWidth();
+ final int scrollX = getScrollX();
+ final ItemInfo ii = infoForCurrentScrollPosition();
+ final int currentPage = ii.position;
+ final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
+ final int totalDelta = (int) (mLastMotionX - mInitialMotionX);
+ int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
+ totalDelta);
+ setCurrentItemInternal(nextPage, true, true, initialVelocity);
+ endDrag();
+
+ mFakeDragging = false;
+ }
+
+ /**
+ * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
+ *
+ * @param xOffset Offset in pixels to drag by.
+ * @see #beginFakeDrag()
+ * @see #endFakeDrag()
+ */
+ public void fakeDragBy(float xOffset) {
+ if (!mFakeDragging) {
+ throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
+ }
+
+ mLastMotionX += xOffset;
+
+ float oldScrollX = getScrollX();
+ float scrollX = oldScrollX - xOffset;
+ final int width = getClientWidth();
+
+ float leftBound = width * mFirstOffset;
+ float rightBound = width * mLastOffset;
+
+ final ItemInfo firstItem = mItems.get(0);
+ final ItemInfo lastItem = mItems.get(mItems.size() - 1);
+ if (firstItem.position != 0) {
+ leftBound = firstItem.offset * width;
+ }
+ if (lastItem.position != mAdapter.getCount() - 1) {
+ rightBound = lastItem.offset * width;
+ }
+
+ if (scrollX < leftBound) {
+ scrollX = leftBound;
+ } else if (scrollX > rightBound) {
+ scrollX = rightBound;
+ }
+ // Don't lose the rounded component
+ mLastMotionX += scrollX - (int) scrollX;
+ scrollTo((int) scrollX, getScrollY());
+ pageScrolled((int) scrollX);
+
+ // Synthesize an event for the VelocityTracker.
+ final long time = SystemClock.uptimeMillis();
+ final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
+ mLastMotionX, 0, 0);
+ mVelocityTracker.addMovement(ev);
+ ev.recycle();
+ }
+
+ /**
+ * Returns true if a fake drag is in progress.
+ *
+ * @return true if currently in a fake drag, false otherwise.
+ *
+ * @see #beginFakeDrag()
+ * @see #fakeDragBy(float)
+ * @see #endFakeDrag()
+ */
+ public boolean isFakeDragging() {
+ return mFakeDragging;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = MotionEventCompat.getActionIndex(ev);
+ final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
+ mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
+ if (mVelocityTracker != null) {
+ mVelocityTracker.clear();
+ }
+ }
+ }
+
+ private void endDrag() {
+ mIsBeingDragged = false;
+ mIsUnableToDrag = false;
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void setScrollingCacheEnabled(boolean enabled) {
+ if (mScrollingCacheEnabled != enabled) {
+ mScrollingCacheEnabled = enabled;
+ if (USE_CACHE) {
+ final int size = getChildCount();
+ for (int i = 0; i < size; ++i) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ child.setDrawingCacheEnabled(enabled);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean canScrollHorizontally(int direction) {
+ if (mAdapter == null) {
+ return false;
+ }
+
+ final int width = getClientWidth();
+ final int scrollX = getScrollX();
+ if (direction < 0) {
+ return (scrollX > (int) (width * mFirstOffset));
+ } else if (direction > 0) {
+ return (scrollX < (int) (width * mLastOffset));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Tests scrollability within child views of v given a delta of dx.
+ *
+ * @param v View to test for horizontal scrollability
+ * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+ * or just its children (false).
+ * @param dx Delta scrolled in pixels
+ * @param x X coordinate of the active touch point
+ * @param y Y coordinate of the active touch point
+ * @return true if child views of v can be scrolled by delta of dx.
+ */
+ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+ if (v instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) v;
+ final int scrollX = v.getScrollX();
+ final int scrollY = v.getScrollY();
+ final int count = group.getChildCount();
+ // Count backwards - let topmost views consume scroll distance first.
+ for (int i = count - 1; i >= 0; i--) {
+ // TODO: Add versioned support here for transformed views.
+ // This will not work for transformed views in Honeycomb+
+ final View child = group.getChildAt(i);
+ if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
+ y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
+ canScroll(child, true, dx, x + scrollX - child.getLeft(),
+ y + scrollY - child.getTop())) {
+ return true;
+ }
+ }
+ }
+
+ return checkV && ViewCompat.canScrollHorizontally(v, -dx);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Let the focused view and/or our descendants get the key first
+ return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+ }
+
+ /**
+ * You can call this function yourself to have the scroll view perform
+ * scrolling from a key event, just as if the event had been dispatched to
+ * it by the view hierarchy.
+ *
+ * @param event The key event to execute.
+ * @return Return true if the event was handled, else false.
+ */
+ public boolean executeKeyEvent(KeyEvent event) {
+ boolean handled = false;
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled = arrowScroll(FOCUS_LEFT);
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled = arrowScroll(FOCUS_RIGHT);
+ break;
+ case KeyEvent.KEYCODE_TAB:
+ if (Build.VERSION.SDK_INT >= 11) {
+ // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
+ // before Android 3.0. Ignore the tab key on those devices.
+ if (KeyEventCompat.hasNoModifiers(event)) {
+ handled = arrowScroll(FOCUS_FORWARD);
+ } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
+ handled = arrowScroll(FOCUS_BACKWARD);
+ }
+ }
+ break;
+ }
+ }
+ return handled;
+ }
+
+ public boolean arrowScroll(int direction) {
+ View currentFocused = findFocus();
+ if (currentFocused == this) {
+ currentFocused = null;
+ } else if (currentFocused != null) {
+ boolean isChild = false;
+ for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
+ parent = parent.getParent()) {
+ if (parent == this) {
+ isChild = true;
+ break;
+ }
+ }
+ if (!isChild) {
+ // This would cause the focus search down below to fail in fun ways.
+ final StringBuilder sb = new StringBuilder();
+ sb.append(currentFocused.getClass().getSimpleName());
+ for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
+ parent = parent.getParent()) {
+ sb.append(" => ").append(parent.getClass().getSimpleName());
+ }
+ Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
+ "current focused view " + sb.toString());
+ currentFocused = null;
+ }
+ }
+
+ boolean handled = false;
+
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
+ direction);
+ if (nextFocused != null && nextFocused != currentFocused) {
+ if (direction == View.FOCUS_LEFT) {
+ // If there is nothing to the left, or this is causing us to
+ // jump to the right, then what we really want to do is page left.
+ final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
+ final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
+ if (currentFocused != null && nextLeft >= currLeft) {
+ handled = pageLeft();
+ } else {
+ handled = nextFocused.requestFocus();
+ }
+ } else if (direction == View.FOCUS_RIGHT) {
+ // If there is nothing to the right, or this is causing us to
+ // jump to the left, then what we really want to do is page right.
+ final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
+ final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
+ if (currentFocused != null && nextLeft <= currLeft) {
+ handled = pageRight();
+ } else {
+ handled = nextFocused.requestFocus();
+ }
+ }
+ } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
+ // Trying to move left and nothing there; try to page.
+ handled = pageLeft();
+ } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
+ // Trying to move right and nothing there; try to page.
+ handled = pageRight();
+ }
+ if (handled) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ }
+ return handled;
+ }
+
+ private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
+ if (outRect == null) {
+ outRect = new Rect();
+ }
+ if (child == null) {
+ outRect.set(0, 0, 0, 0);
+ return outRect;
+ }
+ outRect.left = child.getLeft();
+ outRect.right = child.getRight();
+ outRect.top = child.getTop();
+ outRect.bottom = child.getBottom();
+
+ ViewParent parent = child.getParent();
+ while (parent instanceof ViewGroup && parent != this) {
+ final ViewGroup group = (ViewGroup) parent;
+ outRect.left += group.getLeft();
+ outRect.right += group.getRight();
+ outRect.top += group.getTop();
+ outRect.bottom += group.getBottom();
+
+ parent = group.getParent();
+ }
+ return outRect;
+ }
+
+ boolean pageLeft() {
+ if (mCurItem > 0) {
+ setCurrentItem(mCurItem-1, true);
+ return true;
+ }
+ return false;
+ }
+
+ boolean pageRight() {
+ if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) {
+ setCurrentItem(mCurItem+1, true);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * We only want the current page that is being shown to be focusable.
+ */
+ @Override
+ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+ final int focusableCount = views.size();
+
+ final int descendantFocusability = getDescendantFocusability();
+
+ if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ ItemInfo ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ child.addFocusables(views, direction, focusableMode);
+ }
+ }
+ }
+ }
+
+ // we add ourselves (if focusable) in all cases except for when we are
+ // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
+ // to avoid the focus search finding layouts when a more precise search
+ // among the focusable children would be more interesting.
+ if (
+ descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
+ // No focusable descendants
+ (focusableCount == views.size())) {
+ // Note that we can't call the superclass here, because it will
+ // add all views in. So we need to do the same thing View does.
+ if (!isFocusable()) {
+ return;
+ }
+ if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
+ isInTouchMode() && !isFocusableInTouchMode()) {
+ return;
+ }
+ if (views != null) {
+ views.add(this);
+ }
+ }
+ }
+
+ /**
+ * We only want the current page that is being shown to be touchable.
+ */
+ @Override
+ public void addTouchables(ArrayList<View> views) {
+ // Note that we don't call super.addTouchables(), which means that
+ // we don't call View.addTouchables(). This is okay because a ViewPager
+ // is itself not touchable.
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ ItemInfo ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ child.addTouchables(views);
+ }
+ }
+ }
+ }
+
+ /**
+ * We only want the current page that is being shown to be focusable.
+ */
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+ int index;
+ int increment;
+ int end;
+ int count = getChildCount();
+ if ((direction & FOCUS_FORWARD) != 0) {
+ index = 0;
+ increment = 1;
+ end = count;
+ } else {
+ index = count - 1;
+ increment = -1;
+ end = -1;
+ }
+ for (int i = index; i != end; i += increment) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ ItemInfo ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem) {
+ if (child.requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ // Dispatch scroll events from this ViewPager.
+ if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) {
+ return super.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ // Dispatch all other accessibility events from the current page.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == VISIBLE) {
+ final ItemInfo ii = infoForChild(child);
+ if (ii != null && ii.position == mCurItem &&
+ child.dispatchPopulateAccessibilityEvent(event)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams();
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return generateDefaultLayoutParams();
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && super.checkLayoutParams(p);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ class MyAccessibilityDelegate extends AccessibilityDelegateCompat {
+
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ event.setClassName(ViewPager.class.getName());
+ final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain();
+ recordCompat.setScrollable(canScroll());
+ if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED
+ && mAdapter != null) {
+ recordCompat.setItemCount(mAdapter.getCount());
+ recordCompat.setFromIndex(mCurItem);
+ recordCompat.setToIndex(mCurItem);
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setClassName(ViewPager.class.getName());
+ info.setScrollable(canScroll());
+ if (canScrollHorizontally(1)) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
+ }
+ if (canScrollHorizontally(-1)) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (super.performAccessibilityAction(host, action, args)) {
+ return true;
+ }
+ switch (action) {
+ case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
+ if (canScrollHorizontally(1)) {
+ setCurrentItem(mCurItem + 1);
+ return true;
+ }
+ } return false;
+ case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
+ if (canScrollHorizontally(-1)) {
+ setCurrentItem(mCurItem - 1);
+ return true;
+ }
+ } return false;
+ }
+ return false;
+ }
+
+ private boolean canScroll() {
+ return (mAdapter != null) && (mAdapter.getCount() > 1);
+ }
+ }
+
+ private class PagerObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ dataSetChanged();
+ }
+ @Override
+ public void onInvalidated() {
+ dataSetChanged();
+ }
+ }
+
+ /**
+ * Layout parameters that should be supplied for views added to a
+ * ViewPager.
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ /**
+ * true if this view is a decoration on the pager itself and not
+ * a view supplied by the adapter.
+ */
+ public boolean isDecor;
+
+ /**
+ * Gravity setting for use on decor views only:
+ * Where to position the view page within the overall ViewPager
+ * container; constants are defined in {@link android.view.Gravity}.
+ */
+ public int gravity;
+
+ /**
+ * Width as a 0-1 multiplier of the measured pager width
+ */
+ float widthFactor = 0.f;
+
+ /**
+ * true if this view was added during layout and needs to be measured
+ * before being positioned.
+ */
+ boolean needsMeasure;
+
+ /**
+ * Adapter position this view is for if !isDecor
+ */
+ int position;
+
+ /**
+ * Current child index within the ViewPager that this view occupies
+ */
+ int childIndex;
+
+ public LayoutParams() {
+ super(FILL_PARENT, FILL_PARENT);
+ }
+
+ public LayoutParams(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+ gravity = a.getInteger(0, Gravity.TOP);
+ a.recycle();
+ }
+ }
+
+ static class ViewPositionComparator implements Comparator<View> {
+ @Override
+ public int compare(View lhs, View rhs) {
+ final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
+ final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
+ if (llp.isDecor != rlp.isDecor) {
+ return llp.isDecor ? 1 : -1;
+ }
+ return llp.position - rlp.position;
+ }
+ }
+}
diff --git a/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java b/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java
index 1124fe47..d59640de 100644
--- a/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java
+++ b/main/src/main/java/de/blinkt/openvpn/activities/MainActivity.java
@@ -5,95 +5,120 @@
package de.blinkt.openvpn.activities;
-import android.app.ActionBar;
-import android.app.ActionBar.Tab;
import android.app.Activity;
import android.app.Fragment;
-import android.app.FragmentTransaction;
+import android.app.FragmentManager;
import android.content.Intent;
+import android.support.annotation.StringRes;
+import android.support.v4n.app.FragmentStatePagerAdapter;
+import android.support.v4n.view.ViewPager;
+
+import java.util.Vector;
import de.blinkt.openvpn.R;
-import de.blinkt.openvpn.fragments.*;
+import de.blinkt.openvpn.fragments.AboutFragment;
+import de.blinkt.openvpn.fragments.FaqFragment;
+import de.blinkt.openvpn.fragments.GeneralSettings;
+import de.blinkt.openvpn.fragments.SendDumpFragment;
+import de.blinkt.openvpn.fragments.VPNProfileList;
+import de.blinkt.openvpn.views.PagerSlidingTabStrip;
+import de.blinkt.openvpn.views.SlidingTabLayout;
+import de.blinkt.openvpn.views.TabBarView;
public class MainActivity extends Activity {
- protected void onCreate(android.os.Bundle savedInstanceState) {
+ private ViewPager mPager;
+ private ScreenSlidePagerAdapter mPagerAdapter;
+ private SlidingTabLayout mSlidingTabLayout;
+
+ protected void onCreate(android.os.Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- ActionBar bar = getActionBar();
- bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
- Tab vpnListTab = bar.newTab().setText(R.string.vpn_list_title);
- Tab generalTab = bar.newTab().setText(R.string.generalsettings);
- Tab faqtab = bar.newTab().setText(R.string.faq);
- Tab abouttab = bar.newTab().setText(R.string.about);
+ setContentView(R.layout.main_activity);
+
+
+ // Instantiate a ViewPager and a PagerAdapter.
+ mPager = (ViewPager) findViewById(R.id.pager);
+ mPagerAdapter = new ScreenSlidePagerAdapter(getFragmentManager());
+
+
+
+
+
+ mPagerAdapter.addTab(R.string.vpn_list_title, VPNProfileList.class);
+
+ mPagerAdapter.addTab(R.string.generalsettings, GeneralSettings.class);
+ mPagerAdapter.addTab(R.string.faq, FaqFragment.class);
+
+ if(SendDumpFragment.getLastestDump(this)!=null) {
+ mPagerAdapter.addTab(R.string.crashdump, SendDumpFragment.class);
+ }
+
+ mPagerAdapter.addTab(R.string.about, AboutFragment.class);
+ mPager.setAdapter(mPagerAdapter);
- vpnListTab.setTabListener(new TabListener<VPNProfileList>("profiles", VPNProfileList.class));
- generalTab.setTabListener(new TabListener<GeneralSettings>("settings", GeneralSettings.class));
- faqtab.setTabListener(new TabListener<FaqFragment>("faq", FaqFragment.class));
- abouttab.setTabListener(new TabListener<AboutFragment>("about", AboutFragment.class));
+ /*mSlidingTabLayout = (SlidingTabLayout) findViewById(R.id.slding_tabs);
+ mSlidingTabLayout.setViewPager(mPager); */
- bar.addTab(vpnListTab);
- bar.addTab(generalTab);
- bar.addTab(faqtab);
- bar.addTab(abouttab);
+ TabBarView tabs = (TabBarView) findViewById(R.id.sliding_tabs);
+ tabs.setViewPager(mPager);
+ /*
if (false) {
Tab logtab = bar.newTab().setText("Log");
logtab.setTabListener(new TabListener<LogFragment>("log", LogFragment.class));
bar.addTab(logtab);
- }
+ }*/
+
- if(SendDumpFragment.getLastestDump(this)!=null) {
- Tab sendDump = bar.newTab().setText(R.string.crashdump);
- sendDump.setTabListener(new TabListener<SendDumpFragment>("crashdump",SendDumpFragment.class));
- bar.addTab(sendDump);
- }
}
- protected class TabListener<T extends Fragment> implements ActionBar.TabListener
- {
- private Fragment mFragment;
- private String mTag;
- private Class<T> mClass;
-
- public TabListener(String tag, Class<T> clz) {
- mTag = tag;
- mClass = clz;
-
- // Check to see if we already have a fragment for this tab, probably
- // from a previously saved state. If so, deactivate it, because our
- // initial state is that a tab isn't shown.
- mFragment = getFragmentManager().findFragmentByTag(mTag);
- if (mFragment != null && !mFragment.isDetached()) {
- FragmentTransaction ft = getFragmentManager().beginTransaction();
- ft.detach(mFragment);
- ft.commit();
- }
+ class Tab {
+ public Class<? extends Fragment> fragmentClass;
+ String mName;
+
+ public Tab(Class<? extends Fragment> fClass, @StringRes String name){
+ mName = name;
+ fragmentClass = fClass;
}
-
- public void onTabSelected(Tab tab, FragmentTransaction ft) {
- if (mFragment == null) {
- mFragment = Fragment.instantiate(MainActivity.this, mClass.getName());
- ft.add(android.R.id.content, mFragment, mTag);
- } else {
- ft.attach(mFragment);
- }
+
+ }
+
+ private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
+ private Vector<Tab> mTabs = new Vector<Tab>();
+
+ public ScreenSlidePagerAdapter(FragmentManager fm) {
+ super(fm);
}
- public void onTabUnselected(Tab tab, FragmentTransaction ft) {
- if (mFragment != null) {
- ft.detach(mFragment);
+ @Override
+ public Fragment getItem(int position) {
+ try {
+ return mTabs.get(position).fragmentClass.newInstance();
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
}
+ return null;
}
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mTabs.get(position).mName;
+ }
- @Override
- public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
- }
- }
+ public void addTab(@StringRes int name, Class<? extends Fragment> fragmentClass) {
+ mTabs.add(new Tab(fragmentClass, getString(name)));
+ }
+ }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
diff --git a/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java b/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java
index d811e029..5bff4828 100644
--- a/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java
+++ b/main/src/main/java/de/blinkt/openvpn/activities/VPNPreferences.java
@@ -112,6 +112,7 @@ public class VPNPreferences extends PreferenceActivity {
setTitle(getString(R.string.edit_profile_title, mProfile.getName()));
}
super.onCreate(savedInstanceState);
+
}
diff --git a/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java b/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java
index d250ea01..ea114d7e 100644
--- a/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java
+++ b/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java
@@ -76,7 +76,6 @@ public class VPNLaunchHelper {
args.add("--config");
args.add(c.getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE);
-
return args.toArray(new String[args.size()]);
}
diff --git a/main/src/main/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.java b/main/src/main/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.java
index 451da07f..fe165353 100644
--- a/main/src/main/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.java
+++ b/main/src/main/java/de/blinkt/openvpn/fragments/Settings_Allowed_Apps.java
@@ -14,6 +14,7 @@ import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewParent;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
@@ -167,6 +168,12 @@ public class Settings_Allowed_Apps extends Fragment {
mListView.setAdapter(new PackageAdapter(getActivity(), mProfile));
+ v.setPadding(0,0,0,0);
+
+ /* 'orrible 'ack */
+
+
+
return v;
}
diff --git a/main/src/main/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java b/main/src/main/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java
new file mode 100644
index 00000000..ab8598c6
--- /dev/null
+++ b/main/src/main/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java
@@ -0,0 +1,732 @@
+/*
+ * Copyright (C) 2013 Andreas Stuetz <andreas.stuetz@gmail.com>
+ *
+ * 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 de.blinkt.openvpn.views;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
+import android.support.v4n.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Pair;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+
+import java.util.Locale;
+
+import de.blinkt.openvpn.R;
+
+public class PagerSlidingTabStrip extends HorizontalScrollView implements TabBarView {
+
+ private static final float OPAQUE = 1.0f;
+ private static final float HALF_TRANSP = 0.5f;
+
+ public interface CustomTabProvider {
+ public View getCustomTabView(ViewGroup parent, int position);
+ }
+
+ // @formatter:off
+ private static final int[] ATTRS = new int[]{
+ android.R.attr.textSize,
+ android.R.attr.textColor,
+ android.R.attr.paddingLeft,
+ android.R.attr.paddingRight,
+ };
+ // @formatter:on
+
+ private final PagerAdapterObserver adapterObserver = new PagerAdapterObserver();
+
+ //These indexes must be related with the ATTR array above
+ private static final int TEXT_SIZE_INDEX = 0;
+ private static final int TEXT_COLOR_INDEX = 1;
+ private static final int PADDING_LEFT_INDEX = 2;
+ private static final int PADDING_RIGHT_INDEX = 3;
+
+ private LinearLayout.LayoutParams defaultTabLayoutParams;
+ private LinearLayout.LayoutParams expandedTabLayoutParams;
+
+ private final PageListener pageListener = new PageListener();
+ public ViewPager.OnPageChangeListener delegatePageListener;
+
+ private LinearLayout tabsContainer;
+ private ViewPager pager;
+
+ private int tabCount;
+
+ private int currentPosition = 0;
+ private float currentPositionOffset = 0f;
+
+ private Paint rectPaint;
+ private Paint dividerPaint;
+
+ private int indicatorColor;
+ private int indicatorHeight = 2;
+
+ private int underlineHeight = 0;
+ private int underlineColor;
+
+ private int dividerWidth = 0;
+ private int dividerPadding = 0;
+ private int dividerColor;
+
+ private int tabPadding = 12;
+ private int tabTextSize = 14;
+ private ColorStateList tabTextColor = null;
+ private float tabTextAlpha = HALF_TRANSP;
+ private float tabTextSelectedAlpha = OPAQUE;
+
+ private int paddingLeft = 0;
+ private int paddingRight = 0;
+
+ private boolean shouldExpand = false;
+ private boolean textAllCaps = true;
+ private boolean isPaddingMiddle = false;
+
+ private Typeface tabTypeface = null;
+ private int tabTypefaceStyle = Typeface.BOLD;
+ private int tabTypefaceSelectedStyle = Typeface.BOLD;
+
+ private int scrollOffset;
+ private int lastScrollX = 0;
+
+ private int tabBackgroundResId = R.drawable.slidingtab_background;
+
+ private Locale locale;
+
+ public PagerSlidingTabStrip(Context context) {
+ this(context, null);
+ }
+
+ public PagerSlidingTabStrip(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PagerSlidingTabStrip(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setFillViewport(true);
+ setWillNotDraw(false);
+ tabsContainer = new LinearLayout(context);
+ tabsContainer.setOrientation(LinearLayout.HORIZONTAL);
+ tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ addView(tabsContainer);
+
+ //Default color will be 'textColorPrimary'
+ int colorPrimary = context.getResources().getColor(android.R.color.primary_text_dark);
+ setTextColor(colorPrimary);
+ underlineColor = colorPrimary;
+ dividerColor = colorPrimary;
+ indicatorColor = colorPrimary;
+
+
+ DisplayMetrics dm = getResources().getDisplayMetrics();
+ scrollOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, scrollOffset, dm);
+ indicatorHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, indicatorHeight, dm);
+ underlineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, underlineHeight, dm);
+ dividerPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerPadding, dm);
+ tabPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, tabPadding, dm);
+ dividerWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerWidth, dm);
+ tabTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, tabTextSize, dm);
+
+ // get system attrs (android:textSize and android:textColor)
+ TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+ tabTextSize = a.getDimensionPixelSize(TEXT_SIZE_INDEX, tabTextSize);
+ ColorStateList colorStateList = a.getColorStateList(TEXT_COLOR_INDEX);
+ if (colorStateList != null) {
+ tabTextColor = colorStateList;
+ }
+ paddingLeft = a.getDimensionPixelSize(PADDING_LEFT_INDEX, paddingLeft);
+ paddingRight = a.getDimensionPixelSize(PADDING_RIGHT_INDEX, paddingRight);
+ a.recycle();
+
+ //In case we have the padding they must be equal so we take the biggest
+ if (paddingRight < paddingLeft) {
+ paddingRight = paddingLeft;
+ }
+
+ if (paddingLeft < paddingRight) {
+ paddingLeft = paddingRight;
+ }
+
+ // get custom attrs
+ a = context.obtainStyledAttributes(attrs, R.styleable.PagerSlidingTabStrip);
+ indicatorColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsIndicatorColor, indicatorColor);
+ underlineColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsUnderlineColor, underlineColor);
+ dividerColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsDividerColor, dividerColor);
+ dividerWidth = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerWidth, dividerWidth);
+ indicatorHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsIndicatorHeight, indicatorHeight);
+ underlineHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsUnderlineHeight, underlineHeight);
+ dividerPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerPadding, dividerPadding);
+ tabPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsTabPaddingLeftRight, tabPadding);
+ tabBackgroundResId = a.getResourceId(R.styleable.PagerSlidingTabStrip_pstsTabBackground, tabBackgroundResId);
+ shouldExpand = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsShouldExpand, shouldExpand);
+ scrollOffset = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsScrollOffset, scrollOffset);
+ textAllCaps = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsTextAllCaps, textAllCaps);
+ isPaddingMiddle = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsPaddingMiddle, isPaddingMiddle);
+ tabTypefaceStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextStyle, Typeface.BOLD);
+ tabTypefaceSelectedStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextSelectedStyle, Typeface.BOLD);
+ tabTextAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextAlpha, HALF_TRANSP);
+ tabTextSelectedAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextSelectedAlpha, OPAQUE);
+ a.recycle();
+
+ rectPaint = new Paint();
+ rectPaint.setAntiAlias(true);
+ rectPaint.setStyle(Style.FILL);
+
+
+ dividerPaint = new Paint();
+ dividerPaint.setAntiAlias(true);
+ dividerPaint.setStrokeWidth(dividerWidth);
+
+ defaultTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+ expandedTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);
+
+ if (locale == null) {
+ locale = getResources().getConfiguration().locale;
+ }
+ }
+
+ public void setViewPager(ViewPager pager) {
+ this.pager = pager;
+ if (pager.getAdapter() == null) {
+ throw new IllegalStateException("ViewPager does not have adapter instance.");
+ }
+
+ pager.setOnPageChangeListener(pageListener);
+ pager.getAdapter().registerDataSetObserver(adapterObserver);
+ adapterObserver.setAttached(true);
+ notifyDataSetChanged();
+ }
+
+ public void notifyDataSetChanged() {
+ tabsContainer.removeAllViews();
+ tabCount = pager.getAdapter().getCount();
+ View tabView;
+ for (int i = 0; i < tabCount; i++) {
+
+ if (pager.getAdapter() instanceof CustomTabProvider) {
+ tabView = ((CustomTabProvider) pager.getAdapter()).getCustomTabView(this, i);
+ } else {
+ tabView = LayoutInflater.from(getContext()).inflate(R.layout.padersliding_tab, this, false);
+ }
+
+ CharSequence title = pager.getAdapter().getPageTitle(i);
+
+ addTab(i, title, tabView);
+ }
+
+ updateTabStyles();
+ getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ @Override
+ public void onGlobalLayout() {
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ } else {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+
+ currentPosition = pager.getCurrentItem();
+ currentPositionOffset = 0f;
+ scrollToChild(currentPosition, 0);
+ updateSelection(currentPosition);
+ }
+ });
+ }
+
+ private void addTab(final int position, CharSequence title, View tabView) {
+ TextView textView = (TextView) tabView.findViewById(R.id.tab_title);
+ if (textView != null) {
+ if (title != null) textView.setText(title);
+ float alpha = pager.getCurrentItem() == position ? tabTextSelectedAlpha : tabTextAlpha;
+ ViewCompat.setAlpha(textView, alpha);
+ }
+
+ tabView.setFocusable(true);
+ tabView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (pager.getCurrentItem() != position) {
+ View tab = tabsContainer.getChildAt(pager.getCurrentItem());
+ notSelected(tab);
+ pager.setCurrentItem(position);
+ }
+ }
+ });
+
+ tabView.setPadding(tabPadding, tabView.getPaddingTop(), tabPadding, tabView.getPaddingBottom());
+ tabsContainer.addView(tabView, position, shouldExpand ? expandedTabLayoutParams : defaultTabLayoutParams);
+ }
+
+ private void updateTabStyles() {
+ for (int i = 0; i < tabCount; i++) {
+ View v = tabsContainer.getChildAt(i);
+ v.setBackgroundResource(tabBackgroundResId);
+ TextView tab_title = (TextView) v.findViewById(R.id.tab_title);
+
+ if (tab_title != null) {
+ tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize);
+ tab_title.setTypeface(tabTypeface, pager.getCurrentItem() == i ? tabTypefaceSelectedStyle : tabTypefaceStyle);
+ if (tabTextColor != null) {
+ tab_title.setTextColor(tabTextColor);
+ }
+ // setAllCaps() is only available from API 14, so the upper case is made manually if we are on a
+ // pre-ICS-build
+ if (textAllCaps) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ tab_title.setAllCaps(true);
+ } else {
+ tab_title.setText(tab_title.getText().toString().toUpperCase(locale));
+ }
+ }
+ }
+ }
+
+ }
+
+ private void scrollToChild(int position, int offset) {
+ if (tabCount == 0) {
+ return;
+ }
+
+ int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset;
+ if (position > 0 || offset > 0) {
+
+ //Half screen offset.
+ //- Either tabs start at the middle of the view scrolling straight away
+ //- Or tabs start at the begging (no padding) scrolling when indicator gets
+ // to the middle of the view width
+ newScrollX -= scrollOffset;
+ Pair<Float, Float> lines = getIndicatorCoordinates();
+ newScrollX += ((lines.second - lines.first) / 2);
+ }
+
+ if (newScrollX != lastScrollX) {
+ lastScrollX = newScrollX;
+ scrollTo(newScrollX, 0);
+ }
+ }
+
+ private Pair<Float, Float> getIndicatorCoordinates() {
+ // default: line below current tab
+ View currentTab = tabsContainer.getChildAt(currentPosition);
+ float lineLeft = currentTab.getLeft();
+ float lineRight = currentTab.getRight();
+
+ // if there is an offset, start interpolating left and right coordinates between current and next tab
+ if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
+
+ View nextTab = tabsContainer.getChildAt(currentPosition + 1);
+ final float nextTabLeft = nextTab.getLeft();
+ final float nextTabRight = nextTab.getRight();
+
+ lineLeft = (currentPositionOffset * nextTabLeft + (1f - currentPositionOffset) * lineLeft);
+ lineRight = (currentPositionOffset * nextTabRight + (1f - currentPositionOffset) * lineRight);
+ }
+ return new Pair<Float, Float>(lineLeft, lineRight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (isInEditMode() || tabCount == 0) {
+ return;
+ }
+
+ final int height = getHeight();
+ // draw indicator line
+ rectPaint.setColor(indicatorColor);
+ Pair<Float, Float> lines = getIndicatorCoordinates();
+ canvas.drawRect(lines.first + paddingLeft, height - indicatorHeight, lines.second + paddingRight, height, rectPaint);
+ // draw underline
+ rectPaint.setColor(underlineColor);
+ canvas.drawRect(paddingLeft, height - underlineHeight, tabsContainer.getWidth() + paddingRight, height, rectPaint);
+ // draw divider
+ if (dividerWidth != 0) {
+ dividerPaint.setStrokeWidth(dividerWidth);
+ dividerPaint.setColor(dividerColor);
+ for (int i = 0; i < tabCount - 1; i++) {
+ View tab = tabsContainer.getChildAt(i);
+ canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (isPaddingMiddle) {
+ //Make sure tabContainer is bigger than the HorizontalScrollView to be able to scroll
+ tabsContainer.setMinimumWidth(getWidth());
+ int halfFirstTab = 0;
+ if (tabsContainer.getChildCount() > 0) {
+ halfFirstTab = (tabsContainer.getChildAt(0).getWidth() / 2);
+ }
+ //The user choose the tabs to start in the middle of the view width (padding)
+ paddingLeft = paddingRight = getWidth() / 2 - halfFirstTab;
+ //Clipping padding to false to see the tabs while we pass them swiping
+ setClipToPadding(false);
+ }
+
+ if (scrollOffset == 0) scrollOffset = getWidth() / 2 - paddingLeft;
+ setPadding(paddingLeft, getPaddingTop(), paddingRight, getPaddingBottom());
+ super.onLayout(changed, l, t, r, b);
+ }
+
+ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
+ this.delegatePageListener = listener;
+ }
+
+ private class PageListener implements ViewPager.OnPageChangeListener {
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ currentPosition = position;
+ currentPositionOffset = positionOffset;
+ int offset = tabCount > 0 ? (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()) : 0;
+ scrollToChild(position, offset);
+ invalidate();
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if (state == ViewPager.SCROLL_STATE_IDLE) {
+ scrollToChild(pager.getCurrentItem(), 0);
+ }
+ //Full alpha for current item
+ View currentTab = tabsContainer.getChildAt(pager.getCurrentItem());
+ selected(currentTab);
+ //Half transparent for prev item
+ if (pager.getCurrentItem() - 1 >= 0) {
+ View prevTab = tabsContainer.getChildAt(pager.getCurrentItem() - 1);
+ notSelected(prevTab);
+ }
+ //Half transparent for next item
+ if (pager.getCurrentItem() + 1 <= pager.getAdapter().getCount() - 1) {
+ View nextTab = tabsContainer.getChildAt(pager.getCurrentItem() + 1);
+ notSelected(nextTab);
+ }
+
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageScrollStateChanged(state);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ updateSelection(position);
+ if (delegatePageListener != null) {
+ delegatePageListener.onPageSelected(position);
+ }
+ }
+
+ }
+
+ private void updateSelection(int position) {
+ for (int i = 0; i < tabCount; ++i) {
+ View tv = tabsContainer.getChildAt(i);
+ tv.setSelected(i == position);
+ }
+ }
+
+ private void notSelected(View tab) {
+ TextView title = (TextView) tab.findViewById(R.id.tab_title);
+ if (title != null) {
+ title.setTypeface(tabTypeface, tabTypefaceStyle);
+ ViewCompat.setAlpha(title, tabTextAlpha);
+ }
+ }
+
+ private void selected(View tab) {
+ TextView title = (TextView) tab.findViewById(R.id.tab_title);
+ if (title != null) {
+ title.setTypeface(tabTypeface, tabTypefaceSelectedStyle);
+ ViewCompat.setAlpha(title, tabTextSelectedAlpha);
+ }
+ }
+
+ private class PagerAdapterObserver extends DataSetObserver {
+
+ private boolean attached = false;
+
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ public void setAttached(boolean attached) {
+ this.attached = attached;
+ }
+
+ public boolean isAttached() {
+ return attached;
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (pager != null) {
+ if (!adapterObserver.isAttached()) {
+ pager.getAdapter().registerDataSetObserver(adapterObserver);
+ adapterObserver.setAttached(true);
+ }
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (pager != null) {
+ if (adapterObserver.isAttached()) {
+ pager.getAdapter().unregisterDataSetObserver(adapterObserver);
+ adapterObserver.setAttached(false);
+ }
+ }
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ currentPosition = savedState.currentPosition;
+ if (currentPosition != 0 && tabsContainer.getChildCount() > 0) {
+ notSelected(tabsContainer.getChildAt(0));
+ selected(tabsContainer.getChildAt(currentPosition));
+ }
+ requestLayout();
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState savedState = new SavedState(superState);
+ savedState.currentPosition = currentPosition;
+ return savedState;
+ }
+
+ static class SavedState extends BaseSavedState {
+ int currentPosition;
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ currentPosition = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(currentPosition);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ public int getIndicatorColor() {
+ return this.indicatorColor;
+ }
+
+ public int getIndicatorHeight() {
+ return indicatorHeight;
+ }
+
+ public int getUnderlineColor() {
+ return underlineColor;
+ }
+
+ public int getDividerColor() {
+ return dividerColor;
+ }
+
+ public int getDividerWidth() {
+ return dividerWidth;
+ }
+
+ public int getUnderlineHeight() {
+ return underlineHeight;
+ }
+
+ public int getDividerPadding() {
+ return dividerPadding;
+ }
+
+ public int getScrollOffset() {
+ return scrollOffset;
+ }
+
+ public boolean getShouldExpand() {
+ return shouldExpand;
+ }
+
+ public int getTextSize() {
+ return tabTextSize;
+ }
+
+ public boolean isTextAllCaps() {
+ return textAllCaps;
+ }
+
+ public ColorStateList getTextColor() {
+ return tabTextColor;
+ }
+
+ public int getTabBackground() {
+ return tabBackgroundResId;
+ }
+
+ public int getTabPaddingLeftRight() {
+ return tabPadding;
+ }
+
+ public void setIndicatorColor(int indicatorColor) {
+ this.indicatorColor = indicatorColor;
+ invalidate();
+ }
+
+ public void setIndicatorColorResource(int resId) {
+ this.indicatorColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public void setIndicatorHeight(int indicatorLineHeightPx) {
+ this.indicatorHeight = indicatorLineHeightPx;
+ invalidate();
+ }
+
+ public void setUnderlineColor(int underlineColor) {
+ this.underlineColor = underlineColor;
+ invalidate();
+ }
+
+ public void setUnderlineColorResource(int resId) {
+ this.underlineColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public void setDividerColor(int dividerColor) {
+ this.dividerColor = dividerColor;
+ invalidate();
+ }
+
+ public void setDividerColorResource(int resId) {
+ this.dividerColor = getResources().getColor(resId);
+ invalidate();
+ }
+
+ public void setDividerWidth(int dividerWidthPx) {
+ this.dividerWidth = dividerWidthPx;
+ invalidate();
+ }
+
+ public void setUnderlineHeight(int underlineHeightPx) {
+ this.underlineHeight = underlineHeightPx;
+ invalidate();
+ }
+
+ public void setDividerPadding(int dividerPaddingPx) {
+ this.dividerPadding = dividerPaddingPx;
+ invalidate();
+ }
+
+ public void setScrollOffset(int scrollOffsetPx) {
+ this.scrollOffset = scrollOffsetPx;
+ invalidate();
+ }
+
+ public void setShouldExpand(boolean shouldExpand) {
+ this.shouldExpand = shouldExpand;
+ if (pager != null) {
+ requestLayout();
+ }
+ }
+
+ public void setAllCaps(boolean textAllCaps) {
+ this.textAllCaps = textAllCaps;
+ }
+
+ public void setTextSize(int textSizePx) {
+ this.tabTextSize = textSizePx;
+ updateTabStyles();
+ }
+
+ public void setTextColor(int textColor) {
+ setTextColor(new ColorStateList(new int[][]{new int[]{}}, new int[]{textColor}));
+ }
+
+ public void setTextColor(ColorStateList colorStateList) {
+ this.tabTextColor = colorStateList;
+ updateTabStyles();
+ }
+
+ public void setTextColorResource(int resId) {
+ setTextColor(getResources().getColor(resId));
+ }
+
+ public void setTextColorStateListResource(int resId) {
+ setTextColor(getResources().getColorStateList(resId));
+ }
+
+ public void setTypeface(Typeface typeface, int style) {
+ this.tabTypeface = typeface;
+ this.tabTypefaceSelectedStyle = style;
+ updateTabStyles();
+ }
+
+ public void setTabBackground(int resId) {
+ this.tabBackgroundResId = resId;
+ }
+
+ public void setTabPaddingLeftRight(int paddingPx) {
+ this.tabPadding = paddingPx;
+ updateTabStyles();
+ }
+} \ No newline at end of file
diff --git a/main/src/main/java/de/blinkt/openvpn/views/SlidingTabLayout.java b/main/src/main/java/de/blinkt/openvpn/views/SlidingTabLayout.java
new file mode 100644
index 00000000..ea3b1c26
--- /dev/null
+++ b/main/src/main/java/de/blinkt/openvpn/views/SlidingTabLayout.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2013 The Android Open Source 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 de.blinkt.openvpn.views;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.support.v4n.view.PagerAdapter;
+import android.support.v4n.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.HorizontalScrollView;
+import android.widget.TextView;
+
+/**
+ * To be used with ViewPager to provide a tab indicator component which give constant feedback as to
+ * the user's scroll progress.
+ * <p>
+ * To use the component, simply add it to your view hierarchy. Then in your
+ * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call
+ * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for.
+ * <p>
+ * The colors can be customized in two ways. The first and simplest is to provide an array of colors
+ * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The
+ * alternative is via the {@link TabColorizer} interface which provides you complete control over
+ * which color is used for any individual position.
+ * <p>
+ * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)},
+ * providing the layout ID of your custom layout.
+ */
+public class SlidingTabLayout extends HorizontalScrollView implements TabBarView {
+
+ /**
+ * Allows complete control over the colors drawn in the tab layout. Set with
+ * {@link #setCustomTabColorizer(TabColorizer)}.
+ */
+ public interface TabColorizer {
+
+ /**
+ * @return return the color of the indicator used when {@code position} is selected.
+ */
+ int getIndicatorColor(int position);
+
+ /**
+ * @return return the color of the divider drawn to the right of {@code position}.
+ */
+ int getDividerColor(int position);
+
+ }
+
+ private static final int TITLE_OFFSET_DIPS = 24;
+ private static final int TAB_VIEW_PADDING_DIPS = 16;
+ private static final int TAB_VIEW_TEXT_SIZE_SP = 12;
+
+ private int mTitleOffset;
+
+ private int mTabViewLayoutId;
+ private int mTabViewTextViewId;
+
+ private ViewPager mViewPager;
+ private ViewPager.OnPageChangeListener mViewPagerPageChangeListener;
+
+ private final SlidingTabStrip mTabStrip;
+
+ public SlidingTabLayout(Context context) {
+ this(context, null);
+ }
+
+ public SlidingTabLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ // Disable the Scroll Bar
+ setHorizontalScrollBarEnabled(false);
+ // Make sure that the Tab Strips fills this View
+ setFillViewport(true);
+
+ mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density);
+
+ mTabStrip = new SlidingTabStrip(context);
+ addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ /**
+ * Set the custom {@link TabColorizer} to be used.
+ *
+ * If you only require simple custmisation then you can use
+ * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve
+ * similar effects.
+ */
+ public void setCustomTabColorizer(TabColorizer tabColorizer) {
+ mTabStrip.setCustomTabColorizer(tabColorizer);
+ }
+
+ /**
+ * Sets the colors to be used for indicating the selected tab. These colors are treated as a
+ * circular array. Providing one color will mean that all tabs are indicated with the same color.
+ */
+ public void setSelectedIndicatorColors(int... colors) {
+ mTabStrip.setSelectedIndicatorColors(colors);
+ }
+
+ /**
+ * Sets the colors to be used for tab dividers. These colors are treated as a circular array.
+ * Providing one color will mean that all tabs are indicated with the same color.
+ */
+ public void setDividerColors(int... colors) {
+ mTabStrip.setDividerColors(colors);
+ }
+
+ /**
+ * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are
+ * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so
+ * that the layout can update it's scroll position correctly.
+ *
+ * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener)
+ */
+ public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
+ mViewPagerPageChangeListener = listener;
+ }
+
+ /**
+ * Set the custom layout to be inflated for the tab views.
+ *
+ * @param layoutResId Layout id to be inflated
+ * @param textViewId id of the {@link TextView} in the inflated view
+ */
+ public void setCustomTabView(int layoutResId, int textViewId) {
+ mTabViewLayoutId = layoutResId;
+ mTabViewTextViewId = textViewId;
+ }
+
+ /**
+ * Sets the associated view pager. Note that the assumption here is that the pager content
+ * (number of tabs and tab titles) does not change after this call has been made.
+ */
+ public void setViewPager(ViewPager viewPager) {
+ mTabStrip.removeAllViews();
+
+ mViewPager = viewPager;
+ if (viewPager != null) {
+ viewPager.setOnPageChangeListener(new InternalViewPagerListener());
+ populateTabStrip();
+ }
+ }
+
+ /**
+ * Create a default view to be used for tabs. This is called if a custom tab view is not set via
+ * {@link #setCustomTabView(int, int)}.
+ */
+ protected TextView createDefaultTabView(Context context) {
+ TextView textView = new TextView(context);
+ textView.setGravity(Gravity.CENTER);
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP);
+ textView.setTypeface(Typeface.DEFAULT_BOLD);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ // If we're running on Honeycomb or newer, then we can use the Theme's
+ // selectableItemBackground to ensure that the View has a pressed state
+ TypedValue outValue = new TypedValue();
+ getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
+ outValue, true);
+ textView.setBackgroundResource(outValue.resourceId);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style
+ textView.setAllCaps(true);
+ }
+
+ int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density);
+ textView.setPadding(padding, padding, padding, padding);
+
+ return textView;
+ }
+
+ private void populateTabStrip() {
+ final PagerAdapter adapter = mViewPager.getAdapter();
+ final View.OnClickListener tabClickListener = new TabClickListener();
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ View tabView = null;
+ TextView tabTitleView = null;
+
+ if (mTabViewLayoutId != 0) {
+ // If there is a custom tab view layout id set, try and inflate it
+ tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip,
+ false);
+ tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId);
+ }
+
+ if (tabView == null) {
+ tabView = createDefaultTabView(getContext());
+ }
+
+ if (tabTitleView == null && TextView.class.isInstance(tabView)) {
+ tabTitleView = (TextView) tabView;
+ }
+
+ tabTitleView.setText(adapter.getPageTitle(i));
+ tabView.setOnClickListener(tabClickListener);
+
+ mTabStrip.addView(tabView);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mViewPager != null) {
+ scrollToTab(mViewPager.getCurrentItem(), 0);
+ }
+ }
+
+ private void scrollToTab(int tabIndex, int positionOffset) {
+ final int tabStripChildCount = mTabStrip.getChildCount();
+ if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {
+ return;
+ }
+
+ View selectedChild = mTabStrip.getChildAt(tabIndex);
+ if (selectedChild != null) {
+ int targetScrollX = selectedChild.getLeft() + positionOffset;
+
+ if (tabIndex > 0 || positionOffset > 0) {
+ // If we're not at the first child and are mid-scroll, make sure we obey the offset
+ targetScrollX -= mTitleOffset;
+ }
+
+ scrollTo(targetScrollX, 0);
+ }
+ }
+
+ private class InternalViewPagerListener implements ViewPager.OnPageChangeListener {
+ private int mScrollState;
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ int tabStripChildCount = mTabStrip.getChildCount();
+ if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+ return;
+ }
+
+ mTabStrip.onViewPagerPageChanged(position, positionOffset);
+
+ View selectedTitle = mTabStrip.getChildAt(position);
+ int extraOffset = (selectedTitle != null)
+ ? (int) (positionOffset * selectedTitle.getWidth())
+ : 0;
+ scrollToTab(position, extraOffset);
+
+ if (mViewPagerPageChangeListener != null) {
+ mViewPagerPageChangeListener.onPageScrolled(position, positionOffset,
+ positionOffsetPixels);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ mScrollState = state;
+
+ if (mViewPagerPageChangeListener != null) {
+ mViewPagerPageChangeListener.onPageScrollStateChanged(state);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
+ mTabStrip.onViewPagerPageChanged(position, 0f);
+ scrollToTab(position, 0);
+ }
+
+ if (mViewPagerPageChangeListener != null) {
+ mViewPagerPageChangeListener.onPageSelected(position);
+ }
+ }
+
+ }
+
+ private class TabClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ for (int i = 0; i < mTabStrip.getChildCount(); i++) {
+ if (v == mTabStrip.getChildAt(i)) {
+ mViewPager.setCurrentItem(i);
+ return;
+ }
+ }
+ }
+ }
+
+}
diff --git a/main/src/main/java/de/blinkt/openvpn/views/SlidingTabStrip.java b/main/src/main/java/de/blinkt/openvpn/views/SlidingTabStrip.java
new file mode 100644
index 00000000..b11c356b
--- /dev/null
+++ b/main/src/main/java/de/blinkt/openvpn/views/SlidingTabStrip.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2013 The Android Open Source 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 de.blinkt.openvpn.views;
+
+import android.R;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.LinearLayout;
+
+class SlidingTabStrip extends LinearLayout {
+
+ private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2;
+ private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26;
+ private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8;
+ private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5;
+
+ private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1;
+ private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20;
+ private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f;
+
+ private final int mBottomBorderThickness;
+ private final Paint mBottomBorderPaint;
+
+ private final int mSelectedIndicatorThickness;
+ private final Paint mSelectedIndicatorPaint;
+
+ private final int mDefaultBottomBorderColor;
+
+ private final Paint mDividerPaint;
+ private final float mDividerHeight;
+
+ private int mSelectedPosition;
+ private float mSelectionOffset;
+
+ private SlidingTabLayout.TabColorizer mCustomTabColorizer;
+ private final SimpleTabColorizer mDefaultTabColorizer;
+
+ SlidingTabStrip(Context context) {
+ this(context, null);
+ }
+
+ SlidingTabStrip(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setWillNotDraw(false);
+
+ final float density = getResources().getDisplayMetrics().density;
+
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true);
+ final int themeForegroundColor = outValue.data;
+
+ mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor,
+ DEFAULT_BOTTOM_BORDER_COLOR_ALPHA);
+
+ mDefaultTabColorizer = new SimpleTabColorizer();
+ mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR);
+ mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor,
+ DEFAULT_DIVIDER_COLOR_ALPHA));
+
+ mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density);
+ mBottomBorderPaint = new Paint();
+ mBottomBorderPaint.setColor(mDefaultBottomBorderColor);
+
+ mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density);
+ mSelectedIndicatorPaint = new Paint();
+
+ mDividerHeight = DEFAULT_DIVIDER_HEIGHT;
+ mDividerPaint = new Paint();
+ mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density));
+ }
+
+ void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) {
+ mCustomTabColorizer = customTabColorizer;
+ invalidate();
+ }
+
+ void setSelectedIndicatorColors(int... colors) {
+ // Make sure that the custom colorizer is removed
+ mCustomTabColorizer = null;
+ mDefaultTabColorizer.setIndicatorColors(colors);
+ invalidate();
+ }
+
+ void setDividerColors(int... colors) {
+ // Make sure that the custom colorizer is removed
+ mCustomTabColorizer = null;
+ mDefaultTabColorizer.setDividerColors(colors);
+ invalidate();
+ }
+
+ void onViewPagerPageChanged(int position, float positionOffset) {
+ mSelectedPosition = position;
+ mSelectionOffset = positionOffset;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int height = getHeight();
+ final int childCount = getChildCount();
+ final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height);
+ final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null
+ ? mCustomTabColorizer
+ : mDefaultTabColorizer;
+
+ // Thick colored underline below the current selection
+ if (childCount > 0) {
+ View selectedTitle = getChildAt(mSelectedPosition);
+ int left = selectedTitle.getLeft();
+ int right = selectedTitle.getRight();
+ int color = tabColorizer.getIndicatorColor(mSelectedPosition);
+
+ if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) {
+ int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1);
+ if (color != nextColor) {
+ color = blendColors(nextColor, color, mSelectionOffset);
+ }
+
+ // Draw the selection partway between the tabs
+ View nextTitle = getChildAt(mSelectedPosition + 1);
+ left = (int) (mSelectionOffset * nextTitle.getLeft() +
+ (1.0f - mSelectionOffset) * left);
+ right = (int) (mSelectionOffset * nextTitle.getRight() +
+ (1.0f - mSelectionOffset) * right);
+ }
+
+ mSelectedIndicatorPaint.setColor(color);
+
+ canvas.drawRect(left, height - mSelectedIndicatorThickness, right,
+ height, mSelectedIndicatorPaint);
+ }
+
+ // Thin underline along the entire bottom edge
+ canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint);
+
+ // Vertical separators between the titles
+ int separatorTop = (height - dividerHeightPx) / 2;
+ for (int i = 0; i < childCount - 1; i++) {
+ View child = getChildAt(i);
+ mDividerPaint.setColor(tabColorizer.getDividerColor(i));
+ canvas.drawLine(child.getRight(), separatorTop, child.getRight(),
+ separatorTop + dividerHeightPx, mDividerPaint);
+ }
+ }
+
+ /**
+ * Set the alpha value of the {@code color} to be the given {@code alpha} value.
+ */
+ private static int setColorAlpha(int color, byte alpha) {
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
+ }
+
+ /**
+ * Blend {@code color1} and {@code color2} using the given ratio.
+ *
+ * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend,
+ * 0.0 will return {@code color2}.
+ */
+ private static int blendColors(int color1, int color2, float ratio) {
+ final float inverseRation = 1f - ratio;
+ float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);
+ float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);
+ float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);
+ return Color.rgb((int) r, (int) g, (int) b);
+ }
+
+ private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer {
+ private int[] mIndicatorColors;
+ private int[] mDividerColors;
+
+ @Override
+ public final int getIndicatorColor(int position) {
+ return mIndicatorColors[position % mIndicatorColors.length];
+ }
+
+ @Override
+ public final int getDividerColor(int position) {
+ return mDividerColors[position % mDividerColors.length];
+ }
+
+ void setIndicatorColors(int... colors) {
+ mIndicatorColors = colors;
+ }
+
+ void setDividerColors(int... colors) {
+ mDividerColors = colors;
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/main/java/de/blinkt/openvpn/views/TabBarView.java b/main/src/main/java/de/blinkt/openvpn/views/TabBarView.java
new file mode 100644
index 00000000..105f5d6b
--- /dev/null
+++ b/main/src/main/java/de/blinkt/openvpn/views/TabBarView.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2012-2014 Arne Schwabe
+ * Distributed under the GNU GPL v2. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.views;
+
+import android.support.v4n.view.ViewPager;
+
+/**
+ * Created by arne on 18.11.14.
+ */
+public interface TabBarView {
+
+ void setViewPager(ViewPager mPager);
+}
diff --git a/main/src/main/res/drawable/bg_tabs.xml b/main/src/main/res/drawable/bg_tabs.xml
new file mode 100644
index 00000000..129a637d
--- /dev/null
+++ b/main/src/main/res/drawable/bg_tabs.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (c) 2012-2014 Arne Schwabe
+ ~ Distributed under the GNU GPL v2. For full terms see the file doc/LICENSE.txt
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/primary" />
+</shape> \ No newline at end of file
diff --git a/main/src/main/res/drawable/slidingtab_background.xml b/main/src/main/res/drawable/slidingtab_background.xml
new file mode 100644
index 00000000..885cf036
--- /dev/null
+++ b/main/src/main/res/drawable/slidingtab_background.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android" android:exitFadeDuration="@android:integer/config_shortAnimTime">
+
+ <item android:state_pressed="true" android:drawable="@color/background_tab_pressed" />
+ <item android:state_focused="true" android:drawable="@color/background_tab_pressed"/>
+ <item android:drawable="@android:color/transparent"/>
+
+</selector> \ No newline at end of file
diff --git a/main/src/main/res/layout-v21/tabs.xml b/main/src/main/res/layout-v21/tabs.xml
new file mode 100644
index 00000000..095f863e
--- /dev/null
+++ b/main/src/main/res/layout-v21/tabs.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (c) 2012-2014 Arne Schwabe
+ ~ Distributed under the GNU GPL v2. For full terms see the file doc/LICENSE.txt
+ -->
+<merge>
+
+ <de.blinkt.openvpn.views.PagerSlidingTabStrip xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/sliding_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"
+ android:background="@drawable/bg_tabs"
+ android:elevation="4dp" />
+</merge> \ No newline at end of file
diff --git a/main/src/main/res/layout/allowed_vpn_apps.xml b/main/src/main/res/layout/allowed_vpn_apps.xml
index c4369885..b3d074e8 100644
--- a/main/src/main/res/layout/allowed_vpn_apps.xml
+++ b/main/src/main/res/layout/allowed_vpn_apps.xml
@@ -7,6 +7,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/rot"
android:orientation="vertical"
+ android:layout_marginLeft="-20dp"
+ android:layout_marginRight="-20dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
diff --git a/main/src/main/res/layout/main_activity.xml b/main/src/main/res/layout/main_activity.xml
new file mode 100644
index 00000000..0b0aa8fe
--- /dev/null
+++ b/main/src/main/res/layout/main_activity.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (c) 2012-2014 Arne Schwabe
+ ~ Distributed under the GNU GPL v2. For full terms see the file doc/LICENSE.txt
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include layout="@layout/tabs" />
+
+ <android.support.v4n.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout>
+
diff --git a/main/src/main/res/layout/padersliding_tab.xml b/main/src/main/res/layout/padersliding_tab.xml
new file mode 100644
index 00000000..80b104b5
--- /dev/null
+++ b/main/src/main/res/layout/padersliding_tab.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tab_title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:singleLine="true" /> \ No newline at end of file
diff --git a/main/src/main/res/layout/tabs.xml b/main/src/main/res/layout/tabs.xml
new file mode 100644
index 00000000..56a24ebe
--- /dev/null
+++ b/main/src/main/res/layout/tabs.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (c) 2012-2014 Arne Schwabe
+ ~ Distributed under the GNU GPL v2. For full terms see the file doc/LICENSE.txt
+ -->
+<merge>
+
+ <de.blinkt.openvpn.views.SlidingTabLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/sliding_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</merge> \ No newline at end of file
diff --git a/main/src/main/res/values-v21/styles.xml b/main/src/main/res/values-v21/styles.xml
index 754dbc9a..4379dd6d 100644
--- a/main/src/main/res/values-v21/styles.xml
+++ b/main/src/main/res/values-v21/styles.xml
@@ -5,12 +5,13 @@
-->
<resources>
+ <style name="blinkt.baseTheme" parent="android:Theme.Material.Light.DarkActionBar" />
+
<!-- http://www.google.de/design/spec/style/color.html#color-color-palette -->
- <style name="appstyle" parent="android:Theme.Material.Light.DarkActionBar">
+ <style name="blinkt" parent="blinkt.common">
<item name="android:colorPrimary">@color/primary</item>
<item name="android:colorPrimaryDark">@color/primary_dark</item>
<item name="android:colorAccent">@color/accent</item>
- <item name="android:preferenceStyle">@style/BlinktPreferencePanel</item>
</style>
</resources>
diff --git a/main/src/main/res/values/attrs.xml b/main/src/main/res/values/attrs.xml
index 9bc2317c..c2e12f3c 100644
--- a/main/src/main/res/values/attrs.xml
+++ b/main/src/main/res/values/attrs.xml
@@ -6,10 +6,39 @@
-->
<resources>
- <declare-styleable name="FileSelectLayout">
- <attr name="title" format="string|reference" />
- <attr name="certificate" format="boolean" />
-<!-- <attr name="taskid" format="integer" /> -->
- <attr name="showClear" format="boolean" />
- </declare-styleable>
+ <declare-styleable name="FileSelectLayout">
+ <attr name="title" format="string|reference" />
+ <attr name="certificate" format="boolean" />
+ <!-- <attr name="taskid" format="integer" /> -->
+ <attr name="showClear" format="boolean" />
+ </declare-styleable>
+
+ <declare-styleable name="PagerSlidingTabStrip">
+ <attr name="pstsIndicatorColor" format="color" />
+ <attr name="pstsUnderlineColor" format="color" />
+ <attr name="pstsDividerColor" format="color" />
+ <attr name="pstsDividerWidth" format="dimension" />
+ <attr name="pstsIndicatorHeight" format="dimension" />
+ <attr name="pstsUnderlineHeight" format="dimension" />
+ <attr name="pstsDividerPadding" format="dimension" />
+ <attr name="pstsTabPaddingLeftRight" format="dimension" />
+ <attr name="pstsScrollOffset" format="dimension" />
+ <attr name="pstsTabBackground" format="reference" />
+ <attr name="pstsShouldExpand" format="boolean" />
+ <attr name="pstsTextAllCaps" format="boolean" />
+ <attr name="pstsPaddingMiddle" format="boolean" />
+ <attr name="pstsTextStyle">
+ <flag name="normal" value="0x0" />
+ <flag name="bold" value="0x1" />
+ <flag name="italic" value="0x2" />
+ </attr>
+ <attr name="pstsTextSelectedStyle">
+ <flag name="normal" value="0x0" />
+ <flag name="bold" value="0x1" />
+ <flag name="italic" value="0x2" />
+ </attr>
+ <attr name="pstsTextAlpha" format="float" />
+ <attr name="pstsTextSelectedAlpha" format="float" />
+ </declare-styleable>
+
</resources>
diff --git a/main/src/main/res/values/colours.xml b/main/src/main/res/values/colours.xml
index 7e67dacd..3d8ce000 100644
--- a/main/src/main/res/values/colours.xml
+++ b/main/src/main/res/values/colours.xml
@@ -14,4 +14,7 @@
<color name="gelb">#ffff00</color>
<color name="rot">#ff0000</color>
+
+ <color name="background_tab_pressed">#1AFFFFFF</color>
+
</resources> \ No newline at end of file
diff --git a/main/src/main/res/values/styles.xml b/main/src/main/res/values/styles.xml
index cb503aed..94970c88 100644
--- a/main/src/main/res/values/styles.xml
+++ b/main/src/main/res/values/styles.xml
@@ -5,10 +5,15 @@
-->
<resources>
- <style name="appstyle" parent="android:Theme.DeviceDefault.Light">
+ <style name="blinkt.baseTheme" parent="android:Theme.DeviceDefault.Light" />
+ <style name="blinkt.common" parent="blinkt.baseTheme" >
+ <!-- Shared between Holo and Material -->
<item name="android:preferenceStyle">@style/BlinktPreferencePanel</item>
+
</style>
+ <style name="blinkt" parent="blinkt.common">
+ </style>
<!-- No margins or background by default. Not different for x-large screens -->
<style name="BlinktPreferencePanel">
diff --git a/remoteExample/build.gradle b/remoteExample/build.gradle
index bc9aeb7c..397ad5ce 100644
--- a/remoteExample/build.gradle
+++ b/remoteExample/build.gradle
@@ -1,12 +1,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 19
+ compileSdkVersion 21
buildToolsVersion "19.1.0"
defaultConfig {
minSdkVersion 15
- targetSdkVersion 19
+ targetSdkVersion 21
versionCode 1
versionName "1.0"
}
diff --git a/vpndialogxposed/build.gradle b/vpndialogxposed/build.gradle
index 77dacc7f..b8cd5678 100644
--- a/vpndialogxposed/build.gradle
+++ b/vpndialogxposed/build.gradle
@@ -14,12 +14,12 @@ dependencies {
android {
- compileSdkVersion 19
+ compileSdkVersion 21
buildToolsVersion "19.1.0"
defaultConfig {
minSdkVersion 14
- targetSdkVersion 19
+ targetSdkVersion 21
versionCode = 3
versionName = "0.3"