1. 程式人生 > >Xamarin自定義佈局系列——支援無限滾動的自動輪播檢視CarouselView

Xamarin自定義佈局系列——支援無限滾動的自動輪播檢視CarouselView

原文: Xamarin自定義佈局系列——支援無限滾動的自動輪播檢視CarouselView

背景簡述


自動輪播檢視(CarouselView)在現在App中的地位不言而喻,絕大多數的App中都有類似的檢視,無論是WebApp還是Native App。在安卓、iOS以及Windows(UWP)開發中,有一些控制元件可以很方便的來實現類似的效果。

  1. ViewPager(安卓)
  2. UIScrollView(iOS)
  3. FlipView(UWP)

自動輪播示例

Xamarin.Forms怎麼實現自動輪播檢視呢?


Xamarin.Forms有自己的一套佈局系統,結合各平臺特性,也可以實現一個比較好的自動輪播檢視。

上次介紹我實現的一個多頁面水平切換佈局中,提到我使用了一個叫做ViewPanel的自定義佈局,他與自動輪播檢視相比,只是缺少了無線滾動和自動輪播,這次也以這個佈局為基礎,來實現自動輪播檢視。

核心依然是ViewPanel在各個平臺中的具體實現:

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是一個集合型別,它用來儲存需要在ViewPanel中顯示的檢視,一般子檢視的都從Xamarin.Forms.View派生或者是他本身

其次,ViewPanel能互動,需要實現一個事件,一個方法

  • event EventHandler SelectChanged:當ViewPanel中顯示的元素改變時提供通知,並且提供OnSelectChanged()來觸發該事件
    *void select():用於設定ViewPanel需要顯示的子檢視(實際Select會是一個委託,因為ViewPanel並不能設定當前顯示的內容,需要呼叫各平臺一些特定的方法實現)

安卓:

直接利用Renderer實現

ViewPanelRenderer : ViewRenderer<ViewPanel, ViewPager>

在安卓平臺上,ViewPanel直接利用ViewPager來實現,所以ViewPanel對子元素的佈局等方法都會無效,所有的子元素佈局,顯示狀態都由ViewPager來管理,ViewPanel的作用只限於提供子檢視。而ViewPager中子檢視的建立刪除都由相應的Adapter來實現,這兒用到的是ViewPagerAdapter

ViewPagerAdpter需要的子檢視的型別是Android.Views.View,而上面提到,ViewPanel提供的子檢視型別是Xamarin.Forms.View,所以在新增Xamarin.Forms.View型別檢視到ViewPagerAdpter中的時候,需要完成一次轉換,實則是獲取Xamarin.Forms.View型別物件對應在安卓平臺中的Renderer,實現方法如下:

//view is Xamarin.Forms.View
var renderer = Platform.CreateRenderer(view);
var viewGroup = renderer.ViewGroup;

//viewGroup is Android.Views.View

需要注意,雖然子試圖的佈局直接由ViewPager來管理,但是ViewPager本身的位置,大小是可以由ViewPanel自己或者他的上層佈局決定的。如果它的父佈局沒有約束他的位置大小,那麼他可以通過在ViewPanel中重寫的OnMeasure方法來自定義自己的大小:

protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
    ...
    //最簡單的就是返回固定尺寸,但通常不這麼寫,一般根據它的子檢視位置大小等資訊,來相應的設定他自己的尺寸,測量子元素的尺寸可以呼叫`Measure()`方法;
    return new SizeRequest(new Size(385, 400));
}

完成了ViewPanel檢視的顯示,還需要實現互動部分:

  • 訂閱ViewPagerPageSelected事件,再訂閱方法中呼叫ViewPanelOnSelectChanged()方法,用於通知訂閱了ViewPanelSelectChanged事件的所有物件;

  • ViewPanel的屬性Select是委託型別,通過為該屬性賦值,真正設定ViewPanel顯示的子檢視(呼叫ViewPager的SetCurrentItem()方法);

iOS:

