diff options
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"  | 
