diff options
34 files changed, 3325 insertions, 0 deletions
diff --git a/res/color/vpi__dark_theme.xml b/res/color/vpi__dark_theme.xml new file mode 100644 index 0000000..3e7a08f --- /dev/null +++ b/res/color/vpi__dark_theme.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" android:color="@color/vpi__bright_foreground_disabled_holo_dark"/> + <item android:state_window_focused="false" android:color="@color/vpi__bright_foreground_holo_dark"/> + <item android:state_pressed="true" android:color="@color/vpi__bright_foreground_holo_dark"/> + <item android:state_selected="true" android:color="@color/vpi__bright_foreground_holo_dark"/> + <!--item android:state_activated="true" android:color="@color/vpi__bright_foreground_holo_dark"/--> + <item android:color="@color/vpi__bright_foreground_holo_dark"/> <!-- not selected --> +</selector> diff --git a/res/color/vpi__light_theme.xml b/res/color/vpi__light_theme.xml new file mode 100644 index 0000000..f955db7 --- /dev/null +++ b/res/color/vpi__light_theme.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" android:color="@color/vpi__bright_foreground_disabled_holo_light"/> + <item android:state_window_focused="false" android:color="@color/vpi__bright_foreground_holo_light"/> + <item android:state_pressed="true" android:color="@color/vpi__bright_foreground_holo_light"/> + <item android:state_selected="true" android:color="@color/vpi__bright_foreground_holo_light"/> + <!--item android:state_activated="true" android:color="@color/vpi__bright_foreground_holo_light"/--> + <item android:color="@color/vpi__bright_foreground_holo_light"/> <!-- not selected --> + +</selector> + diff --git a/res/drawable-hdpi/vpi__tab_selected_focused_holo.9.png b/res/drawable-hdpi/vpi__tab_selected_focused_holo.9.png Binary files differnew file mode 100644 index 0000000..673e3bf --- /dev/null +++ b/res/drawable-hdpi/vpi__tab_selected_focused_holo.9.png diff --git a/res/drawable-hdpi/vpi__tab_selected_holo.9.png b/res/drawable-hdpi/vpi__tab_selected_holo.9.png Binary files differnew file mode 100644 index 0000000..d57df98 --- /dev/null +++ b/res/drawable-hdpi/vpi__tab_selected_holo.9.png diff --git a/res/drawable-hdpi/vpi__tab_selected_pressed_holo.9.png b/res/drawable-hdpi/vpi__tab_selected_pressed_holo.9.png Binary files differnew file mode 100644 index 0000000..6278eef --- /dev/null +++ b/res/drawable-hdpi/vpi__tab_selected_pressed_holo.9.png diff --git a/res/drawable-hdpi/vpi__tab_unselected_focused_holo.9.png b/res/drawable-hdpi/vpi__tab_unselected_focused_holo.9.png Binary files differnew file mode 100644 index 0000000..294991d --- /dev/null +++ b/res/drawable-hdpi/vpi__tab_unselected_focused_holo.9.png diff --git a/res/drawable-hdpi/vpi__tab_unselected_holo.9.png b/res/drawable-hdpi/vpi__tab_unselected_holo.9.png Binary files differnew file mode 100644 index 0000000..19532ab --- /dev/null +++ b/res/drawable-hdpi/vpi__tab_unselected_holo.9.png diff --git a/res/drawable-hdpi/vpi__tab_unselected_pressed_holo.9.png b/res/drawable-hdpi/vpi__tab_unselected_pressed_holo.9.png Binary files differnew file mode 100644 index 0000000..aadc6f8 --- /dev/null +++ b/res/drawable-hdpi/vpi__tab_unselected_pressed_holo.9.png diff --git a/res/drawable-mdpi/vpi__tab_selected_focused_holo.9.png b/res/drawable-mdpi/vpi__tab_selected_focused_holo.9.png Binary files differnew file mode 100644 index 0000000..c9972e7 --- /dev/null +++ b/res/drawable-mdpi/vpi__tab_selected_focused_holo.9.png diff --git a/res/drawable-mdpi/vpi__tab_selected_holo.9.png b/res/drawable-mdpi/vpi__tab_selected_holo.9.png Binary files differnew file mode 100644 index 0000000..587337c --- /dev/null +++ b/res/drawable-mdpi/vpi__tab_selected_holo.9.png diff --git a/res/drawable-mdpi/vpi__tab_selected_pressed_holo.9.png b/res/drawable-mdpi/vpi__tab_selected_pressed_holo.9.png Binary files differnew file mode 100644 index 0000000..155c4fc --- /dev/null +++ b/res/drawable-mdpi/vpi__tab_selected_pressed_holo.9.png diff --git a/res/drawable-mdpi/vpi__tab_unselected_focused_holo.9.png b/res/drawable-mdpi/vpi__tab_unselected_focused_holo.9.png Binary files differnew file mode 100644 index 0000000..f0cecd1 --- /dev/null +++ b/res/drawable-mdpi/vpi__tab_unselected_focused_holo.9.png diff --git a/res/drawable-mdpi/vpi__tab_unselected_holo.9.png b/res/drawable-mdpi/vpi__tab_unselected_holo.9.png Binary files differnew file mode 100644 index 0000000..a2dbf42 --- /dev/null +++ b/res/drawable-mdpi/vpi__tab_unselected_holo.9.png diff --git a/res/drawable-mdpi/vpi__tab_unselected_pressed_holo.9.png b/res/drawable-mdpi/vpi__tab_unselected_pressed_holo.9.png Binary files differnew file mode 100644 index 0000000..b1223fe --- /dev/null +++ b/res/drawable-mdpi/vpi__tab_unselected_pressed_holo.9.png diff --git a/res/drawable-xhdpi/vpi__tab_selected_focused_holo.9.png b/res/drawable-xhdpi/vpi__tab_selected_focused_holo.9.png Binary files differnew file mode 100644 index 0000000..03cfb09 --- /dev/null +++ b/res/drawable-xhdpi/vpi__tab_selected_focused_holo.9.png diff --git a/res/drawable-xhdpi/vpi__tab_selected_holo.9.png b/res/drawable-xhdpi/vpi__tab_selected_holo.9.png Binary files differnew file mode 100644 index 0000000..e4229f2 --- /dev/null +++ b/res/drawable-xhdpi/vpi__tab_selected_holo.9.png diff --git a/res/drawable-xhdpi/vpi__tab_selected_pressed_holo.9.png b/res/drawable-xhdpi/vpi__tab_selected_pressed_holo.9.png Binary files differnew file mode 100644 index 0000000..e862cb1 --- /dev/null +++ b/res/drawable-xhdpi/vpi__tab_selected_pressed_holo.9.png diff --git a/res/drawable-xhdpi/vpi__tab_unselected_focused_holo.9.png b/res/drawable-xhdpi/vpi__tab_unselected_focused_holo.9.png Binary files differnew file mode 100644 index 0000000..f3a5cbd --- /dev/null +++ b/res/drawable-xhdpi/vpi__tab_unselected_focused_holo.9.png diff --git a/res/drawable-xhdpi/vpi__tab_unselected_holo.9.png b/res/drawable-xhdpi/vpi__tab_unselected_holo.9.png Binary files differnew file mode 100644 index 0000000..9465173 --- /dev/null +++ b/res/drawable-xhdpi/vpi__tab_unselected_holo.9.png diff --git a/res/drawable-xhdpi/vpi__tab_unselected_pressed_holo.9.png b/res/drawable-xhdpi/vpi__tab_unselected_pressed_holo.9.png Binary files differnew file mode 100644 index 0000000..f1eb673 --- /dev/null +++ b/res/drawable-xhdpi/vpi__tab_unselected_pressed_holo.9.png diff --git a/res/drawable/vpi__tab_indicator.xml b/res/drawable/vpi__tab_indicator.xml new file mode 100644 index 0000000..520d08c --- /dev/null +++ b/res/drawable/vpi__tab_indicator.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2008 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Non focused states --> + <item android:state_focused="false" android:state_selected="false" android:state_pressed="false" android:drawable="@drawable/vpi__tab_unselected_holo" /> + <item android:state_focused="false" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/vpi__tab_selected_holo" /> + + <!-- Focused states --> + <item android:state_focused="true" android:state_selected="false" android:state_pressed="false" android:drawable="@drawable/vpi__tab_unselected_focused_holo" /> + <item android:state_focused="true" android:state_selected="true" android:state_pressed="false" android:drawable="@drawable/vpi__tab_selected_focused_holo" /> + + <!-- Pressed --> + <!-- Non focused states --> + <item android:state_focused="false" android:state_selected="false" android:state_pressed="true" android:drawable="@drawable/vpi__tab_unselected_pressed_holo" /> + <item android:state_focused="false" android:state_selected="true" android:state_pressed="true" android:drawable="@drawable/vpi__tab_selected_pressed_holo" /> + + <!-- Focused states --> + <item android:state_focused="true" android:state_selected="false" android:state_pressed="true" android:drawable="@drawable/vpi__tab_unselected_pressed_holo" /> + <item android:state_focused="true" android:state_selected="true" android:state_pressed="true" android:drawable="@drawable/vpi__tab_selected_pressed_holo" /> +</selector> diff --git a/res/values/vpi__attrs.xml b/res/values/vpi__attrs.xml new file mode 100644 index 0000000..a4d1f10 --- /dev/null +++ b/res/values/vpi__attrs.xml @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2012 Jake Wharton + Copyright (C) 2011 Patrik Ã…kerfeldt + + 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. +--> + +<resources> + <declare-styleable name="ViewPagerIndicator"> + <!-- Style of the circle indicator. --> + <attr name="vpiCirclePageIndicatorStyle" format="reference"/> + <!-- Style of the icon indicator's views. --> + <attr name="vpiIconPageIndicatorStyle" format="reference"/> + <!-- Style of the line indicator. --> + <attr name="vpiLinePageIndicatorStyle" format="reference"/> + <!-- Style of the title indicator. --> + <attr name="vpiTitlePageIndicatorStyle" format="reference"/> + <!-- Style of the tab indicator's tabs. --> + <attr name="vpiTabPageIndicatorStyle" format="reference"/> + <!-- Style of the underline indicator. --> + <attr name="vpiUnderlinePageIndicatorStyle" format="reference"/> + </declare-styleable> + + <attr name="centered" format="boolean" /> + <attr name="selectedColor" format="color" /> + <attr name="strokeWidth" format="dimension" /> + <attr name="unselectedColor" format="color" /> + + <declare-styleable name="CirclePageIndicator"> + <!-- Whether or not the indicators should be centered. --> + <attr name="centered" /> + <!-- Color of the filled circle that represents the current page. --> + <attr name="fillColor" format="color" /> + <!-- Color of the filled circles that represents pages. --> + <attr name="pageColor" format="color" /> + <!-- Orientation of the indicator. --> + <attr name="android:orientation"/> + <!-- Radius of the circles. This is also the spacing between circles. --> + <attr name="radius" format="dimension" /> + <!-- Whether or not the selected indicator snaps to the circles. --> + <attr name="snap" format="boolean" /> + <!-- Color of the open circles. --> + <attr name="strokeColor" format="color" /> + <!-- Width of the stroke used to draw the circles. --> + <attr name="strokeWidth" /> + <!-- View background --> + <attr name="android:background"/> + </declare-styleable> + + <declare-styleable name="LinePageIndicator"> + <!-- Whether or not the indicators should be centered. --> + <attr name="centered" /> + <!-- Color of the unselected lines that represent the pages. --> + <attr name="unselectedColor" /> + <!-- Color of the selected line that represents the current page. --> + <attr name="selectedColor" /> + <!-- Width of each indicator line. --> + <attr name="lineWidth" format="dimension" /> + <!-- Width of each indicator line's stroke. --> + <attr name="strokeWidth" /> + <!-- Width of the gap between each indicator line. --> + <attr name="gapWidth" format="dimension" /> + <!-- View background --> + <attr name="android:background"/> + </declare-styleable> + + <declare-styleable name="TitlePageIndicator"> + <!-- Screen edge padding. --> + <attr name="clipPadding" format="dimension" /> + <!-- Color of the footer line and indicator. --> + <attr name="footerColor" format="color" /> + <!-- Height of the footer line. --> + <attr name="footerLineHeight" format="dimension" /> + <!-- Style of the indicator. Default is triangle. --> + <attr name="footerIndicatorStyle"> + <enum name="none" value="0" /> + <enum name="triangle" value="1" /> + <enum name="underline" value="2" /> + </attr> + <!-- Height of the indicator above the footer line. --> + <attr name="footerIndicatorHeight" format="dimension" /> + <!-- Left and right padding of the underline indicator. --> + <attr name="footerIndicatorUnderlinePadding" format="dimension" /> + <!-- Padding between the bottom of the title and the footer. --> + <attr name="footerPadding" format="dimension" /> + <!-- Position of the line. --> + <attr name="linePosition"> + <enum name="bottom" value="0"/> + <enum name="top" value="1"/> + </attr> + <!-- Color of the selected title. --> + <attr name="selectedColor" /> + <!-- Whether or not the selected item is displayed as bold. --> + <attr name="selectedBold" format="boolean" /> + <!-- Color of regular titles. --> + <attr name="android:textColor" /> + <!-- Size of title text. --> + <attr name="android:textSize" /> + <!-- Padding between titles when bumping into each other. --> + <attr name="titlePadding" format="dimension" /> + <!-- Padding between titles and the top of the View. --> + <attr name="topPadding" format="dimension" /> + <!-- View background --> + <attr name="android:background"/> + </declare-styleable> + + <declare-styleable name="UnderlinePageIndicator"> + <!-- Whether or not the selected indicator fades. --> + <attr name="fades" format="boolean" /> + <!-- Length of the delay to fade the indicator. --> + <attr name="fadeDelay" format="integer" /> + <!-- Length of the indicator fade to transparent. --> + <attr name="fadeLength" format="integer" /> + <!-- Color of the selected line that represents the current page. --> + <attr name="selectedColor" /> + <!-- View background --> + <attr name="android:background"/> + </declare-styleable> +</resources> diff --git a/res/values/vpi__colors.xml b/res/values/vpi__colors.xml new file mode 100644 index 0000000..62ca607 --- /dev/null +++ b/res/values/vpi__colors.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2012 Jake Wharton + + 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. +--> + +<resources> + <color name="vpi__background_holo_dark">#ff000000</color> + <color name="vpi__background_holo_light">#fff3f3f3</color> + <color name="vpi__bright_foreground_holo_dark">@color/vpi__background_holo_light</color> + <color name="vpi__bright_foreground_holo_light">@color/vpi__background_holo_dark</color> + <color name="vpi__bright_foreground_disabled_holo_dark">#ff4c4c4c</color> + <color name="vpi__bright_foreground_disabled_holo_light">#ffb2b2b2</color> + <color name="vpi__bright_foreground_inverse_holo_dark">@color/vpi__bright_foreground_holo_light</color> + <color name="vpi__bright_foreground_inverse_holo_light">@color/vpi__bright_foreground_holo_dark</color> +</resources> diff --git a/res/values/vpi__defaults.xml b/res/values/vpi__defaults.xml new file mode 100644 index 0000000..e4d44cb --- /dev/null +++ b/res/values/vpi__defaults.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2012 Jake Wharton + + 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. +--> + +<resources> + <bool name="default_circle_indicator_centered">true</bool> + <color name="default_circle_indicator_fill_color">#FFFFFFFF</color> + <color name="default_circle_indicator_page_color">#00000000</color> + <integer name="default_circle_indicator_orientation">0</integer> + <dimen name="default_circle_indicator_radius">3dp</dimen> + <bool name="default_circle_indicator_snap">false</bool> + <color name="default_circle_indicator_stroke_color">#FFDDDDDD</color> + <dimen name="default_circle_indicator_stroke_width">1dp</dimen> + + <dimen name="default_line_indicator_line_width">12dp</dimen> + <dimen name="default_line_indicator_gap_width">4dp</dimen> + <dimen name="default_line_indicator_stroke_width">1dp</dimen> + <color name="default_line_indicator_selected_color">#FF33B5E5</color> + <color name="default_line_indicator_unselected_color">#FFBBBBBB</color> + <bool name="default_line_indicator_centered">true</bool> + + <dimen name="default_title_indicator_clip_padding">4dp</dimen> + <color name="default_title_indicator_footer_color">#FF33B5E5</color> + <dimen name="default_title_indicator_footer_line_height">2dp</dimen> + <integer name="default_title_indicator_footer_indicator_style">2</integer> + <dimen name="default_title_indicator_footer_indicator_height">4dp</dimen> + <dimen name="default_title_indicator_footer_indicator_underline_padding">20dp</dimen> + <dimen name="default_title_indicator_footer_padding">7dp</dimen> + <integer name="default_title_indicator_line_position">0</integer> + <color name="default_title_indicator_selected_color">#FFFFFFFF</color> + <bool name="default_title_indicator_selected_bold">true</bool> + <color name="default_title_indicator_text_color">#BBFFFFFF</color> + <dimen name="default_title_indicator_text_size">15dp</dimen> + <dimen name="default_title_indicator_title_padding">5dp</dimen> + <dimen name="default_title_indicator_top_padding">7dp</dimen> + + <bool name="default_underline_indicator_fades">true</bool> + <integer name="default_underline_indicator_fade_delay">300</integer> + <integer name="default_underline_indicator_fade_length">400</integer> + <color name="default_underline_indicator_selected_color">#FF33B5E5</color> +</resources>
\ No newline at end of file diff --git a/res/values/vpi__styles.xml b/res/values/vpi__styles.xml new file mode 100644 index 0000000..4f40f4a --- /dev/null +++ b/res/values/vpi__styles.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 Jake Wharton + + 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. +--> + +<resources> + <style name="Theme.PageIndicatorDefaults" parent="android:Theme"> + <item name="vpiIconPageIndicatorStyle">@style/Widget.IconPageIndicator</item> + <item name="vpiTabPageIndicatorStyle">@style/Widget.TabPageIndicator</item> + </style> + + <style name="Widget"> + </style> + + <style name="Widget.TabPageIndicator" parent="Widget"> + <item name="android:gravity">center</item> + <item name="android:background">@drawable/vpi__tab_indicator</item> + <item name="android:paddingLeft">22dip</item> + <item name="android:paddingRight">22dip</item> + <item name="android:paddingTop">12dp</item> + <item name="android:paddingBottom">12dp</item> + <item name="android:textAppearance">@style/TextAppearance.TabPageIndicator</item> + <item name="android:textSize">12sp</item> + <item name="android:maxLines">1</item> + </style> + + <style name="TextAppearance.TabPageIndicator" parent="Widget"> + <item name="android:textStyle">bold</item> + <item name="android:textColor">@color/vpi__dark_theme</item> + </style> + + <style name="Widget.IconPageIndicator" parent="Widget"> + <item name="android:layout_marginLeft">6dp</item> + <item name="android:layout_marginRight">6dp</item> + </style> +</resources> diff --git a/src/com/viewpagerindicator/CirclePageIndicator.java b/src/com/viewpagerindicator/CirclePageIndicator.java new file mode 100644 index 0000000..f441e89 --- /dev/null +++ b/src/com/viewpagerindicator/CirclePageIndicator.java @@ -0,0 +1,555 @@ +/* + * Copyright (C) 2011 Patrik Akerfeldt + * Copyright (C) 2011 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.viewpagerindicator; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.LinearLayout.VERTICAL; + +/** + * Draws circles (one for each view). The current view position is filled and + * others are only stroked. + */ +public class CirclePageIndicator extends View implements PageIndicator { + private static final int INVALID_POINTER = -1; + + private float mRadius; + private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG); + private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG); + private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG); + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mListener; + private int mCurrentPage; + private int mSnapPage; + private float mPageOffset; + private int mScrollState; + private int mOrientation; + private boolean mCentered; + private boolean mSnap; + + private int mTouchSlop; + private float mLastMotionX = -1; + private int mActivePointerId = INVALID_POINTER; + private boolean mIsDragging; + + + public CirclePageIndicator(Context context) { + this(context, null); + } + + public CirclePageIndicator(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.vpiCirclePageIndicatorStyle); + } + + public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (isInEditMode()) return; + + //Load defaults from resources + final Resources res = getResources(); + final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color); + final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color); + final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation); + final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color); + final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width); + final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius); + final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered); + final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap); + + //Retrieve styles attributes + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0); + + mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered); + mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation); + mPaintPageFill.setStyle(Style.FILL); + mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor)); + mPaintStroke.setStyle(Style.STROKE); + mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor)); + mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth)); + mPaintFill.setStyle(Style.FILL); + mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor)); + mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius); + mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap); + + Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background); + if (background != null) { + setBackgroundDrawable(background); + } + + a.recycle(); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + } + + + public void setCentered(boolean centered) { + mCentered = centered; + invalidate(); + } + + public boolean isCentered() { + return mCentered; + } + + public void setPageColor(int pageColor) { + mPaintPageFill.setColor(pageColor); + invalidate(); + } + + public int getPageColor() { + return mPaintPageFill.getColor(); + } + + public void setFillColor(int fillColor) { + mPaintFill.setColor(fillColor); + invalidate(); + } + + public int getFillColor() { + return mPaintFill.getColor(); + } + + public void setOrientation(int orientation) { + switch (orientation) { + case HORIZONTAL: + case VERTICAL: + mOrientation = orientation; + requestLayout(); + break; + + default: + throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL."); + } + } + + public int getOrientation() { + return mOrientation; + } + + public void setStrokeColor(int strokeColor) { + mPaintStroke.setColor(strokeColor); + invalidate(); + } + + public int getStrokeColor() { + return mPaintStroke.getColor(); + } + + public void setStrokeWidth(float strokeWidth) { + mPaintStroke.setStrokeWidth(strokeWidth); + invalidate(); + } + + public float getStrokeWidth() { + return mPaintStroke.getStrokeWidth(); + } + + public void setRadius(float radius) { + mRadius = radius; + invalidate(); + } + + public float getRadius() { + return mRadius; + } + + public void setSnap(boolean snap) { + mSnap = snap; + invalidate(); + } + + public boolean isSnap() { + return mSnap; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mViewPager == null) { + return; + } + final int count = mViewPager.getAdapter().getCount(); + if (count == 0) { + return; + } + + if (mCurrentPage >= count) { + setCurrentItem(count - 1); + return; + } + + int longSize; + int longPaddingBefore; + int longPaddingAfter; + int shortPaddingBefore; + if (mOrientation == HORIZONTAL) { + longSize = getWidth(); + longPaddingBefore = getPaddingLeft(); + longPaddingAfter = getPaddingRight(); + shortPaddingBefore = getPaddingTop(); + } else { + longSize = getHeight(); + longPaddingBefore = getPaddingTop(); + longPaddingAfter = getPaddingBottom(); + shortPaddingBefore = getPaddingLeft(); + } + + final float threeRadius = mRadius * 3; + final float shortOffset = shortPaddingBefore + mRadius; + float longOffset = longPaddingBefore + mRadius; + if (mCentered) { + longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f); + } + + float dX; + float dY; + + float pageFillRadius = mRadius; + if (mPaintStroke.getStrokeWidth() > 0) { + pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f; + } + + //Draw stroked circles + for (int iLoop = 0; iLoop < count; iLoop++) { + float drawLong = longOffset + (iLoop * threeRadius); + if (mOrientation == HORIZONTAL) { + dX = drawLong; + dY = shortOffset; + } else { + dX = shortOffset; + dY = drawLong; + } + // Only paint fill if not completely transparent + if (mPaintPageFill.getAlpha() > 0) { + canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill); + } + + // Only paint stroke if a stroke width was non-zero + if (pageFillRadius != mRadius) { + canvas.drawCircle(dX, dY, mRadius, mPaintStroke); + } + } + + //Draw the filled circle according to the current scroll + float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius; + if (!mSnap) { + cx += mPageOffset * threeRadius; + } + if (mOrientation == HORIZONTAL) { + dX = longOffset + cx; + dY = shortOffset; + } else { + dX = shortOffset; + dY = longOffset + cx; + } + canvas.drawCircle(dX, dY, mRadius, mPaintFill); + } + + public boolean onTouchEvent(android.view.MotionEvent ev) { + if (super.onTouchEvent(ev)) { + return true; + } + if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { + return false; + } + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mLastMotionX = ev.getX(); + break; + + case MotionEvent.ACTION_MOVE: { + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final float deltaX = x - mLastMotionX; + + if (!mIsDragging) { + if (Math.abs(deltaX) > mTouchSlop) { + mIsDragging = true; + } + } + + if (mIsDragging) { + mLastMotionX = x; + if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { + mViewPager.fakeDragBy(deltaX); + } + } + + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (!mIsDragging) { + final int count = mViewPager.getAdapter().getCount(); + final int width = getWidth(); + final float halfWidth = width / 2f; + final float sixthWidth = width / 6f; + + if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage - 1); + } + return true; + } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage + 1); + } + return true; + } + } + + mIsDragging = false; + mActivePointerId = INVALID_POINTER; + if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); + break; + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionX = MotionEventCompat.getX(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + return true; + } + + @Override + public void setViewPager(ViewPager view) { + if (mViewPager == view) { + return; + } + if (mViewPager != null) { + mViewPager.setOnPageChangeListener(null); + } + if (view.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = view; + mViewPager.setOnPageChangeListener(this); + invalidate(); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mViewPager.setCurrentItem(item); + mCurrentPage = item; + invalidate(); + } + + @Override + public void notifyDataSetChanged() { + invalidate(); + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mListener != null) { + mListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mCurrentPage = position; + mPageOffset = positionOffset; + invalidate(); + + if (mListener != null) { + mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mCurrentPage = position; + mSnapPage = position; + invalidate(); + } + + if (mListener != null) { + mListener.onPageSelected(position); + } + } + + @Override + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mListener = listener; + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onMeasure(int, int) + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mOrientation == HORIZONTAL) { + setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec)); + } else { + setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec)); + } + } + + /** + * Determines the width of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The width of the view, honoring constraints from measureSpec + */ + private int measureLong(int measureSpec) { + int result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) { + //We were told how big to be + result = specSize; + } else { + //Calculate the width according the views count + final int count = mViewPager.getAdapter().getCount(); + result = (int)(getPaddingLeft() + getPaddingRight() + + (count * 2 * mRadius) + (count - 1) * mRadius + 1); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return result; + } + + /** + * Determines the height of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The height of the view, honoring constraints from measureSpec + */ + private int measureShort(int measureSpec) { + int result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + //We were told how big to be + result = specSize; + } else { + //Measure the height + result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return result; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState)state; + super.onRestoreInstanceState(savedState.getSuperState()); + mCurrentPage = savedState.currentPage; + mSnapPage = savedState.currentPage; + requestLayout(); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState savedState = new SavedState(superState); + savedState.currentPage = mCurrentPage; + return savedState; + } + + static class SavedState extends BaseSavedState { + int currentPage; + + public SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(currentPage); + } + + @SuppressWarnings("UnusedDeclaration") + 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]; + } + }; + } +} diff --git a/src/com/viewpagerindicator/IconPageIndicator.java b/src/com/viewpagerindicator/IconPageIndicator.java new file mode 100644 index 0000000..2e7d246 --- /dev/null +++ b/src/com/viewpagerindicator/IconPageIndicator.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2012 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.viewpagerindicator; + +import android.content.Context; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; + +import static android.view.ViewGroup.LayoutParams.FILL_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +/** + * This widget implements the dynamic action bar tab behavior that can change + * across different configurations or circumstances. + */ +public class IconPageIndicator extends HorizontalScrollView implements PageIndicator { + private final IcsLinearLayout mIconsLayout; + + private ViewPager mViewPager; + private OnPageChangeListener mListener; + private Runnable mIconSelector; + private int mSelectedIndex; + + public IconPageIndicator(Context context) { + this(context, null); + } + + public IconPageIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + setHorizontalScrollBarEnabled(false); + + mIconsLayout = new IcsLinearLayout(context, R.attr.vpiIconPageIndicatorStyle); + addView(mIconsLayout, new LayoutParams(WRAP_CONTENT, FILL_PARENT, Gravity.CENTER)); + } + + private void animateToIcon(final int position) { + final View iconView = mIconsLayout.getChildAt(position); + if (mIconSelector != null) { + removeCallbacks(mIconSelector); + } + mIconSelector = new Runnable() { + public void run() { + final int scrollPos = iconView.getLeft() - (getWidth() - iconView.getWidth()) / 2; + smoothScrollTo(scrollPos, 0); + mIconSelector = null; + } + }; + post(mIconSelector); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mIconSelector != null) { + // Re-post the selector we saved + post(mIconSelector); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mIconSelector != null) { + removeCallbacks(mIconSelector); + } + } + + @Override + public void onPageScrollStateChanged(int arg0) { + if (mListener != null) { + mListener.onPageScrollStateChanged(arg0); + } + } + + @Override + public void onPageScrolled(int arg0, float arg1, int arg2) { + if (mListener != null) { + mListener.onPageScrolled(arg0, arg1, arg2); + } + } + + @Override + public void onPageSelected(int arg0) { + setCurrentItem(arg0); + if (mListener != null) { + mListener.onPageSelected(arg0); + } + } + + @Override + public void setViewPager(ViewPager view) { + if (mViewPager == view) { + return; + } + if (mViewPager != null) { + mViewPager.setOnPageChangeListener(null); + } + PagerAdapter adapter = view.getAdapter(); + if (adapter == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = view; + view.setOnPageChangeListener(this); + notifyDataSetChanged(); + } + + public void notifyDataSetChanged() { + mIconsLayout.removeAllViews(); + IconPagerAdapter iconAdapter = (IconPagerAdapter) mViewPager.getAdapter(); + int count = iconAdapter.getCount(); + for (int i = 0; i < count; i++) { + ImageView view = new ImageView(getContext(), null, R.attr.vpiIconPageIndicatorStyle); + view.setImageResource(iconAdapter.getIconResId(i)); + mIconsLayout.addView(view); + } + if (mSelectedIndex > count) { + mSelectedIndex = count - 1; + } + setCurrentItem(mSelectedIndex); + requestLayout(); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mSelectedIndex = item; + mViewPager.setCurrentItem(item); + + int tabCount = mIconsLayout.getChildCount(); + for (int i = 0; i < tabCount; i++) { + View child = mIconsLayout.getChildAt(i); + boolean isSelected = (i == item); + child.setSelected(isSelected); + if (isSelected) { + animateToIcon(item); + } + } + } + + @Override + public void setOnPageChangeListener(OnPageChangeListener listener) { + mListener = listener; + } +} diff --git a/src/com/viewpagerindicator/IconPagerAdapter.java b/src/com/viewpagerindicator/IconPagerAdapter.java new file mode 100644 index 0000000..b133d48 --- /dev/null +++ b/src/com/viewpagerindicator/IconPagerAdapter.java @@ -0,0 +1,11 @@ +package com.viewpagerindicator; + +public interface IconPagerAdapter { + /** + * Get icon representing the page at {@code index} in the adapter. + */ + int getIconResId(int index); + + // From PagerAdapter + int getCount(); +} diff --git a/src/com/viewpagerindicator/IcsLinearLayout.java b/src/com/viewpagerindicator/IcsLinearLayout.java new file mode 100644 index 0000000..cbf619e --- /dev/null +++ b/src/com/viewpagerindicator/IcsLinearLayout.java @@ -0,0 +1,182 @@ +package com.viewpagerindicator; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.LinearLayout; + +/** + * A simple extension of a regular linear layout that supports the divider API + * of Android 4.0+. The dividers are added adjacent to the children by changing + * their layout params. If you need to rely on the margins which fall in the + * same orientation as the layout you should wrap the child in a simple + * {@link android.widget.FrameLayout} so it can receive the margin. + */ +class IcsLinearLayout extends LinearLayout { + private static final int[] LL = new int[] { + /* 0 */ android.R.attr.divider, + /* 1 */ android.R.attr.showDividers, + /* 2 */ android.R.attr.dividerPadding, + }; + private static final int LL_DIVIDER = 0; + private static final int LL_SHOW_DIVIDER = 1; + private static final int LL_DIVIDER_PADDING = 2; + + private Drawable mDivider; + private int mDividerWidth; + private int mDividerHeight; + private int mShowDividers; + private int mDividerPadding; + + + public IcsLinearLayout(Context context, int themeAttr) { + super(context); + + TypedArray a = context.obtainStyledAttributes(null, LL, themeAttr, 0); + setDividerDrawable(a.getDrawable(IcsLinearLayout.LL_DIVIDER)); + mDividerPadding = a.getDimensionPixelSize(LL_DIVIDER_PADDING, 0); + mShowDividers = a.getInteger(LL_SHOW_DIVIDER, SHOW_DIVIDER_NONE); + a.recycle(); + } + + public void setDividerDrawable(Drawable divider) { + if (divider == mDivider) { + return; + } + mDivider = divider; + if (divider != null) { + mDividerWidth = divider.getIntrinsicWidth(); + mDividerHeight = divider.getIntrinsicHeight(); + } else { + mDividerWidth = 0; + mDividerHeight = 0; + } + setWillNotDraw(divider == null); + requestLayout(); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { + final int index = indexOfChild(child); + final int orientation = getOrientation(); + final LayoutParams params = (LayoutParams) child.getLayoutParams(); + if (hasDividerBeforeChildAt(index)) { + if (orientation == VERTICAL) { + //Account for the divider by pushing everything up + params.topMargin = mDividerHeight; + } else { + //Account for the divider by pushing everything left + params.leftMargin = mDividerWidth; + } + } + + final int count = getChildCount(); + if (index == count - 1) { + if (hasDividerBeforeChildAt(count)) { + if (orientation == VERTICAL) { + params.bottomMargin = mDividerHeight; + } else { + params.rightMargin = mDividerWidth; + } + } + } + super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed); + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDivider != null) { + if (getOrientation() == VERTICAL) { + drawDividersVertical(canvas); + } else { + drawDividersHorizontal(canvas); + } + } + super.onDraw(canvas); + } + + private void drawDividersVertical(Canvas canvas) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + + if (child != null && child.getVisibility() != GONE) { + if (hasDividerBeforeChildAt(i)) { + final android.widget.LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child.getLayoutParams(); + final int top = child.getTop() - lp.topMargin/* - mDividerHeight*/; + drawHorizontalDivider(canvas, top); + } + } + } + + if (hasDividerBeforeChildAt(count)) { + final View child = getChildAt(count - 1); + int bottom = 0; + if (child == null) { + bottom = getHeight() - getPaddingBottom() - mDividerHeight; + } else { + //final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + bottom = child.getBottom()/* + lp.bottomMargin*/; + } + drawHorizontalDivider(canvas, bottom); + } + } + + private void drawDividersHorizontal(Canvas canvas) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + + if (child != null && child.getVisibility() != GONE) { + if (hasDividerBeforeChildAt(i)) { + final android.widget.LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child.getLayoutParams(); + final int left = child.getLeft() - lp.leftMargin/* - mDividerWidth*/; + drawVerticalDivider(canvas, left); + } + } + } + + if (hasDividerBeforeChildAt(count)) { + final View child = getChildAt(count - 1); + int right = 0; + if (child == null) { + right = getWidth() - getPaddingRight() - mDividerWidth; + } else { + //final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + right = child.getRight()/* + lp.rightMargin*/; + } + drawVerticalDivider(canvas, right); + } + } + + private void drawHorizontalDivider(Canvas canvas, int top) { + mDivider.setBounds(getPaddingLeft() + mDividerPadding, top, + getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight); + mDivider.draw(canvas); + } + + private void drawVerticalDivider(Canvas canvas, int left) { + mDivider.setBounds(left, getPaddingTop() + mDividerPadding, + left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding); + mDivider.draw(canvas); + } + + private boolean hasDividerBeforeChildAt(int childIndex) { + if (childIndex == 0 || childIndex == getChildCount()) { + return false; + } + if ((mShowDividers & SHOW_DIVIDER_MIDDLE) != 0) { + boolean hasVisibleViewBefore = false; + for (int i = childIndex - 1; i >= 0; i--) { + if (getChildAt(i).getVisibility() != GONE) { + hasVisibleViewBefore = true; + break; + } + } + return hasVisibleViewBefore; + } + return false; + } +} diff --git a/src/com/viewpagerindicator/LinePageIndicator.java b/src/com/viewpagerindicator/LinePageIndicator.java new file mode 100644 index 0000000..a009cbf --- /dev/null +++ b/src/com/viewpagerindicator/LinePageIndicator.java @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2012 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.viewpagerindicator; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.FloatMath; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Draws a line for each page. The current page line is colored differently + * than the unselected page lines. + */ +public class LinePageIndicator extends View implements PageIndicator { + private static final int INVALID_POINTER = -1; + + private final Paint mPaintUnselected = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint mPaintSelected = new Paint(Paint.ANTI_ALIAS_FLAG); + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mListener; + private int mCurrentPage; + private boolean mCentered; + private float mLineWidth; + private float mGapWidth; + + private int mTouchSlop; + private float mLastMotionX = -1; + private int mActivePointerId = INVALID_POINTER; + private boolean mIsDragging; + + + public LinePageIndicator(Context context) { + this(context, null); + } + + public LinePageIndicator(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.vpiLinePageIndicatorStyle); + } + + public LinePageIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (isInEditMode()) return; + + final Resources res = getResources(); + + //Load defaults from resources + final int defaultSelectedColor = res.getColor(R.color.default_line_indicator_selected_color); + final int defaultUnselectedColor = res.getColor(R.color.default_line_indicator_unselected_color); + final float defaultLineWidth = res.getDimension(R.dimen.default_line_indicator_line_width); + final float defaultGapWidth = res.getDimension(R.dimen.default_line_indicator_gap_width); + final float defaultStrokeWidth = res.getDimension(R.dimen.default_line_indicator_stroke_width); + final boolean defaultCentered = res.getBoolean(R.bool.default_line_indicator_centered); + + //Retrieve styles attributes + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LinePageIndicator, defStyle, 0); + + mCentered = a.getBoolean(R.styleable.LinePageIndicator_centered, defaultCentered); + mLineWidth = a.getDimension(R.styleable.LinePageIndicator_lineWidth, defaultLineWidth); + mGapWidth = a.getDimension(R.styleable.LinePageIndicator_gapWidth, defaultGapWidth); + setStrokeWidth(a.getDimension(R.styleable.LinePageIndicator_strokeWidth, defaultStrokeWidth)); + mPaintUnselected.setColor(a.getColor(R.styleable.LinePageIndicator_unselectedColor, defaultUnselectedColor)); + mPaintSelected.setColor(a.getColor(R.styleable.LinePageIndicator_selectedColor, defaultSelectedColor)); + + Drawable background = a.getDrawable(R.styleable.LinePageIndicator_android_background); + if (background != null) { + setBackgroundDrawable(background); + } + + a.recycle(); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + } + + + public void setCentered(boolean centered) { + mCentered = centered; + invalidate(); + } + + public boolean isCentered() { + return mCentered; + } + + public void setUnselectedColor(int unselectedColor) { + mPaintUnselected.setColor(unselectedColor); + invalidate(); + } + + public int getUnselectedColor() { + return mPaintUnselected.getColor(); + } + + public void setSelectedColor(int selectedColor) { + mPaintSelected.setColor(selectedColor); + invalidate(); + } + + public int getSelectedColor() { + return mPaintSelected.getColor(); + } + + public void setLineWidth(float lineWidth) { + mLineWidth = lineWidth; + invalidate(); + } + + public float getLineWidth() { + return mLineWidth; + } + + public void setStrokeWidth(float lineHeight) { + mPaintSelected.setStrokeWidth(lineHeight); + mPaintUnselected.setStrokeWidth(lineHeight); + invalidate(); + } + + public float getStrokeWidth() { + return mPaintSelected.getStrokeWidth(); + } + + public void setGapWidth(float gapWidth) { + mGapWidth = gapWidth; + invalidate(); + } + + public float getGapWidth() { + return mGapWidth; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mViewPager == null) { + return; + } + final int count = mViewPager.getAdapter().getCount(); + if (count == 0) { + return; + } + + if (mCurrentPage >= count) { + setCurrentItem(count - 1); + return; + } + + final float lineWidthAndGap = mLineWidth + mGapWidth; + final float indicatorWidth = (count * lineWidthAndGap) - mGapWidth; + final float paddingTop = getPaddingTop(); + final float paddingLeft = getPaddingLeft(); + final float paddingRight = getPaddingRight(); + + float verticalOffset = paddingTop + ((getHeight() - paddingTop - getPaddingBottom()) / 2.0f); + float horizontalOffset = paddingLeft; + if (mCentered) { + horizontalOffset += ((getWidth() - paddingLeft - paddingRight) / 2.0f) - (indicatorWidth / 2.0f); + } + + //Draw stroked circles + for (int i = 0; i < count; i++) { + float dx1 = horizontalOffset + (i * lineWidthAndGap); + float dx2 = dx1 + mLineWidth; + canvas.drawLine(dx1, verticalOffset, dx2, verticalOffset, (i == mCurrentPage) ? mPaintSelected : mPaintUnselected); + } + } + + public boolean onTouchEvent(android.view.MotionEvent ev) { + if (super.onTouchEvent(ev)) { + return true; + } + if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { + return false; + } + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mLastMotionX = ev.getX(); + break; + + case MotionEvent.ACTION_MOVE: { + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final float deltaX = x - mLastMotionX; + + if (!mIsDragging) { + if (Math.abs(deltaX) > mTouchSlop) { + mIsDragging = true; + } + } + + if (mIsDragging) { + mLastMotionX = x; + if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { + mViewPager.fakeDragBy(deltaX); + } + } + + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (!mIsDragging) { + final int count = mViewPager.getAdapter().getCount(); + final int width = getWidth(); + final float halfWidth = width / 2f; + final float sixthWidth = width / 6f; + + if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage - 1); + } + return true; + } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage + 1); + } + return true; + } + } + + mIsDragging = false; + mActivePointerId = INVALID_POINTER; + if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); + break; + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionX = MotionEventCompat.getX(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + return true; + } + + @Override + public void setViewPager(ViewPager viewPager) { + if (mViewPager == viewPager) { + return; + } + if (mViewPager != null) { + //Clear us from the old pager. + mViewPager.setOnPageChangeListener(null); + } + if (viewPager.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = viewPager; + mViewPager.setOnPageChangeListener(this); + invalidate(); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mViewPager.setCurrentItem(item); + mCurrentPage = item; + invalidate(); + } + + @Override + public void notifyDataSetChanged() { + invalidate(); + } + + @Override + public void onPageScrollStateChanged(int state) { + if (mListener != null) { + mListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (mListener != null) { + mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + mCurrentPage = position; + invalidate(); + + if (mListener != null) { + mListener.onPageSelected(position); + } + } + + @Override + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mListener = listener; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); + } + + /** + * Determines the width of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The width of the view, honoring constraints from measureSpec + */ + private int measureWidth(int measureSpec) { + float result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) { + //We were told how big to be + result = specSize; + } else { + //Calculate the width according the views count + final int count = mViewPager.getAdapter().getCount(); + result = getPaddingLeft() + getPaddingRight() + (count * mLineWidth) + ((count - 1) * mGapWidth); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return (int)FloatMath.ceil(result); + } + + /** + * Determines the height of this view + * + * @param measureSpec + * A measureSpec packed into an int + * @return The height of the view, honoring constraints from measureSpec + */ + private int measureHeight(int measureSpec) { + float result; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + //We were told how big to be + result = specSize; + } else { + //Measure the height + result = mPaintSelected.getStrokeWidth() + getPaddingTop() + getPaddingBottom(); + //Respect AT_MOST value if that was what is called for by measureSpec + if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(result, specSize); + } + } + return (int)FloatMath.ceil(result); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState)state; + super.onRestoreInstanceState(savedState.getSuperState()); + mCurrentPage = savedState.currentPage; + requestLayout(); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState savedState = new SavedState(superState); + savedState.currentPage = mCurrentPage; + return savedState; + } + + static class SavedState extends BaseSavedState { + int currentPage; + + public SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(currentPage); + } + + @SuppressWarnings("UnusedDeclaration") + 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]; + } + }; + } +}
\ No newline at end of file diff --git a/src/com/viewpagerindicator/PageIndicator.java b/src/com/viewpagerindicator/PageIndicator.java new file mode 100644 index 0000000..c08c00a --- /dev/null +++ b/src/com/viewpagerindicator/PageIndicator.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 Patrik Akerfeldt + * Copyright (C) 2011 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.viewpagerindicator; + +import android.support.v4.view.ViewPager; + +/** + * A PageIndicator is responsible to show an visual indicator on the total views + * number and the current visible view. + */ +public interface PageIndicator extends ViewPager.OnPageChangeListener { + /** + * Bind the indicator to a ViewPager. + * + * @param view + */ + void setViewPager(ViewPager view); + + /** + * Bind the indicator to a ViewPager. + * + * @param view + * @param initialPosition + */ + void setViewPager(ViewPager view, int initialPosition); + + /** + * <p>Set the current page of both the ViewPager and indicator.</p> + * + * <p>This <strong>must</strong> be used if you need to set the page before + * the views are drawn on screen (e.g., default start page).</p> + * + * @param item + */ + void setCurrentItem(int item); + + /** + * Set a page change listener which will receive forwarded events. + * + * @param listener + */ + void setOnPageChangeListener(ViewPager.OnPageChangeListener listener); + + /** + * Notify the indicator that the fragment list has changed. + */ + void notifyDataSetChanged(); +} diff --git a/src/com/viewpagerindicator/TabPageIndicator.java b/src/com/viewpagerindicator/TabPageIndicator.java new file mode 100644 index 0000000..1fceda3 --- /dev/null +++ b/src/com/viewpagerindicator/TabPageIndicator.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2011 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.viewpagerindicator; + +import android.content.Context; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +/** + * This widget implements the dynamic action bar tab behavior that can change + * across different configurations or circumstances. + */ +public class TabPageIndicator extends HorizontalScrollView implements PageIndicator { + /** Title text used when no title is provided by the adapter. */ + private static final CharSequence EMPTY_TITLE = ""; + + /** + * Interface for a callback when the selected tab has been reselected. + */ + public interface OnTabReselectedListener { + /** + * Callback when the selected tab has been reselected. + * + * @param position Position of the current center item. + */ + void onTabReselected(int position); + } + + private Runnable mTabSelector; + + private final OnClickListener mTabClickListener = new OnClickListener() { + public void onClick(View view) { + TabView tabView = (TabView)view; + final int oldSelected = mViewPager.getCurrentItem(); + final int newSelected = tabView.getIndex(); + mViewPager.setCurrentItem(newSelected); + if (oldSelected == newSelected && mTabReselectedListener != null) { + mTabReselectedListener.onTabReselected(newSelected); + } + } + }; + + private final IcsLinearLayout mTabLayout; + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mListener; + + private int mMaxTabWidth; + private int mSelectedTabIndex; + + private OnTabReselectedListener mTabReselectedListener; + + public TabPageIndicator(Context context) { + this(context, null); + } + + public TabPageIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + setHorizontalScrollBarEnabled(false); + + mTabLayout = new IcsLinearLayout(context, R.attr.vpiTabPageIndicatorStyle); + addView(mTabLayout, new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT)); + } + + public void setOnTabReselectedListener(OnTabReselectedListener listener) { + mTabReselectedListener = listener; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final boolean lockedExpanded = widthMode == MeasureSpec.EXACTLY; + setFillViewport(lockedExpanded); + + final int childCount = mTabLayout.getChildCount(); + if (childCount > 1 && (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) { + if (childCount > 2) { + mMaxTabWidth = (int)(MeasureSpec.getSize(widthMeasureSpec) * 0.4f); + } else { + mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2; + } + } else { + mMaxTabWidth = -1; + } + + final int oldWidth = getMeasuredWidth(); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int newWidth = getMeasuredWidth(); + + if (lockedExpanded && oldWidth != newWidth) { + // Recenter the tab display if we're at a new (scrollable) size. + setCurrentItem(mSelectedTabIndex); + } + } + + private void animateToTab(final int position) { + final View tabView = mTabLayout.getChildAt(position); + if (mTabSelector != null) { + removeCallbacks(mTabSelector); + } + mTabSelector = new Runnable() { + public void run() { + final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2; + smoothScrollTo(scrollPos, 0); + mTabSelector = null; + } + }; + post(mTabSelector); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mTabSelector != null) { + // Re-post the selector we saved + post(mTabSelector); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mTabSelector != null) { + removeCallbacks(mTabSelector); + } + } + + private void addTab(int index, CharSequence text, int iconResId) { + final TabView tabView = new TabView(getContext()); + tabView.mIndex = index; + tabView.setFocusable(true); + tabView.setOnClickListener(mTabClickListener); + tabView.setText(text); + + if (iconResId != 0) { + tabView.setCompoundDrawablesWithIntrinsicBounds(iconResId, 0, 0, 0); + } + + mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, MATCH_PARENT, 1)); + } + + @Override + public void onPageScrollStateChanged(int arg0) { + if (mListener != null) { + mListener.onPageScrollStateChanged(arg0); + } + } + + @Override + public void onPageScrolled(int arg0, float arg1, int arg2) { + if (mListener != null) { + mListener.onPageScrolled(arg0, arg1, arg2); + } + } + + @Override + public void onPageSelected(int arg0) { + setCurrentItem(arg0); + if (mListener != null) { + mListener.onPageSelected(arg0); + } + } + + @Override + public void setViewPager(ViewPager view) { + if (mViewPager == view) { + return; + } + if (mViewPager != null) { + mViewPager.setOnPageChangeListener(null); + } + final PagerAdapter adapter = view.getAdapter(); + if (adapter == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = view; + view.setOnPageChangeListener(this); + notifyDataSetChanged(); + } + + public void notifyDataSetChanged() { + mTabLayout.removeAllViews(); + PagerAdapter adapter = mViewPager.getAdapter(); + IconPagerAdapter iconAdapter = null; + if (adapter instanceof IconPagerAdapter) { + iconAdapter = (IconPagerAdapter)adapter; + } + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + CharSequence title = adapter.getPageTitle(i); + if (title == null) { + title = EMPTY_TITLE; + } + int iconResId = 0; + if (iconAdapter != null) { + iconResId = iconAdapter.getIconResId(i); + } + addTab(i, title, iconResId); + } + if (mSelectedTabIndex > count) { + mSelectedTabIndex = count - 1; + } + setCurrentItem(mSelectedTabIndex); + requestLayout(); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mSelectedTabIndex = item; + mViewPager.setCurrentItem(item); + + final int tabCount = mTabLayout.getChildCount(); + for (int i = 0; i < tabCount; i++) { + final View child = mTabLayout.getChildAt(i); + final boolean isSelected = (i == item); + child.setSelected(isSelected); + if (isSelected) { + animateToTab(item); + } + } + } + + @Override + public void setOnPageChangeListener(OnPageChangeListener listener) { + mListener = listener; + } + + private class TabView extends TextView { + private int mIndex; + + public TabView(Context context) { + super(context, null, R.attr.vpiTabPageIndicatorStyle); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Re-measure if we went beyond our maximum size. + if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) { + super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY), + heightMeasureSpec); + } + } + + public int getIndex() { + return mIndex; + } + } +} diff --git a/src/com/viewpagerindicator/TitlePageIndicator.java b/src/com/viewpagerindicator/TitlePageIndicator.java new file mode 100644 index 0000000..f155d83 --- /dev/null +++ b/src/com/viewpagerindicator/TitlePageIndicator.java @@ -0,0 +1,870 @@ +/* + * Copyright (C) 2011 Jake Wharton + * Copyright (C) 2011 Patrik Akerfeldt + * Copyright (C) 2011 Francisco Figueiredo Jr. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.viewpagerindicator; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +import java.util.ArrayList; + +/** + * A TitlePageIndicator is a PageIndicator which displays the title of left view + * (if exist), the title of the current select view (centered) and the title of + * the right view (if exist). When the user scrolls the ViewPager then titles are + * also scrolled. + */ +public class TitlePageIndicator extends View implements PageIndicator { + /** + * Percentage indicating what percentage of the screen width away from + * center should the underline be fully faded. A value of 0.25 means that + * halfway between the center of the screen and an edge. + */ + private static final float SELECTION_FADE_PERCENTAGE = 0.25f; + + /** + * Percentage indicating what percentage of the screen width away from + * center should the selected text bold turn off. A value of 0.05 means + * that 10% between the center and an edge. + */ + private static final float BOLD_FADE_PERCENTAGE = 0.05f; + + /** + * Title text used when no title is provided by the adapter. + */ + private static final String EMPTY_TITLE = ""; + + /** + * Interface for a callback when the center item has been clicked. + */ + public interface OnCenterItemClickListener { + /** + * Callback when the center item has been clicked. + * + * @param position Position of the current center item. + */ + void onCenterItemClick(int position); + } + + public enum IndicatorStyle { + None(0), Triangle(1), Underline(2); + + public final int value; + + private IndicatorStyle(int value) { + this.value = value; + } + + public static IndicatorStyle fromValue(int value) { + for (IndicatorStyle style : IndicatorStyle.values()) { + if (style.value == value) { + return style; + } + } + return null; + } + } + + public enum LinePosition { + Bottom(0), Top(1); + + public final int value; + + private LinePosition(int value) { + this.value = value; + } + + public static LinePosition fromValue(int value) { + for (LinePosition position : LinePosition.values()) { + if (position.value == value) { + return position; + } + } + return null; + } + } + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mListener; + private int mCurrentPage = -1; + private float mPageOffset; + private int mScrollState; + private final Paint mPaintText = new Paint(); + private boolean mBoldText; + private int mColorText; + private int mColorSelected; + private Path mPath = new Path(); + private final Rect mBounds = new Rect(); + private final Paint mPaintFooterLine = new Paint(); + private IndicatorStyle mFooterIndicatorStyle; + private LinePosition mLinePosition; + private final Paint mPaintFooterIndicator = new Paint(); + private float mFooterIndicatorHeight; + private float mFooterIndicatorUnderlinePadding; + private float mFooterPadding; + private float mTitlePadding; + private float mTopPadding; + /** Left and right side padding for not active view titles. */ + private float mClipPadding; + private float mFooterLineHeight; + + private static final int INVALID_POINTER = -1; + + private int mTouchSlop; + private float mLastMotionX = -1; + private int mActivePointerId = INVALID_POINTER; + private boolean mIsDragging; + + private OnCenterItemClickListener mCenterItemClickListener; + + + public TitlePageIndicator(Context context) { + this(context, null); + } + + public TitlePageIndicator(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.vpiTitlePageIndicatorStyle); + } + + public TitlePageIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (isInEditMode()) return; + + //Load defaults from resources + final Resources res = getResources(); + final int defaultFooterColor = res.getColor(R.color.default_title_indicator_footer_color); + final float defaultFooterLineHeight = res.getDimension(R.dimen.default_title_indicator_footer_line_height); + final int defaultFooterIndicatorStyle = res.getInteger(R.integer.default_title_indicator_footer_indicator_style); + final float defaultFooterIndicatorHeight = res.getDimension(R.dimen.default_title_indicator_footer_indicator_height); + final float defaultFooterIndicatorUnderlinePadding = res.getDimension(R.dimen.default_title_indicator_footer_indicator_underline_padding); + final float defaultFooterPadding = res.getDimension(R.dimen.default_title_indicator_footer_padding); + final int defaultLinePosition = res.getInteger(R.integer.default_title_indicator_line_position); + final int defaultSelectedColor = res.getColor(R.color.default_title_indicator_selected_color); + final boolean defaultSelectedBold = res.getBoolean(R.bool.default_title_indicator_selected_bold); + final int defaultTextColor = res.getColor(R.color.default_title_indicator_text_color); + final float defaultTextSize = res.getDimension(R.dimen.default_title_indicator_text_size); + final float defaultTitlePadding = res.getDimension(R.dimen.default_title_indicator_title_padding); + final float defaultClipPadding = res.getDimension(R.dimen.default_title_indicator_clip_padding); + final float defaultTopPadding = res.getDimension(R.dimen.default_title_indicator_top_padding); + + //Retrieve styles attributes + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TitlePageIndicator, defStyle, 0); + + //Retrieve the colors to be used for this view and apply them. + mFooterLineHeight = a.getDimension(R.styleable.TitlePageIndicator_footerLineHeight, defaultFooterLineHeight); + mFooterIndicatorStyle = IndicatorStyle.fromValue(a.getInteger(R.styleable.TitlePageIndicator_footerIndicatorStyle, defaultFooterIndicatorStyle)); + mFooterIndicatorHeight = a.getDimension(R.styleable.TitlePageIndicator_footerIndicatorHeight, defaultFooterIndicatorHeight); + mFooterIndicatorUnderlinePadding = a.getDimension(R.styleable.TitlePageIndicator_footerIndicatorUnderlinePadding, defaultFooterIndicatorUnderlinePadding); + mFooterPadding = a.getDimension(R.styleable.TitlePageIndicator_footerPadding, defaultFooterPadding); + mLinePosition = LinePosition.fromValue(a.getInteger(R.styleable.TitlePageIndicator_linePosition, defaultLinePosition)); + mTopPadding = a.getDimension(R.styleable.TitlePageIndicator_topPadding, defaultTopPadding); + mTitlePadding = a.getDimension(R.styleable.TitlePageIndicator_titlePadding, defaultTitlePadding); + mClipPadding = a.getDimension(R.styleable.TitlePageIndicator_clipPadding, defaultClipPadding); + mColorSelected = a.getColor(R.styleable.TitlePageIndicator_selectedColor, defaultSelectedColor); + mColorText = a.getColor(R.styleable.TitlePageIndicator_android_textColor, defaultTextColor); + mBoldText = a.getBoolean(R.styleable.TitlePageIndicator_selectedBold, defaultSelectedBold); + + final float textSize = a.getDimension(R.styleable.TitlePageIndicator_android_textSize, defaultTextSize); + final int footerColor = a.getColor(R.styleable.TitlePageIndicator_footerColor, defaultFooterColor); + mPaintText.setTextSize(textSize); + mPaintText.setAntiAlias(true); + mPaintFooterLine.setStyle(Paint.Style.FILL_AND_STROKE); + mPaintFooterLine.setStrokeWidth(mFooterLineHeight); + mPaintFooterLine.setColor(footerColor); + mPaintFooterIndicator.setStyle(Paint.Style.FILL_AND_STROKE); + mPaintFooterIndicator.setColor(footerColor); + + Drawable background = a.getDrawable(R.styleable.TitlePageIndicator_android_background); + if (background != null) { + setBackgroundDrawable(background); + } + + a.recycle(); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + } + + + public int getFooterColor() { + return mPaintFooterLine.getColor(); + } + + public void setFooterColor(int footerColor) { + mPaintFooterLine.setColor(footerColor); + mPaintFooterIndicator.setColor(footerColor); + invalidate(); + } + + public float getFooterLineHeight() { + return mFooterLineHeight; + } + + public void setFooterLineHeight(float footerLineHeight) { + mFooterLineHeight = footerLineHeight; + mPaintFooterLine.setStrokeWidth(mFooterLineHeight); + invalidate(); + } + + public float getFooterIndicatorHeight() { + return mFooterIndicatorHeight; + } + + public void setFooterIndicatorHeight(float footerTriangleHeight) { + mFooterIndicatorHeight = footerTriangleHeight; + invalidate(); + } + + public float getFooterIndicatorPadding() { + return mFooterPadding; + } + + public void setFooterIndicatorPadding(float footerIndicatorPadding) { + mFooterPadding = footerIndicatorPadding; + invalidate(); + } + + public IndicatorStyle getFooterIndicatorStyle() { + return mFooterIndicatorStyle; + } + + public void setFooterIndicatorStyle(IndicatorStyle indicatorStyle) { + mFooterIndicatorStyle = indicatorStyle; + invalidate(); + } + + public LinePosition getLinePosition() { + return mLinePosition; + } + + public void setLinePosition(LinePosition linePosition) { + mLinePosition = linePosition; + invalidate(); + } + + public int getSelectedColor() { + return mColorSelected; + } + + public void setSelectedColor(int selectedColor) { + mColorSelected = selectedColor; + invalidate(); + } + + public boolean isSelectedBold() { + return mBoldText; + } + + public void setSelectedBold(boolean selectedBold) { + mBoldText = selectedBold; + invalidate(); + } + + public int getTextColor() { + return mColorText; + } + + public void setTextColor(int textColor) { + mPaintText.setColor(textColor); + mColorText = textColor; + invalidate(); + } + + public float getTextSize() { + return mPaintText.getTextSize(); + } + + public void setTextSize(float textSize) { + mPaintText.setTextSize(textSize); + invalidate(); + } + + public float getTitlePadding() { + return this.mTitlePadding; + } + + public void setTitlePadding(float titlePadding) { + mTitlePadding = titlePadding; + invalidate(); + } + + public float getTopPadding() { + return this.mTopPadding; + } + + public void setTopPadding(float topPadding) { + mTopPadding = topPadding; + invalidate(); + } + + public float getClipPadding() { + return this.mClipPadding; + } + + public void setClipPadding(float clipPadding) { + mClipPadding = clipPadding; + invalidate(); + } + + public void setTypeface(Typeface typeface) { + mPaintText.setTypeface(typeface); + invalidate(); + } + + public Typeface getTypeface() { + return mPaintText.getTypeface(); + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onDraw(android.graphics.Canvas) + */ + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mViewPager == null) { + return; + } + final int count = mViewPager.getAdapter().getCount(); + if (count == 0) { + return; + } + + // mCurrentPage is -1 on first start and after orientation changed. If so, retrieve the correct index from viewpager. + if (mCurrentPage == -1 && mViewPager != null) { + mCurrentPage = mViewPager.getCurrentItem(); + } + + //Calculate views bounds + ArrayList<Rect> bounds = calculateAllBounds(mPaintText); + final int boundsSize = bounds.size(); + + //Make sure we're on a page that still exists + if (mCurrentPage >= boundsSize) { + setCurrentItem(boundsSize - 1); + return; + } + + final int countMinusOne = count - 1; + final float halfWidth = getWidth() / 2f; + final int left = getLeft(); + final float leftClip = left + mClipPadding; + final int width = getWidth(); + int height = getHeight(); + final int right = left + width; + final float rightClip = right - mClipPadding; + + int page = mCurrentPage; + float offsetPercent; + if (mPageOffset <= 0.5) { + offsetPercent = mPageOffset; + } else { + page += 1; + offsetPercent = 1 - mPageOffset; + } + final boolean currentSelected = (offsetPercent <= SELECTION_FADE_PERCENTAGE); + final boolean currentBold = (offsetPercent <= BOLD_FADE_PERCENTAGE); + final float selectedPercent = (SELECTION_FADE_PERCENTAGE - offsetPercent) / SELECTION_FADE_PERCENTAGE; + + //Verify if the current view must be clipped to the screen + Rect curPageBound = bounds.get(mCurrentPage); + float curPageWidth = curPageBound.right - curPageBound.left; + if (curPageBound.left < leftClip) { + //Try to clip to the screen (left side) + clipViewOnTheLeft(curPageBound, curPageWidth, left); + } + if (curPageBound.right > rightClip) { + //Try to clip to the screen (right side) + clipViewOnTheRight(curPageBound, curPageWidth, right); + } + + //Left views starting from the current position + if (mCurrentPage > 0) { + for (int i = mCurrentPage - 1; i >= 0; i--) { + Rect bound = bounds.get(i); + //Is left side is outside the screen + if (bound.left < leftClip) { + int w = bound.right - bound.left; + //Try to clip to the screen (left side) + clipViewOnTheLeft(bound, w, left); + //Except if there's an intersection with the right view + Rect rightBound = bounds.get(i + 1); + //Intersection + if (bound.right + mTitlePadding > rightBound.left) { + bound.left = (int) (rightBound.left - w - mTitlePadding); + bound.right = bound.left + w; + } + } + } + } + //Right views starting from the current position + if (mCurrentPage < countMinusOne) { + for (int i = mCurrentPage + 1 ; i < count; i++) { + Rect bound = bounds.get(i); + //If right side is outside the screen + if (bound.right > rightClip) { + int w = bound.right - bound.left; + //Try to clip to the screen (right side) + clipViewOnTheRight(bound, w, right); + //Except if there's an intersection with the left view + Rect leftBound = bounds.get(i - 1); + //Intersection + if (bound.left - mTitlePadding < leftBound.right) { + bound.left = (int) (leftBound.right + mTitlePadding); + bound.right = bound.left + w; + } + } + } + } + + //Now draw views + int colorTextAlpha = mColorText >>> 24; + for (int i = 0; i < count; i++) { + //Get the title + Rect bound = bounds.get(i); + //Only if one side is visible + if ((bound.left > left && bound.left < right) || (bound.right > left && bound.right < right)) { + final boolean currentPage = (i == page); + final CharSequence pageTitle = getTitle(i); + + //Only set bold if we are within bounds + mPaintText.setFakeBoldText(currentPage && currentBold && mBoldText); + + //Draw text as unselected + mPaintText.setColor(mColorText); + if(currentPage && currentSelected) { + //Fade out/in unselected text as the selected text fades in/out + mPaintText.setAlpha(colorTextAlpha - (int)(colorTextAlpha * selectedPercent)); + } + + //Except if there's an intersection with the right view + if (i < boundsSize - 1) { + Rect rightBound = bounds.get(i + 1); + //Intersection + if (bound.right + mTitlePadding > rightBound.left) { + int w = bound.right - bound.left; + bound.left = (int) (rightBound.left - w - mTitlePadding); + bound.right = bound.left + w; + } + } + canvas.drawText(pageTitle, 0, pageTitle.length(), bound.left, bound.bottom + mTopPadding, mPaintText); + + //If we are within the selected bounds draw the selected text + if (currentPage && currentSelected) { + mPaintText.setColor(mColorSelected); + mPaintText.setAlpha((int)((mColorSelected >>> 24) * selectedPercent)); + canvas.drawText(pageTitle, 0, pageTitle.length(), bound.left, bound.bottom + mTopPadding, mPaintText); + } + } + } + + //If we want the line on the top change height to zero and invert the line height to trick the drawing code + float footerLineHeight = mFooterLineHeight; + float footerIndicatorLineHeight = mFooterIndicatorHeight; + if (mLinePosition == LinePosition.Top) { + height = 0; + footerLineHeight = -footerLineHeight; + footerIndicatorLineHeight = -footerIndicatorLineHeight; + } + + //Draw the footer line + mPath.reset(); + mPath.moveTo(0, height - footerLineHeight / 2f); + mPath.lineTo(width, height - footerLineHeight / 2f); + mPath.close(); + canvas.drawPath(mPath, mPaintFooterLine); + + float heightMinusLine = height - footerLineHeight; + switch (mFooterIndicatorStyle) { + case Triangle: + mPath.reset(); + mPath.moveTo(halfWidth, heightMinusLine - footerIndicatorLineHeight); + mPath.lineTo(halfWidth + footerIndicatorLineHeight, heightMinusLine); + mPath.lineTo(halfWidth - footerIndicatorLineHeight, heightMinusLine); + mPath.close(); + canvas.drawPath(mPath, mPaintFooterIndicator); + break; + + case Underline: + if (!currentSelected || page >= boundsSize) { + break; + } + + Rect underlineBounds = bounds.get(page); + final float rightPlusPadding = underlineBounds.right + mFooterIndicatorUnderlinePadding; + final float leftMinusPadding = underlineBounds.left - mFooterIndicatorUnderlinePadding; + final float heightMinusLineMinusIndicator = heightMinusLine - footerIndicatorLineHeight; + + mPath.reset(); + mPath.moveTo(leftMinusPadding, heightMinusLine); + mPath.lineTo(rightPlusPadding, heightMinusLine); + mPath.lineTo(rightPlusPadding, heightMinusLineMinusIndicator); + mPath.lineTo(leftMinusPadding, heightMinusLineMinusIndicator); + mPath.close(); + + mPaintFooterIndicator.setAlpha((int)(0xFF * selectedPercent)); + canvas.drawPath(mPath, mPaintFooterIndicator); + mPaintFooterIndicator.setAlpha(0xFF); + break; + } + } + + public boolean onTouchEvent(android.view.MotionEvent ev) { + if (super.onTouchEvent(ev)) { + return true; + } + if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { + return false; + } + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mLastMotionX = ev.getX(); + break; + + case MotionEvent.ACTION_MOVE: { + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final float deltaX = x - mLastMotionX; + + if (!mIsDragging) { + if (Math.abs(deltaX) > mTouchSlop) { + mIsDragging = true; + } + } + + if (mIsDragging) { + mLastMotionX = x; + if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { + mViewPager.fakeDragBy(deltaX); + } + } + + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (!mIsDragging) { + final int count = mViewPager.getAdapter().getCount(); + final int width = getWidth(); + final float halfWidth = width / 2f; + final float sixthWidth = width / 6f; + final float leftThird = halfWidth - sixthWidth; + final float rightThird = halfWidth + sixthWidth; + final float eventX = ev.getX(); + + if (eventX < leftThird) { + if (mCurrentPage > 0) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage - 1); + } + return true; + } + } else if (eventX > rightThird) { + if (mCurrentPage < count - 1) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage + 1); + } + return true; + } + } else { + //Middle third + if (mCenterItemClickListener != null && action != MotionEvent.ACTION_CANCEL) { + mCenterItemClickListener.onCenterItemClick(mCurrentPage); + } + } + } + + mIsDragging = false; + mActivePointerId = INVALID_POINTER; + if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); + break; + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionX = MotionEventCompat.getX(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + return true; + } + + /** + * Set bounds for the right textView including clip padding. + * + * @param curViewBound + * current bounds. + * @param curViewWidth + * width of the view. + */ + private void clipViewOnTheRight(Rect curViewBound, float curViewWidth, int right) { + curViewBound.right = (int) (right - mClipPadding); + curViewBound.left = (int) (curViewBound.right - curViewWidth); + } + + /** + * Set bounds for the left textView including clip padding. + * + * @param curViewBound + * current bounds. + * @param curViewWidth + * width of the view. + */ + private void clipViewOnTheLeft(Rect curViewBound, float curViewWidth, int left) { + curViewBound.left = (int) (left + mClipPadding); + curViewBound.right = (int) (mClipPadding + curViewWidth); + } + + /** + * Calculate views bounds and scroll them according to the current index + * + * @param paint + * @return + */ + private ArrayList<Rect> calculateAllBounds(Paint paint) { + ArrayList<Rect> list = new ArrayList<Rect>(); + //For each views (If no values then add a fake one) + final int count = mViewPager.getAdapter().getCount(); + final int width = getWidth(); + final int halfWidth = width / 2; + for (int i = 0; i < count; i++) { + Rect bounds = calcBounds(i, paint); + int w = bounds.right - bounds.left; + int h = bounds.bottom - bounds.top; + bounds.left = (int)(halfWidth - (w / 2f) + ((i - mCurrentPage - mPageOffset) * width)); + bounds.right = bounds.left + w; + bounds.top = 0; + bounds.bottom = h; + list.add(bounds); + } + + return list; + } + + /** + * Calculate the bounds for a view's title + * + * @param index + * @param paint + * @return + */ + private Rect calcBounds(int index, Paint paint) { + //Calculate the text bounds + Rect bounds = new Rect(); + CharSequence title = getTitle(index); + bounds.right = (int) paint.measureText(title, 0, title.length()); + bounds.bottom = (int) (paint.descent() - paint.ascent()); + return bounds; + } + + @Override + public void setViewPager(ViewPager view) { + if (mViewPager == view) { + return; + } + if (mViewPager != null) { + mViewPager.setOnPageChangeListener(null); + } + if (view.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = view; + mViewPager.setOnPageChangeListener(this); + invalidate(); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void notifyDataSetChanged() { + invalidate(); + } + + /** + * Set a callback listener for the center item click. + * + * @param listener Callback instance. + */ + public void setOnCenterItemClickListener(OnCenterItemClickListener listener) { + mCenterItemClickListener = listener; + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mViewPager.setCurrentItem(item); + mCurrentPage = item; + invalidate(); + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mListener != null) { + mListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mCurrentPage = position; + mPageOffset = positionOffset; + invalidate(); + + if (mListener != null) { + mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mCurrentPage = position; + invalidate(); + } + + if (mListener != null) { + mListener.onPageSelected(position); + } + } + + @Override + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mListener = listener; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //Measure our width in whatever mode specified + final int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + + //Determine our height + float height; + final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightSpecMode == MeasureSpec.EXACTLY) { + //We were told how big to be + height = MeasureSpec.getSize(heightMeasureSpec); + } else { + //Calculate the text bounds + mBounds.setEmpty(); + mBounds.bottom = (int) (mPaintText.descent() - mPaintText.ascent()); + height = mBounds.bottom - mBounds.top + mFooterLineHeight + mFooterPadding + mTopPadding; + if (mFooterIndicatorStyle != IndicatorStyle.None) { + height += mFooterIndicatorHeight; + } + } + final int measuredHeight = (int)height; + + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState)state; + super.onRestoreInstanceState(savedState.getSuperState()); + mCurrentPage = savedState.currentPage; + requestLayout(); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState savedState = new SavedState(superState); + savedState.currentPage = mCurrentPage; + return savedState; + } + + static class SavedState extends BaseSavedState { + int currentPage; + + public SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(currentPage); + } + + @SuppressWarnings("UnusedDeclaration") + 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]; + } + }; + } + + private CharSequence getTitle(int i) { + CharSequence title = mViewPager.getAdapter().getPageTitle(i); + if (title == null) { + title = EMPTY_TITLE; + } + return title; + } +} diff --git a/src/com/viewpagerindicator/UnderlinePageIndicator.java b/src/com/viewpagerindicator/UnderlinePageIndicator.java new file mode 100644 index 0000000..dc6f82a --- /dev/null +++ b/src/com/viewpagerindicator/UnderlinePageIndicator.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2012 Jake Wharton + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.viewpagerindicator; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Draws a line for each page. The current page line is colored differently + * than the unselected page lines. + */ +public class UnderlinePageIndicator extends View implements PageIndicator { + private static final int INVALID_POINTER = -1; + private static final int FADE_FRAME_MS = 30; + + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + private boolean mFades; + private int mFadeDelay; + private int mFadeLength; + private int mFadeBy; + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mListener; + private int mScrollState; + private int mCurrentPage; + private float mPositionOffset; + + private int mTouchSlop; + private float mLastMotionX = -1; + private int mActivePointerId = INVALID_POINTER; + private boolean mIsDragging; + + private final Runnable mFadeRunnable = new Runnable() { + @Override public void run() { + if (!mFades) return; + + final int alpha = Math.max(mPaint.getAlpha() - mFadeBy, 0); + mPaint.setAlpha(alpha); + invalidate(); + if (alpha > 0) { + postDelayed(this, FADE_FRAME_MS); + } + } + }; + + public UnderlinePageIndicator(Context context) { + this(context, null); + } + + public UnderlinePageIndicator(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.vpiUnderlinePageIndicatorStyle); + } + + public UnderlinePageIndicator(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (isInEditMode()) return; + + final Resources res = getResources(); + + //Load defaults from resources + final boolean defaultFades = res.getBoolean(R.bool.default_underline_indicator_fades); + final int defaultFadeDelay = res.getInteger(R.integer.default_underline_indicator_fade_delay); + final int defaultFadeLength = res.getInteger(R.integer.default_underline_indicator_fade_length); + final int defaultSelectedColor = res.getColor(R.color.default_underline_indicator_selected_color); + + //Retrieve styles attributes + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UnderlinePageIndicator, defStyle, 0); + + setFades(a.getBoolean(R.styleable.UnderlinePageIndicator_fades, defaultFades)); + setSelectedColor(a.getColor(R.styleable.UnderlinePageIndicator_selectedColor, defaultSelectedColor)); + setFadeDelay(a.getInteger(R.styleable.UnderlinePageIndicator_fadeDelay, defaultFadeDelay)); + setFadeLength(a.getInteger(R.styleable.UnderlinePageIndicator_fadeLength, defaultFadeLength)); + + Drawable background = a.getDrawable(R.styleable.UnderlinePageIndicator_android_background); + if (background != null) { + setBackgroundDrawable(background); + } + + a.recycle(); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + } + + public boolean getFades() { + return mFades; + } + + public void setFades(boolean fades) { + if (fades != mFades) { + mFades = fades; + if (fades) { + post(mFadeRunnable); + } else { + removeCallbacks(mFadeRunnable); + mPaint.setAlpha(0xFF); + invalidate(); + } + } + } + + public int getFadeDelay() { + return mFadeDelay; + } + + public void setFadeDelay(int fadeDelay) { + mFadeDelay = fadeDelay; + } + + public int getFadeLength() { + return mFadeLength; + } + + public void setFadeLength(int fadeLength) { + mFadeLength = fadeLength; + mFadeBy = 0xFF / (mFadeLength / FADE_FRAME_MS); + } + + public int getSelectedColor() { + return mPaint.getColor(); + } + + public void setSelectedColor(int selectedColor) { + mPaint.setColor(selectedColor); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mViewPager == null) { + return; + } + final int count = mViewPager.getAdapter().getCount(); + if (count == 0) { + return; + } + + if (mCurrentPage >= count) { + setCurrentItem(count - 1); + return; + } + + final int paddingLeft = getPaddingLeft(); + final float pageWidth = (getWidth() - paddingLeft - getPaddingRight()) / (1f * count); + final float left = paddingLeft + pageWidth * (mCurrentPage + mPositionOffset); + final float right = left + pageWidth; + final float top = getPaddingTop(); + final float bottom = getHeight() - getPaddingBottom(); + canvas.drawRect(left, top, right, bottom, mPaint); + } + + public boolean onTouchEvent(MotionEvent ev) { + if (super.onTouchEvent(ev)) { + return true; + } + if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { + return false; + } + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mLastMotionX = ev.getX(); + break; + + case MotionEvent.ACTION_MOVE: { + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final float deltaX = x - mLastMotionX; + + if (!mIsDragging) { + if (Math.abs(deltaX) > mTouchSlop) { + mIsDragging = true; + } + } + + if (mIsDragging) { + mLastMotionX = x; + if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { + mViewPager.fakeDragBy(deltaX); + } + } + + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (!mIsDragging) { + final int count = mViewPager.getAdapter().getCount(); + final int width = getWidth(); + final float halfWidth = width / 2f; + final float sixthWidth = width / 6f; + + if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage - 1); + } + return true; + } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) { + if (action != MotionEvent.ACTION_CANCEL) { + mViewPager.setCurrentItem(mCurrentPage + 1); + } + return true; + } + } + + mIsDragging = false; + mActivePointerId = INVALID_POINTER; + if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); + break; + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionX = MotionEventCompat.getX(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + return true; + } + + @Override + public void setViewPager(ViewPager viewPager) { + if (mViewPager == viewPager) { + return; + } + if (mViewPager != null) { + //Clear us from the old pager. + mViewPager.setOnPageChangeListener(null); + } + if (viewPager.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + mViewPager = viewPager; + mViewPager.setOnPageChangeListener(this); + invalidate(); + post(new Runnable() { + @Override public void run() { + if (mFades) { + post(mFadeRunnable); + } + } + }); + } + + @Override + public void setViewPager(ViewPager view, int initialPosition) { + setViewPager(view); + setCurrentItem(initialPosition); + } + + @Override + public void setCurrentItem(int item) { + if (mViewPager == null) { + throw new IllegalStateException("ViewPager has not been bound."); + } + mViewPager.setCurrentItem(item); + mCurrentPage = item; + invalidate(); + } + + @Override + public void notifyDataSetChanged() { + invalidate(); + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mListener != null) { + mListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mCurrentPage = position; + mPositionOffset = positionOffset; + if (mFades) { + if (positionOffsetPixels > 0) { + removeCallbacks(mFadeRunnable); + mPaint.setAlpha(0xFF); + } else if (mScrollState != ViewPager.SCROLL_STATE_DRAGGING) { + postDelayed(mFadeRunnable, mFadeDelay); + } + } + invalidate(); + + if (mListener != null) { + mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mCurrentPage = position; + mPositionOffset = 0; + invalidate(); + mFadeRunnable.run(); + } + if (mListener != null) { + mListener.onPageSelected(position); + } + } + + @Override + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mListener = listener; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState)state; + super.onRestoreInstanceState(savedState.getSuperState()); + mCurrentPage = savedState.currentPage; + requestLayout(); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState savedState = new SavedState(superState); + savedState.currentPage = mCurrentPage; + return savedState; + } + + static class SavedState extends BaseSavedState { + int currentPage; + + public SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(currentPage); + } + + @SuppressWarnings("UnusedDeclaration") + public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +}
\ No newline at end of file |