天天看點

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

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

背景簡述

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

  1. ViewPager(安卓)
  2. UIScrollView(iOS)
  3. FlipView(UWP)
Xamarin自定義布局系列——支援無限滾動的自動輪播視圖CarouselView

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

視圖的顯示,還需要實作互動部分:

  • 訂閱

    ViewPager

    PageSelected

    事件,再訂閱方法中調用

    ViewPanel

    OnSelectChanged()

    方法,用于通知訂閱了

    ViewPanel

    SelectChanged

    事件的所有對象;
  • ViewPanel

    的屬性

    Select

    是委托類型,通過為該屬性指派,真正設定

    ViewPanel

    顯示的子視圖(調用ViewPager的SetCurrentItem()方法);

iOS:

iOS的

ViewPanel

實際是利用iOS中

UIScrollView

實作,唯一需要用

Renderer

實作的,就是設定

UIScrollView

PagingEnabled

屬性為

Ture

,這樣該滾動條就可以按頁滾動了

實作邏輯如下:

ViewPanel

繼承自

ScrolView

,設定為水準方向滾動,然後設定其

Content

為一個水準方向的

StackLayout

,把要顯示的子試圖添加到

StackLayout

中。這樣,隻要

StackLayout

的寬度超出

ScrolView

的顯示寬度後,就會出現水準滾動條,通過實作

Renderer

設定滾動條的

PagingEnabled

屬性,就能每次滾動都完整的滾動一個子視圖的寬度,如果子視圖的寬度恰好為頁面寬度,那就有了輪播圖的效果。

為了讓子視圖的寬度就是

ScrollView

的可視寬度,需要重寫該

ScrollView

OnMeasure

LayoutChildren

方法。可以自定義一個繼承自

StackLayout

HorizentalStackLayout

來重寫以上兩個方法。

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

和她并不相同,是以我們要做一次他們的同步,同步發生在

ViewPanel

Children

屬性改變的時候,如下:

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

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

  • UIScrollView

    ScrollView_DecelerationEnded

    事件,再訂閱方法中計算目前選中的索引,然後調用

    ViewPanel的OnSelectChanged()

    ViewPanel

    SelectChanged

    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的子視圖,這次滾動很特殊,沒有任何動畫效果,直接跳轉,因為滾動前後顯示的視圖相同,是以肉眼看不出任何差別,給人以無限滾動的假象。
    Xamarin自定義布局系列——支援無限滾動的自動輪播視圖CarouselView
  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

繼續閱讀