iOS的ViewPanel實際是利用iOS中UIScrollView實現,唯一需要用Renderer實現的,就是設定UIScrollViewPagingEnabled屬性為Ture,這樣該滾動條就可以按頁滾動了

實現邏輯如下:
ViewPanel繼承自ScrolView,設定為水平方向滾動,然後設定其Content為一個水平方向的StackLayout,把要顯示的子試圖新增到StackLayout中。這樣,只要StackLayout的寬度超出ScrolView的顯示寬度後,就會出現水平滾動條,通過實現Renderer設定滾動條的PagingEnabled屬性,就能每次滾動都完整的滾動一個子檢視的寬度,如果子檢視的寬度恰好為頁面寬度,那就有了輪播圖的效果。

為了讓子檢視的寬度就是ScrollView的可視寬度,需要重寫該ScrollViewOnMeasureLayoutChildren方法。可以自定義一個繼承自StackLayoutHorizentalStackLayout來重寫以上兩個方法。

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就是滾動條可視寬度
    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;
    }
}

現在有一個問題,在ViewPanel中,我們定義了Children屬性,用來存放子檢視,但是在iOS中,StackLayout的屬性Children和她並不相同,所以我們要做一次他們的同步,同步發生在ViewPanelChildren屬性改變的時候,如下:

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);
    }...
}

至此,同樣檢視顯示部分就完成了,還剩互動部分,和安卓中一樣,設計兩個部分:

  • 訂閱UIScrollViewScrollView_DecelerationEnded事件,再訂閱方法中計算當前選中的索引,然後呼叫ViewPanel的OnSelectChanged()方法,用於通知訂閱了ViewPanelSelectChanged事件的所有物件;

      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是委託型別,通過為該屬性賦值,真正設定ViewPanel顯示的子檢視(根據索引來計算滾動條的水平位置,並設定他);

      public void Select(int index, bool animate = true)
      {
          var perWidth = _viewPanel.Width;
          _viewPanel.CurrentIndex = index;
          _viewPanel.ScrollToAsync(index * perWidth, _viewPanel.ScrollY, animate);
      }

實現了ViewPanel,如何利用他實現自動輪播?


之前介紹到,ViewPanel就是閹割版的自動輪播檢視,相比自動輪播,只少了兩塊兒

  1. 無限滾動

    邏輯如下圖:實際新增到顯示ViewPanel中的子檢視比設定的多兩個,第一個設定為設定子檢視的最後一個,最後一個設定為設定子檢視的第一個。結合下圖以向右滾動為例(紅色),當滾動到索引為3(黑色標號)的子檢視,也就是設定子檢視的最後一個,此時繼續向右滾動,滾動到索引為4的子檢視,他和索引為1的子檢視顯示內容相同,當滾動完成後,繼續滾動到索引為1的子檢視,這次滾動很特殊,沒有任何動畫效果,直接跳轉,因為滾動前後顯示的檢視相同,所以肉眼看不出任何區別,給人以無限滾動的假象。
    無限輪播示例

  2. 自動輪播

    這個簡單,設定Timer即可。

總結


自動輪播檢視(CarouselView)的核心思想就是這些,其他具體程式碼就不在這兒貼出,文末留出GitHub地址。在實現中,遇到一些問題或是新的,總結如下:

  • 在自定義佈局中,OnMeasure方法不是100%會被呼叫的,這個佈局的大小是否已經被約束;
    下面是我摘抄的一段話,來解釋這個:

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實現中,可以利用Xamarin已經為我們提供的Renderer,而不是自己利用ViewRenderer去自定義,這樣很大程度上能避免去寫一些iOS、安卓和UWP中相關的程式碼。這次實踐中iOS平臺下的ViewPanel就直接派生自ScrollViewRenderer

  • 依賴屬性,自定義佈局的知識在自定義一個控制元件,Renderer的時候是非常重要的

  • ······

本次實踐相關連線:

GitHub專案地址:cjw1115/PivotPage