背景簡(jiǎn)述
自動(dòng)輪播視圖(CarouselView)在現(xiàn)在App中的地位不言而喻,絕大多數(shù)的App中都有類似的視圖,無論是WebApp還是Native App。在安卓、iOS以及Windows(UWP)開發(fā)中,有一些控件可以很方便的來實(shí)現(xiàn)類似的效果。
ViewPager(安卓)
UIScrollView(iOS)
FlipView(UWP)
Xamarin.Forms怎么實(shí)現(xiàn)自動(dòng)輪播視圖呢?
Xamarin.Forms有自己的一套布局系統(tǒng),結(jié)合各平臺(tái)特性,也可以實(shí)現(xiàn)一個(gè)比較好的自動(dòng)輪播視圖。
上次介紹我實(shí)現(xiàn)的一個(gè)多頁面水平切換布局中,提到我使用了一個(gè)叫做ViewPanel
的自定義布局,他與自動(dòng)輪播視圖相比,只是缺少了無線滾動(dòng)和自動(dòng)輪播,這次也以這個(gè)布局為基礎(chǔ),來實(shí)現(xiàn)自動(dòng)輪播視圖。
核心依然是ViewPanel
在各個(gè)平臺(tái)中的具體實(shí)現(xiàn):
Portable:
...public static readonly BindableProperty ChildrenProperty = BindableProperty.Create("Children", typeof(IList), typeof(ViewPanel), propertyChanged: OnChildrenChanged);public IList Children { get { return (IList)this.GetValue(ChildrenProperty); } set { SetValue(ChildrenProperty, value); } } ...
依賴屬性Children
是一個(gè)集合類型,它用來存儲(chǔ)需要在ViewPanel
中顯示的視圖,一般子視圖的都從Xamarin.Forms.View
派生或者是他本身
其次,ViewPanel
能交互,需要實(shí)現(xiàn)一個(gè)事件,一個(gè)方法
event EventHandler SelectChanged
:當(dāng)ViewPanel
中顯示的元素改變時(shí)提供通知,并且提供OnSelectChanged()
來觸發(fā)該事件
*void select()
:用于設(shè)置ViewPanel
需要顯示的子視圖(實(shí)際Select
會(huì)是一個(gè)委托,因?yàn)?code style="margin: 1px 5px; padding: 0px 5px !important; line-height: 1.8; vertical-align: middle; display: inline-block; font-family: "Courier New", sans-serif !important; font-size: 12px !important; background-color: rgb(245, 245, 245) !important; border: 1px solid rgb(204, 204, 204) !important; border-radius: 3px !important;">ViewPanel并不能設(shè)置當(dāng)前顯示的內(nèi)容,需要調(diào)用各平臺(tái)一些特定的方法實(shí)現(xiàn))
安卓:
直接利用Renderer
實(shí)現(xiàn)
ViewPanelRenderer : ViewRenderer<ViewPanel, ViewPager>
在安卓平臺(tái)上,ViewPanel
直接利用ViewPager
來實(shí)現(xiàn),所以ViewPanel
對(duì)子元素的布局等方法都會(huì)無效,所有的子元素布局,顯示狀態(tài)都由ViewPager
來管理,ViewPanel
的作用只限于提供子視圖。而ViewPager
中子視圖的創(chuàng)建刪除都由相應(yīng)的Adapter
來實(shí)現(xiàn),這兒用到的是ViewPagerAdapter
。
ViewPagerAdpter
需要的子視圖的類型是Android.Views.View
,而上面提到,ViewPanel
提供的子視圖類型是Xamarin.Forms.View
,所以在添加Xamarin.Forms.View
類型視圖到ViewPagerAdpter
中的時(shí)候,需要完成一次轉(zhuǎn)換,實(shí)則是獲取Xamarin.Forms.View
類型對(duì)象對(duì)應(yīng)在安卓平臺(tái)中的Renderer
,實(shí)現(xiàn)方法如下:
//view is Xamarin.Forms.Viewvar renderer = Platform.CreateRenderer(view);var viewGroup = renderer.ViewGroup;//viewGroup is Android.Views.View
需要注意,雖然子試圖的布局直接由ViewPager
來管理,但是ViewPager
本身的位置,大小是可以由ViewPanel
自己或者他的上層布局決定的。如果它的父布局沒有約束他的位置大小,那么他可以通過在ViewPanel
中重寫的OnMeasure
方法來自定義自己的大?。?/p>
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint){ ... //最簡(jiǎn)單的就是返回固定尺寸,但通常不這么寫,一般根據(jù)它的子視圖位置大小等信息,來相應(yīng)的設(shè)置他自己的尺寸,測(cè)量子元素的尺寸可以調(diào)用`Measure()`方法; return new SizeRequest(new Size(385, 400)); }
完成了ViewPanel
視圖的顯示,還需要實(shí)現(xiàn)交互部分:
訂閱
ViewPager
的PageSelected
事件,再訂閱方法中調(diào)用ViewPanel
的OnSelectChanged()
方法,用于通知訂閱了ViewPanel
的SelectChanged
事件的所有對(duì)象;ViewPanel
的屬性Select
是委托類型,通過為該屬性賦值,真正設(shè)置ViewPanel
顯示的子視圖(調(diào)用ViewPager的SetCurrentItem()方法);
iOS:
iOS的ViewPanel
實(shí)際是利用iOS中UIScrollView
實(shí)現(xiàn),唯一需要用Renderer
實(shí)現(xiàn)的,就是設(shè)置UIScrollView
的PagingEnabled
屬性為Ture
,這樣該滾動(dòng)條就可以按頁滾動(dòng)了
實(shí)現(xiàn)邏輯如下:ViewPanel
繼承自ScrolView
,設(shè)置為水平方向滾動(dòng),然后設(shè)置其Content
為一個(gè)水平方向的StackLayout
,把要顯示的子試圖添加到StackLayout
中。這樣,只要StackLayout
的寬度超出ScrolView
的顯示寬度后,就會(huì)出現(xiàn)水平滾動(dòng)條,通過實(shí)現(xiàn)Renderer
設(shè)置滾動(dòng)條的PagingEnabled
屬性,就能每次滾動(dòng)都完整的滾動(dòng)一個(gè)子視圖的寬度,如果子視圖的寬度恰好為頁面寬度,那就有了輪播圖的效果。
為了讓子視圖的寬度就是ScrollView
的可視寬度,需要重寫該ScrollView
的OnMeasure
和LayoutChildren
方法??梢宰远x一個(gè)繼承自StackLayout
的HorizentalStackLayout
來重寫以上兩個(gè)方法。
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint){ var measuredList = new List<SizeRequest>(); foreach (var item in this.Children) { measuredList.Add(item.Measure(ViewPanel.MeasureWidth, double.PositiveInfinity)); } if (Children == null || Children.Count <= 0) { return new SizeRequest(new Size(ViewPanel.MeasureWidth, 0)); } //ViewPanel.Panel.Width就是滾動(dòng)條可視寬度 Size size = new Size(ViewPanel.Panel.Width * Children.Count(), measuredList.Select(m => m.Request.Height).OrderByDescending(m => m).First()); return new SizeRequest(size, size); }protected override void LayoutChildren(double x, double y, double width, double height){ double posX = 0; foreach (var item in this.Children) { item.Layout(new Rectangle(posX, y, ViewPanel.MeasureWidth, height)); posX += ViewPanel.MeasureWidth; } }
現(xiàn)在有一個(gè)問題,在ViewPanel
中,我們定義了Children
屬性,用來存放子視圖,但是在iOS中,StackLayout
的屬性Children
和她并不相同,所以我們要做一次他們的同步,同步發(fā)生在ViewPanel
的Children
屬性改變的時(shí)候,如下:
static void OnChildrenChanged(BindableObject sender, Object oldValue, Object newValue){ ... var viewPanel = sender as ViewPanel; var stackLayout = viewPanel.Content as StackLayout; stackLayout.Children.Clear(); foreach (View item in viewPanel.Children) { stackLayout.Children.Add(item); }... }
至此,同樣視圖顯示部分就完成了,還剩交互部分,和安卓中一樣,設(shè)計(jì)兩個(gè)部分:
訂閱
UIScrollView
的ScrollView_DecelerationEnded
事件,再訂閱方法中計(jì)算當(dāng)前選中的索引,然后調(diào)用ViewPanel的OnSelectChanged()
方法,用于通知訂閱了ViewPanel
的SelectChanged
事件的所有對(duì)象;private void ScrollView_DecelerationEnded(object sender, EventArgs e){ var index = (int)(_viewPanel.ScrollX / _viewPanel.Width); if (_viewPanel.Width / 2 < (_viewPanel.ScrollX % _viewPanel.Width)) { index++; } _viewPanel.CurrentIndex = index; _viewPanel.OnSelectChanged(); }
ViewPanel
的屬性Select
是委托類型,通過為該屬性賦值,真正設(shè)置ViewPanel
顯示的子視圖(根據(jù)索引來計(jì)算滾動(dòng)條的水平位置,并設(shè)置他);public void Select(int index, bool animate = true){ var perWidth = _viewPanel.Width; _viewPanel.CurrentIndex = index; _viewPanel.ScrollToAsync(index * perWidth, _viewPanel.ScrollY, animate); }
實(shí)現(xiàn)了ViewPanel
,如何利用他實(shí)現(xiàn)自動(dòng)輪播?
之前介紹到,ViewPanel
就是閹割版的自動(dòng)輪播視圖,相比自動(dòng)輪播,只少了兩塊兒
無限滾動(dòng)
邏輯如下圖:實(shí)際添加到顯示
ViewPanel
中的子視圖比設(shè)定的多兩個(gè),第一個(gè)設(shè)置為設(shè)定子視圖的最后一個(gè),最后一個(gè)設(shè)置為設(shè)定子視圖的第一個(gè)。結(jié)合下圖以向右滾動(dòng)為例(紅色),當(dāng)滾動(dòng)到索引為3(黑色標(biāo)號(hào))的子視圖,也就是設(shè)定子視圖的最后一個(gè),此時(shí)繼續(xù)向右滾動(dòng),滾動(dòng)到索引為4的子視圖,他和索引為1的子視圖顯示內(nèi)容相同,當(dāng)滾動(dòng)完成后,繼續(xù)滾動(dòng)到索引為1的子視圖,這次滾動(dòng)很特殊,沒有任何動(dòng)畫效果,直接跳轉(zhuǎn),因?yàn)闈L動(dòng)前后顯示的視圖相同,所以肉眼看不出任何區(qū)別,給人以無限滾動(dòng)的假象。自動(dòng)輪播
這個(gè)簡(jiǎn)單,設(shè)置Timer即可。
總結(jié)
自動(dòng)輪播視圖(CarouselView)的核心思想就是這些,其他具體代碼就不在這兒貼出,文末留出GitHub地址。在實(shí)現(xiàn)中,遇到一些問題或是新的,總結(jié)如下:
在自定義布局中,
OnMeasure
方法不是100%會(huì)被調(diào)用的,這個(gè)布局的大小是否已經(jīng)被約束;
下面是我摘抄的一段話,來解釋這個(gè):
As you’ve seen, it is not guaranteed that the OnSizeRequest override will be called. The method doesn’t need to be called if the size of the layout is governed by its parent rather than its children. The method definitely will be called if one or both of the constraints are infinite, or if the layout class has nondefault settings of VerticalOptions or HorizontalOptions. Otherwise, a call to OnSizeRequest is not guaranteed and you shouldn’t rely on it.
Renderer
實(shí)現(xiàn)中,可以利用Xamarin已經(jīng)為我們提供的Renderer
,而不是自己利用ViewRenderer
去自定義,這樣很大程度上能避免去寫一些iOS、安卓和UWP中相關(guān)的代碼。這次實(shí)踐中iOS平臺(tái)下的ViewPanel
就直接派生自ScrollViewRenderer
依賴屬性,自定義布局的知識(shí)在自定義一個(gè)控件,
Renderer
的時(shí)候是非常重要的······
http://www.cnblogs.com/cjw1115/p/6786615.html