├── LICENSE.md ├── README.md └── src ├── .gitignore ├── Droid ├── Assets │ └── AboutAssets.txt ├── MainActivity.cs ├── Properties │ ├── AndroidManifest.xml │ └── AssemblyInfo.cs ├── Renderers │ ├── CarouselLayoutRenderer.cs │ └── CustomNavigationRenderer.cs ├── Resources │ ├── AboutResources.txt │ ├── Resource.designer.cs │ ├── drawable-hdpi │ │ └── icon.png │ ├── drawable-xhdpi │ │ └── icon.png │ ├── drawable-xxhdpi │ │ └── icon.png │ └── drawable │ │ ├── icon.png │ │ ├── one.jpeg │ │ ├── pin.png │ │ ├── pip.png │ │ └── pip_selected.png ├── SwippableBottomTabView.Droid.csproj └── packages.config ├── SwippableBottomTabView.sln ├── SwippableBottomTabView ├── App.cs ├── Controls │ └── CarouselLayout.cs ├── DynamicTemplateLayout.cs ├── HomePage.cs ├── HomeView.cs ├── PagerIndicatorDots.cs ├── PagerIndicatorTabs.cs ├── SwippableBottomTabView.projitems ├── SwippableBottomTabView.shproj ├── SwitcherPage.cs ├── TabPageOne.cs ├── TabPageTwo.cs └── ViewModels │ ├── BaseViewModel.cs │ ├── ICarouselViewModel.cs │ ├── SwitcherPageViewModel.cs │ ├── TabOneViewModel.cs │ ├── TabTwoViewModel.cs │ └── TabbedPageViewModel.cs ├── iOS ├── AppDelegate.cs ├── Entitlements.plist ├── ITunesArtwork ├── ITunesArtwork@2x ├── Info.plist ├── Main.cs ├── Renderers │ └── CarouselLayoutRenderer.cs ├── Resources │ ├── Default-568h@2x.png │ ├── Default-Portrait.png │ ├── Default-Portrait@2x.png │ ├── Default.png │ ├── Default@2x.png │ ├── Icon-60@2x.png │ ├── Icon-60@3x.png │ ├── Icon-76.png │ ├── Icon-76@2x.png │ ├── Icon-Small-40.png │ ├── Icon-Small-40@2x.png │ ├── Icon-Small-40@3x.png │ ├── Icon-Small.png │ ├── Icon-Small@2x.png │ ├── Icon-Small@3x.png │ ├── LaunchScreen.storyboard │ ├── pin.png │ └── pin@2x.png ├── SwippableBottomTabView.iOS.csproj └── packages.config └── repositories.config /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Hailin Shu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XamarinForms.SwippableBottomTabView 2 | This is a Tab control based off Chris Riesgo's excellent Carousel View 3 | 4 | https://github.com/chrisriesgo/xamarin-forms-carouselview 5 | https://gist.github.com/chrisriesgo/2e9c88fa346e57745e73 6 | 7 | with his point of direction of how to implement a Tab View 8 | 9 | One caveat: this code is not very reusable, meaning it's not something you can drop into your project and it will work right away. You still need to do some work to integrate it into your project. 10 | Also, there is no transiton of the tabs when swiping like View Indicator in Android. The tab will be changed after the swipe is in place. 11 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | #Autosave files 2 | *~ 3 | 4 | #build 5 | [Oo]bj/ 6 | [Bb]in/ 7 | packages/ 8 | TestResults/ 9 | 10 | # globs 11 | Makefile.in 12 | *.DS_Store 13 | *.sln.cache 14 | *.suo 15 | *.cache 16 | *.pidb 17 | *.userprefs 18 | *.usertasks 19 | config.log 20 | config.make 21 | config.status 22 | aclocal.m4 23 | install-sh 24 | autom4te.cache/ 25 | *.user 26 | *.tar.gz 27 | tarballs/ 28 | test-results/ 29 | Thumbs.db 30 | 31 | #Mac bundle stuff 32 | *.dmg 33 | *.app 34 | 35 | #resharper 36 | *_Resharper.* 37 | *.Resharper 38 | 39 | #dotCover 40 | *.dotCover 41 | -------------------------------------------------------------------------------- /src/Droid/Assets/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories) and given a Build Action of "AndroidAsset". 3 | 4 | These files will be deployed with your package and will be accessible using Android's 5 | AssetManager, like this: 6 | 7 | public class ReadAsset : Activity 8 | { 9 | protected override void OnCreate (Bundle bundle) 10 | { 11 | base.OnCreate (bundle); 12 | 13 | InputStream input = Assets.Open ("my_asset.txt"); 14 | } 15 | } 16 | 17 | Additionally, some Android functions will automatically load asset files: 18 | 19 | Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); 20 | -------------------------------------------------------------------------------- /src/Droid/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | 5 | namespace SwippableBottomTabView.Droid 6 | { 7 | [Activity(Label = "TabView Demo", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] 8 | public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity 9 | { 10 | protected override void OnCreate(Bundle bundle) 11 | { 12 | base.OnCreate(bundle); 13 | 14 | global::Xamarin.Forms.Forms.Init(this, bundle); 15 | 16 | LoadApplication(new App()); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Droid/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Droid/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | // Information about this assembly is defined by the following attributes. 4 | // Change them to the values specific to your project. 5 | 6 | [assembly: AssemblyTitle("SwippableBottomTabView.Droid")] 7 | [assembly: AssemblyDescription("")] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("")] 11 | [assembly: AssemblyCopyright("chrisriesgo")] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 16 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 17 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 18 | 19 | [assembly: AssemblyVersion("1.0.0")] 20 | 21 | // The following attributes are used to specify the signing key for the assembly, 22 | // if desired. See the Mono documentation for more information about signing. 23 | 24 | //[assembly: AssemblyDelaySign(false)] 25 | //[assembly: AssemblyKeyFile("")] -------------------------------------------------------------------------------- /src/Droid/Renderers/CarouselLayoutRenderer.cs: -------------------------------------------------------------------------------- 1 | using Android.Graphics; 2 | using Android.Support.V4.View; 3 | using Android.Views; 4 | using Android.Widget; 5 | using Java.Lang; 6 | using SwippableBottomTabView.Controls; 7 | using SwippableBottomTabView.Droid.Renderers; 8 | using System.ComponentModel; 9 | using System.Reflection; 10 | using System.Timers; 11 | using Xamarin.Forms; 12 | using Xamarin.Forms.Platform.Android; 13 | 14 | [assembly: ExportRenderer(typeof(CarouselLayout), typeof(CarouselLayoutRenderer))] 15 | 16 | namespace SwippableBottomTabView.Droid.Renderers 17 | { 18 | public class CarouselLayoutRenderer : ScrollViewRenderer 19 | { 20 | private int _prevScrollX; 21 | private int _deltaX; 22 | private bool _motionDown; 23 | private Timer _deltaXResetTimer; 24 | private Timer _scrollStopTimer; 25 | private HorizontalScrollView _scrollView; 26 | 27 | protected override void OnElementChanged(VisualElementChangedEventArgs e) 28 | { 29 | var a = new ViewPager(this.Context); 30 | var b = a.ClipBounds; 31 | base.OnElementChanged(e); 32 | if (e.NewElement == null) return; 33 | 34 | _deltaXResetTimer = new Timer(100) { AutoReset = false }; 35 | _deltaXResetTimer.Elapsed += (object sender, ElapsedEventArgs args) => _deltaX = 0; 36 | 37 | _scrollStopTimer = new Timer(200) { AutoReset = false }; 38 | _scrollStopTimer.Elapsed += (object sender, ElapsedEventArgs args2) => UpdateSelectedIndex(); 39 | 40 | e.NewElement.PropertyChanged += ElementPropertyChanged; 41 | } 42 | 43 | private void ElementPropertyChanged(object sender, PropertyChangedEventArgs e) 44 | { 45 | if (e.PropertyName == "Renderer") 46 | { 47 | _scrollView = (HorizontalScrollView)typeof(ScrollViewRenderer) 48 | .GetField("hScrollView", BindingFlags.NonPublic | BindingFlags.Instance) 49 | .GetValue(this); 50 | 51 | _scrollView.HorizontalScrollBarEnabled = false; 52 | _scrollView.Touch += HScrollViewTouch; 53 | } 54 | if (e.PropertyName == CarouselLayout.SelectedIndexProperty.PropertyName && !_motionDown) 55 | { 56 | ScrollToIndex(((CarouselLayout)this.Element).SelectedIndex); 57 | } 58 | } 59 | 60 | private void HScrollViewTouch(object sender, TouchEventArgs e) 61 | { 62 | e.Handled = false; 63 | 64 | switch (e.Event.Action) 65 | { 66 | case MotionEventActions.Move: 67 | _deltaXResetTimer.Stop(); 68 | _deltaX = _scrollView.ScrollX - _prevScrollX; 69 | _prevScrollX = _scrollView.ScrollX; 70 | 71 | UpdateSelectedIndex(); 72 | 73 | _deltaXResetTimer.Start(); 74 | break; 75 | 76 | case MotionEventActions.Down: 77 | _motionDown = true; 78 | _scrollStopTimer.Stop(); 79 | break; 80 | 81 | case MotionEventActions.Up: 82 | _motionDown = false; 83 | SnapScroll(); 84 | _scrollStopTimer.Start(); 85 | break; 86 | } 87 | } 88 | 89 | private void UpdateSelectedIndex() 90 | { 91 | var center = _scrollView.ScrollX + (_scrollView.Width / 2); 92 | var carouselLayout = (CarouselLayout)this.Element; 93 | carouselLayout.SelectedIndex = (center / _scrollView.Width); 94 | } 95 | 96 | private void SnapScroll() 97 | { 98 | var roughIndex = (float)_scrollView.ScrollX / _scrollView.Width; 99 | 100 | var targetIndex = 101 | _deltaX < 0 ? Math.Floor(roughIndex) 102 | : _deltaX > 0 ? Math.Ceil(roughIndex) 103 | : Math.Round(roughIndex); 104 | 105 | ScrollToIndex((int)targetIndex); 106 | } 107 | 108 | private void ScrollToIndex(int targetIndex) 109 | { 110 | var targetX = targetIndex * _scrollView.Width; 111 | _scrollView.Post(new Runnable(() => 112 | { 113 | _scrollView.SmoothScrollTo(targetX, 0); 114 | })); 115 | } 116 | 117 | private bool _initialized = false; 118 | 119 | public override void Draw(Canvas canvas) 120 | { 121 | base.Draw(canvas); 122 | if (_initialized) return; 123 | _initialized = true; 124 | var carouselLayout = (CarouselLayout)this.Element; 125 | _scrollView.ScrollTo(carouselLayout.SelectedIndex * Width, 0); 126 | } 127 | 128 | protected override void OnSizeChanged(int width, int height, int oldWidth, int oldHeight) 129 | { 130 | // if the screen width changed 131 | if (_initialized && (width != oldWidth)) 132 | { 133 | // signal that on next Draw we will need ScrollView adjustment 134 | _initialized = false; 135 | } 136 | base.OnSizeChanged(width, height, oldWidth, oldHeight); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/Droid/Renderers/CustomNavigationRenderer.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Graphics.Drawables; 3 | using Xamarin.Forms; 4 | using Xamarin.Forms.Platform.Android; 5 | using CustomLayouts.Droid.Renderers; 6 | 7 | [assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomNavigationRenderer))] 8 | 9 | namespace CustomLayouts.Droid.Renderers 10 | { 11 | public class CustomNavigationRenderer : NavigationRenderer 12 | { 13 | protected override void OnElementChanged(ElementChangedEventArgs e) 14 | { 15 | base.OnElementChanged (e); 16 | 17 | RemoveAppIconFromActionBar (); 18 | } 19 | 20 | void RemoveAppIconFromActionBar() 21 | { 22 | // http://stackoverflow.com/questions/14606294/remove-icon-logo-from-action-bar-on-android 23 | var actionBar = ((Activity)Context).ActionBar; 24 | actionBar.SetIcon (new ColorDrawable(Color.Transparent.ToAndroid())); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Droid/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.axml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable/ 12 | icon.png 13 | 14 | layout/ 15 | main.axml 16 | 17 | values/ 18 | strings.xml 19 | 20 | In order to get the build system to recognize Android resources, set the build action to 21 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 22 | instead operate on resource IDs. When you compile an Android application that uses resources, 23 | the build system will package the resources for distribution and generate a class called "R" 24 | (this is an Android convention) that contains the tokens for each one of the resources 25 | included. For example, for the above Resources layout, this is what the R class would expose: 26 | 27 | public class R { 28 | public class drawable { 29 | public const int icon = 0x123; 30 | } 31 | 32 | public class layout { 33 | public const int main = 0x456; 34 | } 35 | 36 | public class strings { 37 | public const int first_string = 0xabc; 38 | public const int second_string = 0xbcd; 39 | } 40 | } 41 | 42 | You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main 43 | to reference the layout/main.axml file, or R.strings.first_string to reference the first 44 | string in the dictionary file values/strings.xml. 45 | -------------------------------------------------------------------------------- /src/Droid/Resources/Resource.designer.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 1591 2 | //------------------------------------------------------------------------------ 3 | // 4 | // This code was generated by a tool. 5 | // Runtime Version:4.0.30319.34209 6 | // 7 | // Changes to this file may cause incorrect behavior and will be lost if 8 | // the code is regenerated. 9 | // 10 | //------------------------------------------------------------------------------ 11 | 12 | [assembly: global::Android.Runtime.ResourceDesignerAttribute("SwippableBottomTabView.Droid.Resource", IsApplication=true)] 13 | 14 | namespace SwippableBottomTabView.Droid 15 | { 16 | 17 | 18 | [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] 19 | public partial class Resource 20 | { 21 | 22 | static Resource() 23 | { 24 | global::Android.Runtime.ResourceIdManager.UpdateIdValues(); 25 | } 26 | 27 | public static void UpdateIdValues() 28 | { 29 | global::Xamarin.Forms.Platform.Resource.String.ApplicationName = global::SwippableBottomTabView.Droid.Resource.String.ApplicationName; 30 | global::Xamarin.Forms.Platform.Resource.String.Hello = global::SwippableBottomTabView.Droid.Resource.String.Hello; 31 | } 32 | 33 | public partial class Attribute 34 | { 35 | 36 | static Attribute() 37 | { 38 | global::Android.Runtime.ResourceIdManager.UpdateIdValues(); 39 | } 40 | 41 | private Attribute() 42 | { 43 | } 44 | } 45 | 46 | public partial class Drawable 47 | { 48 | 49 | // aapt resource value: 0x7f020000 50 | public const int icon = 2130837504; 51 | 52 | // aapt resource value: 0x7f020001 53 | public const int pin = 2130837505; 54 | 55 | static Drawable() 56 | { 57 | global::Android.Runtime.ResourceIdManager.UpdateIdValues(); 58 | } 59 | 60 | private Drawable() 61 | { 62 | } 63 | } 64 | 65 | public partial class String 66 | { 67 | 68 | // aapt resource value: 0x7f030001 69 | public const int ApplicationName = 2130903041; 70 | 71 | // aapt resource value: 0x7f030000 72 | public const int Hello = 2130903040; 73 | 74 | static String() 75 | { 76 | global::Android.Runtime.ResourceIdManager.UpdateIdValues(); 77 | } 78 | 79 | private String() 80 | { 81 | } 82 | } 83 | } 84 | } 85 | #pragma warning restore 1591 86 | -------------------------------------------------------------------------------- /src/Droid/Resources/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /src/Droid/Resources/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /src/Droid/Resources/drawable-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable-xxhdpi/icon.png -------------------------------------------------------------------------------- /src/Droid/Resources/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable/icon.png -------------------------------------------------------------------------------- /src/Droid/Resources/drawable/one.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable/one.jpeg -------------------------------------------------------------------------------- /src/Droid/Resources/drawable/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable/pin.png -------------------------------------------------------------------------------- /src/Droid/Resources/drawable/pip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable/pip.png -------------------------------------------------------------------------------- /src/Droid/Resources/drawable/pip_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imgen/XamarinForms.SwippableBottomTabView/de205a80f4af5662b22d50486e33f435f73d3eb1/src/Droid/Resources/drawable/pip_selected.png -------------------------------------------------------------------------------- /src/Droid/SwippableBottomTabView.Droid.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 7 | {26422919-3EAD-440B-B516-E41B10D2AC56} 8 | Library 9 | SwippableBottomTabView.Droid 10 | Assets 11 | Resources 12 | Properties\AndroidManifest.xml 13 | Resource 14 | Resources\Resource.designer.cs 15 | True 16 | True 17 | SwippableBottomTabView.Droid 18 | v5.0 19 | 20 | 21 | true 22 | full 23 | false 24 | bin\Debug 25 | DEBUG; 26 | prompt 27 | 4 28 | None 29 | false 30 | True 31 | 32 | False 33 | False 34 | False 35 | armeabi,armeabi-v7a,x86 36 | 37 | 38 | Xamarin 39 | False 40 | True 41 | 42 | 43 | full 44 | true 45 | bin\Release 46 | prompt 47 | 4 48 | false 49 | false 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ..\packages\Xamarin.Forms.1.4.3.6374\lib\MonoAndroid10\Xamarin.Forms.Platform.Android.dll 58 | 59 | 60 | ..\packages\Xamarin.Forms.1.4.3.6374\lib\MonoAndroid10\FormsViewGroup.dll 61 | 62 | 63 | ..\packages\Xamarin.Forms.1.4.3.6374\lib\MonoAndroid10\Xamarin.Forms.Core.dll 64 | 65 | 66 | ..\packages\Xamarin.Forms.1.4.3.6374\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll 67 | 68 | 69 | ..\packages\Xamarin.Forms.1.4.3.6374\lib\MonoAndroid10\Xamarin.Forms.Platform.dll 70 | 71 | 72 | ..\packages\Xamarin.Android.Support.v4.22.2.0.0\lib\MonoAndroid403\Xamarin.Android.Support.v4.dll 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/Droid/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/SwippableBottomTabView.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SwippableBottomTabView", "SwippableBottomTabView\SwippableBottomTabView.shproj", "{77990895-4247-4E5A-9474-E3456B62A28D}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SwippableBottomTabView.iOS", "iOS\SwippableBottomTabView.iOS.csproj", "{E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SwippableBottomTabView.Droid", "Droid\SwippableBottomTabView.Droid.csproj", "{26422919-3EAD-440B-B516-E41B10D2AC56}" 11 | EndProject 12 | Global 13 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 14 | SwippableBottomTabView\SwippableBottomTabView.projitems*{77990895-4247-4e5a-9474-e3456b62a28d}*SharedItemsImports = 13 15 | SwippableBottomTabView\SwippableBottomTabView.projitems*{e97c5e1f-83a0-4cd5-b1e1-91897b8c9f86}*SharedItemsImports = 4 16 | SwippableBottomTabView\SwippableBottomTabView.projitems*{26422919-3ead-440b-b516-e41b10d2ac56}*SharedItemsImports = 4 17 | EndGlobalSection 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Debug|iPhone = Debug|iPhone 21 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 22 | Release|Any CPU = Release|Any CPU 23 | Release|iPhone = Release|iPhone 24 | Release|iPhoneSimulator = Release|iPhoneSimulator 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator 28 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator 29 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Debug|iPhone.ActiveCfg = Debug|iPhone 30 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Debug|iPhone.Build.0 = Debug|iPhone 31 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator 32 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator 33 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Release|Any CPU.ActiveCfg = Release|iPhone 34 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Release|Any CPU.Build.0 = Release|iPhone 35 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Release|iPhone.ActiveCfg = Release|iPhone 36 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Release|iPhone.Build.0 = Release|iPhone 37 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator 38 | {E97C5E1F-83A0-4CD5-B1E1-91897B8C9F86}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator 39 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 42 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Debug|iPhone.ActiveCfg = Debug|Any CPU 43 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Debug|iPhone.Build.0 = Debug|Any CPU 44 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 45 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 46 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Release|iPhone.ActiveCfg = Release|Any CPU 49 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Release|iPhone.Build.0 = Release|Any CPU 50 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 51 | {26422919-3EAD-440B-B516-E41B10D2AC56}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /src/SwippableBottomTabView/App.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | 3 | namespace SwippableBottomTabView 4 | { 5 | public class App : Application 6 | { 7 | public App() 8 | { 9 | // The root page of your application 10 | var navPage = new NavigationPage(new SwitcherPage()); 11 | navPage.Icon = null; 12 | MainPage = navPage; 13 | } 14 | 15 | protected override void OnStart() 16 | { 17 | // Handle when your app starts 18 | } 19 | 20 | protected override void OnSleep() 21 | { 22 | // Handle when your app sleeps 23 | } 24 | 25 | protected override void OnResume() 26 | { 27 | // Handle when your app resumes 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/SwippableBottomTabView/Controls/CarouselLayout.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Timers; 5 | using Xamarin.Forms; 6 | 7 | namespace SwippableBottomTabView.Controls 8 | { 9 | public class CarouselLayout : ScrollView 10 | { 11 | public enum IndicatorStyleEnum 12 | { 13 | None, 14 | Dots, 15 | Tabs 16 | } 17 | 18 | private readonly StackLayout _stack; 19 | private Timer _selectedItemTimer; 20 | 21 | private int _selectedIndex; 22 | 23 | public CarouselLayout() 24 | { 25 | Orientation = ScrollOrientation.Horizontal; 26 | 27 | _stack = new StackLayout 28 | { 29 | Orientation = StackOrientation.Horizontal, 30 | Spacing = 0 31 | }; 32 | 33 | Content = _stack; 34 | 35 | _selectedItemTimer = new Timer 36 | { 37 | AutoReset = false, 38 | Interval = 300 39 | }; 40 | 41 | _selectedItemTimer.Elapsed += SelectedItemTimerElapsed; 42 | } 43 | 44 | public IndicatorStyleEnum IndicatorStyle { get; set; } 45 | 46 | public IList Children 47 | { 48 | get 49 | { 50 | return _stack.Children; 51 | } 52 | } 53 | 54 | private bool _layingOutChildren; 55 | 56 | protected override void LayoutChildren(double x, double y, double width, double height) 57 | { 58 | base.LayoutChildren(x, y, width, height); 59 | if (_layingOutChildren) return; 60 | 61 | _layingOutChildren = true; 62 | foreach (var child in Children) child.WidthRequest = width; 63 | _layingOutChildren = false; 64 | } 65 | 66 | public static readonly BindableProperty SelectedIndexProperty = 67 | BindableProperty.Create( 68 | carousel => carousel.SelectedIndex, 69 | 0, 70 | BindingMode.TwoWay, 71 | propertyChanged: (bindable, oldValue, newValue) => 72 | { 73 | ((CarouselLayout)bindable).UpdateSelectedItem(); 74 | } 75 | ); 76 | 77 | public int SelectedIndex 78 | { 79 | get 80 | { 81 | return (int)GetValue(SelectedIndexProperty); 82 | } 83 | set 84 | { 85 | SetValue(SelectedIndexProperty, value); 86 | } 87 | } 88 | 89 | private void UpdateSelectedItem() 90 | { 91 | _selectedItemTimer.Stop(); 92 | _selectedItemTimer.Start(); 93 | } 94 | 95 | private void SelectedItemTimerElapsed(object sender, ElapsedEventArgs e) 96 | { 97 | SelectedItem = SelectedIndex > -1 ? Children[SelectedIndex].BindingContext : null; 98 | } 99 | 100 | public static readonly BindableProperty ItemsSourceProperty = 101 | BindableProperty.Create( 102 | view => view.ItemsSource, 103 | null, 104 | propertyChanging: (bindableObject, oldValue, newValue) => 105 | { 106 | ((CarouselLayout)bindableObject).ItemsSourceChanging(); 107 | }, 108 | propertyChanged: (bindableObject, oldValue, newValue) => 109 | { 110 | ((CarouselLayout)bindableObject).ItemsSourceChanged(); 111 | } 112 | ); 113 | 114 | public IList ItemsSource 115 | { 116 | get 117 | { 118 | return (IList)GetValue(ItemsSourceProperty); 119 | } 120 | set 121 | { 122 | SetValue(ItemsSourceProperty, value); 123 | } 124 | } 125 | 126 | private void ItemsSourceChanging() 127 | { 128 | if (ItemsSource == null) return; 129 | _selectedIndex = ItemsSource.IndexOf(SelectedItem); 130 | } 131 | 132 | private void ItemsSourceChanged() 133 | { 134 | _stack.Children.Clear(); 135 | foreach (var item in ItemsSource) 136 | { 137 | var content = (DynamicTemplateLayout)ItemTemplate.CreateContent(); 138 | content.BindingContext = item; 139 | _stack.Children.Add(content.View); 140 | } 141 | 142 | if (_selectedIndex >= 0) SelectedIndex = _selectedIndex; 143 | } 144 | 145 | public DataTemplate ItemTemplate 146 | { 147 | get; 148 | set; 149 | } 150 | 151 | public static readonly BindableProperty SelectedItemProperty = 152 | BindableProperty.Create( 153 | view => view.SelectedItem, 154 | null, 155 | BindingMode.TwoWay, 156 | propertyChanged: (bindable, oldValue, newValue) => 157 | { 158 | ((CarouselLayout)bindable).UpdateSelectedIndex(); 159 | } 160 | ); 161 | 162 | public object SelectedItem 163 | { 164 | get 165 | { 166 | return GetValue(SelectedItemProperty); 167 | } 168 | set 169 | { 170 | SetValue(SelectedItemProperty, value); 171 | } 172 | } 173 | 174 | private void UpdateSelectedIndex() 175 | { 176 | if (SelectedItem == BindingContext) return; 177 | 178 | SelectedIndex = Children 179 | .Select(c => c.BindingContext) 180 | .ToList() 181 | .IndexOf(SelectedItem); 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /src/SwippableBottomTabView/DynamicTemplateLayout.cs: -------------------------------------------------------------------------------- 1 | using SwippableBottomTabView.ViewModels; 2 | using Xamarin.Forms; 3 | 4 | namespace SwippableBottomTabView 5 | { 6 | public class DynamicTemplateLayout : ViewCell 7 | { 8 | protected override void OnBindingContextChanged() 9 | { 10 | base.OnBindingContextChanged(); 11 | 12 | var vm = BindingContext as ICarouselViewModel; 13 | var page = vm.View; 14 | page.BindingContext = vm; 15 | View = page; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/SwippableBottomTabView/HomePage.cs: -------------------------------------------------------------------------------- 1 | using SwippableBottomTabView.Controls; 2 | using SwippableBottomTabView.ViewModels; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using Xamarin.Forms; 7 | 8 | namespace SwippableBottomTabView 9 | { 10 | public class HomePage : ContentPage 11 | { 12 | private View _tabs; 13 | 14 | private RelativeLayout _relativeLayout; 15 | 16 | private CarouselLayout.IndicatorStyleEnum _indicatorStyle; 17 | 18 | private TabbedPageViewModel _viewModel; 19 | 20 | public HomePage(CarouselLayout.IndicatorStyleEnum indicatorStyle) 21 | { 22 | _indicatorStyle = indicatorStyle; 23 | 24 | _viewModel = new TabbedPageViewModel(); 25 | BindingContext = _viewModel; 26 | 27 | BackgroundColor = Color.Black; 28 | 29 | Title = _indicatorStyle.ToString(); 30 | 31 | _relativeLayout = new RelativeLayout 32 | { 33 | HorizontalOptions = LayoutOptions.FillAndExpand, 34 | VerticalOptions = LayoutOptions.FillAndExpand 35 | }; 36 | 37 | var pagesCarousel = CreatePagesCarousel(); 38 | var dots = CreatePagerIndicatorContainer(); 39 | _tabs = CreateTabs(); 40 | 41 | switch (pagesCarousel.IndicatorStyle) 42 | { 43 | case CarouselLayout.IndicatorStyleEnum.Dots: 44 | _relativeLayout.Children.Add(pagesCarousel, 45 | Constraint.RelativeToParent((parent) => { return parent.X; }), 46 | Constraint.RelativeToParent((parent) => { return parent.Y; }), 47 | Constraint.RelativeToParent((parent) => { return parent.Width; }), 48 | Constraint.RelativeToParent((parent) => { return parent.Height / 2; }) 49 | ); 50 | 51 | _relativeLayout.Children.Add(dots, 52 | Constraint.Constant(0), 53 | Constraint.RelativeToView(pagesCarousel, 54 | (parent, sibling) => { return sibling.Height - 18; }), 55 | Constraint.RelativeToParent(parent => parent.Width), 56 | Constraint.Constant(18) 57 | ); 58 | break; 59 | 60 | case CarouselLayout.IndicatorStyleEnum.Tabs: 61 | var tabsHeight = 50; 62 | _relativeLayout.Children.Add(_tabs, 63 | Constraint.Constant(0), 64 | Constraint.RelativeToParent((parent) => { return parent.Height - tabsHeight; }), 65 | Constraint.RelativeToParent(parent => parent.Width), 66 | Constraint.Constant(tabsHeight) 67 | ); 68 | 69 | _relativeLayout.Children.Add(pagesCarousel, 70 | Constraint.RelativeToParent((parent) => { return parent.X; }), 71 | Constraint.RelativeToParent((parent) => { return parent.Y; }), 72 | Constraint.RelativeToParent((parent) => { return parent.Width; }), 73 | Constraint.RelativeToView(_tabs, (parent, sibling) => { return parent.Height - (sibling.Height); }) 74 | ); 75 | break; 76 | 77 | default: 78 | _relativeLayout.Children.Add(pagesCarousel, 79 | Constraint.RelativeToParent((parent) => { return parent.X; }), 80 | Constraint.RelativeToParent((parent) => { return parent.Y; }), 81 | Constraint.RelativeToParent((parent) => { return parent.Width; }), 82 | Constraint.RelativeToParent((parent) => { return parent.Height; }) 83 | ); 84 | break; 85 | } 86 | 87 | Content = _relativeLayout; 88 | } 89 | 90 | private CarouselLayout CreatePagesCarousel() 91 | { 92 | var carousel = new CarouselLayout 93 | { 94 | HorizontalOptions = LayoutOptions.FillAndExpand, 95 | VerticalOptions = LayoutOptions.FillAndExpand, 96 | IndicatorStyle = _indicatorStyle, 97 | ItemTemplate = new DataTemplate(typeof(DynamicTemplateLayout)) 98 | }; 99 | carousel.SetBinding(CarouselLayout.ItemsSourceProperty, "Pages"); 100 | carousel.SetBinding(CarouselLayout.SelectedItemProperty, "CurrentPage", BindingMode.TwoWay); 101 | 102 | return carousel; 103 | } 104 | 105 | private View CreatePagerIndicatorContainer() 106 | { 107 | return new StackLayout 108 | { 109 | Children = { CreatePagerIndicators() } 110 | }; 111 | } 112 | 113 | private View CreatePagerIndicators() 114 | { 115 | var pagerIndicator = new PagerIndicatorDots() { DotSize = 5, DotColor = Color.Black }; 116 | pagerIndicator.SetBinding(PagerIndicatorDots.ItemsSourceProperty, "Pages"); 117 | pagerIndicator.SetBinding(PagerIndicatorDots.SelectedItemProperty, "CurrentPage"); 118 | return pagerIndicator; 119 | } 120 | 121 | private View CreateTabsContainer() 122 | { 123 | return new StackLayout 124 | { 125 | Children = { CreateTabs() } 126 | }; 127 | } 128 | 129 | private View CreateTabs() 130 | { 131 | var pagerIndicator = new PagerIndicatorTabs() { HorizontalOptions = LayoutOptions.CenterAndExpand }; 132 | pagerIndicator.RowDefinitions.Add(new RowDefinition() { Height = 50 }); 133 | pagerIndicator.SetBinding(PagerIndicatorTabs.ColumnDefinitionsProperty, "Pages", BindingMode.Default, new SpacingConverter()); 134 | pagerIndicator.SetBinding(PagerIndicatorTabs.ItemsSourceProperty, "Pages"); 135 | pagerIndicator.SetBinding(PagerIndicatorTabs.SelectedItemProperty, "CurrentPage"); 136 | 137 | return pagerIndicator; 138 | } 139 | } 140 | 141 | public class SpacingConverter : IValueConverter 142 | { 143 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 144 | { 145 | var items = value as IEnumerable; 146 | 147 | var collection = new ColumnDefinitionCollection(); 148 | foreach (var item in items) 149 | { 150 | collection.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); 151 | } 152 | return collection; 153 | } 154 | 155 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 156 | { 157 | throw new NotImplementedException(); 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /src/SwippableBottomTabView/HomeView.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | 3 | namespace SwippableBottomTabView 4 | { 5 | public class HomeView : ContentView 6 | { 7 | public HomeView() 8 | { 9 | BackgroundColor = Color.White; 10 | 11 | var label = new Label 12 | { 13 | XAlign = TextAlignment.Center, 14 | TextColor = Color.Black 15 | }; 16 | 17 | label.SetBinding(Label.TextProperty, "Title"); 18 | this.SetBinding(ContentView.BackgroundColorProperty, "Background"); 19 | 20 | Content = new StackLayout 21 | { 22 | VerticalOptions = LayoutOptions.CenterAndExpand, 23 | Children = { 24 | label 25 | } 26 | }; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/SwippableBottomTabView/PagerIndicatorDots.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using Xamarin.Forms; 5 | 6 | namespace SwippableBottomTabView 7 | { 8 | public interface ITabProvider 9 | { 10 | ImageSource ImageSource { get; set; } 11 | } 12 | 13 | public class PagerIndicatorDots : StackLayout 14 | { 15 | private int _dotCount = 1; 16 | private int _selectedIndex; 17 | 18 | public Color DotColor { get; set; } 19 | 20 | public double DotSize { get; set; } 21 | 22 | public PagerIndicatorDots() 23 | { 24 | HorizontalOptions = LayoutOptions.CenterAndExpand; 25 | VerticalOptions = LayoutOptions.Center; 26 | Orientation = StackOrientation.Horizontal; 27 | DotColor = Color.Black; 28 | } 29 | 30 | private void CreateDot() 31 | { 32 | //Make one button and add it to the dotLayout 33 | var dot = new Button 34 | { 35 | BorderRadius = Convert.ToInt32(DotSize / 2), 36 | HeightRequest = DotSize, 37 | WidthRequest = DotSize, 38 | BackgroundColor = DotColor 39 | }; 40 | Children.Add(dot); 41 | } 42 | 43 | private void CreateTabs() 44 | { 45 | foreach (var item in ItemsSource) 46 | { 47 | var tab = item as ITabProvider; 48 | var image = new Image 49 | { 50 | HeightRequest = 42, 51 | WidthRequest = 42, 52 | BackgroundColor = DotColor, 53 | Source = tab.ImageSource, 54 | }; 55 | Children.Add(image); 56 | } 57 | } 58 | 59 | public static BindableProperty ItemsSourceProperty = 60 | BindableProperty.Create( 61 | pi => pi.ItemsSource, 62 | null, 63 | BindingMode.OneWay, 64 | propertyChanging: (bindable, oldValue, newValue) => 65 | { 66 | ((PagerIndicatorDots)bindable).ItemsSourceChanging(); 67 | }, 68 | propertyChanged: (bindable, oldValue, newValue) => 69 | { 70 | ((PagerIndicatorDots)bindable).ItemsSourceChanged(); 71 | } 72 | ); 73 | 74 | public IList ItemsSource 75 | { 76 | get 77 | { 78 | return (IList)GetValue(ItemsSourceProperty); 79 | } 80 | set 81 | { 82 | SetValue(ItemsSourceProperty, value); 83 | } 84 | } 85 | 86 | public static BindableProperty SelectedItemProperty = 87 | BindableProperty.Create( 88 | pi => pi.SelectedItem, 89 | null, 90 | BindingMode.TwoWay, 91 | propertyChanged: (bindable, oldValue, newValue) => 92 | { 93 | ((PagerIndicatorDots)bindable).SelectedItemChanged(); 94 | }); 95 | 96 | public object SelectedItem 97 | { 98 | get 99 | { 100 | return GetValue(SelectedItemProperty); 101 | } 102 | set 103 | { 104 | SetValue(SelectedItemProperty, value); 105 | } 106 | } 107 | 108 | private void ItemsSourceChanging() 109 | { 110 | if (ItemsSource != null) 111 | _selectedIndex = ItemsSource.IndexOf(SelectedItem); 112 | } 113 | 114 | private void ItemsSourceChanged() 115 | { 116 | if (ItemsSource == null) return; 117 | 118 | // Dots ************************************* 119 | var countDelta = ItemsSource.Count - Children.Count; 120 | 121 | if (countDelta > 0) 122 | { 123 | for (var i = 0; i < countDelta; i++) 124 | { 125 | CreateDot(); 126 | } 127 | } 128 | else if (countDelta < 0) 129 | { 130 | for (var i = 0; i < -countDelta; i++) 131 | { 132 | Children.RemoveAt(0); 133 | } 134 | } 135 | //******************************************* 136 | } 137 | 138 | private void SelectedItemChanged() 139 | { 140 | var selectedIndex = ItemsSource.IndexOf(SelectedItem); 141 | var pagerIndicators = Children.Cast