using System;
|
using Android.Views;
|
using Android.Content;
|
using Android.Widget;
|
using Android.Graphics.Drawables;
|
using static ViewFlow;
|
|
namespace Shared
|
{
|
//已经全面检查了代码
|
/// <summary>
|
/// 位置布局
|
/// </summary>
|
public class PageLayout : ViewGroup, ViewSwitchListener
|
{
|
/// <summary>
|
/// 视图高度
|
/// </summary>
|
/// <value>The height.</value>
|
public override int Height {
|
get {
|
return base.Height;
|
}
|
set {
|
base.Height = value;
|
if (!IsCanRefresh) {
|
return;
|
}
|
var layoutParameters = realViewGroup.LayoutParameters;
|
layoutParameters.Height = Height;
|
realViewGroup.LayoutParameters = layoutParameters;
|
}
|
}
|
|
/// <summary>
|
/// 视图宽度
|
/// </summary>
|
/// <value>The width.</value>
|
public override int Width {
|
get {
|
return base.Width;
|
}
|
set {
|
base.Width = value;
|
if (!IsCanRefresh) {
|
return;
|
}
|
var layoutParameters = realViewGroup.LayoutParameters;
|
layoutParameters.Width = Width;
|
realViewGroup.LayoutParameters = layoutParameters;
|
}
|
}
|
/// <summary>
|
/// 页面变化事件
|
/// </summary>
|
public Action<PageLayout, int> PageChange;
|
AndroidLinearLayout androidLinearLayout;
|
/// <summary>
|
/// 构造函数
|
/// </summary>
|
public PageLayout ()
|
{
|
viewGroup = new AndroidFrameLayout (Application.Activity, null);
|
realViewGroup = new ViewFlow (Application.Activity, this) { };
|
(realViewGroup as ViewFlow).setOnViewSwitchListener (this);
|
viewGroup.AddView (realViewGroup);
|
androidLinearLayout = new AndroidLinearLayout (Application.Activity, this);
|
androidLinearLayout.SetGravity (GravityFlags.Center);
|
viewGroup.AddView (androidLinearLayout, new Android.Widget.FrameLayout.LayoutParams (Android.Widget.FrameLayout.LayoutParams.MatchParent, DensityUtil.Dip2Px (30), GravityFlags.Bottom));
|
}
|
|
/// <summary>
|
/// 是否显示下面一排的点
|
/// </summary>
|
/// <value><c>true</c> if is show point; otherwise, <c>false</c>.</value>
|
public bool IsShowPoint {
|
get {
|
return androidLinearLayout.Visibility == ViewStates.Visible;
|
}
|
set {
|
androidLinearLayout.Visibility = value ? ViewStates.Visible : ViewStates.Invisible;
|
}
|
}
|
|
/// <summary>
|
/// 是否允许滑动
|
/// </summary>
|
/// <value><c>true</c> if scroll enabled; otherwise, <c>false</c>.</value>
|
public bool ScrollEnabled {
|
get {
|
return (realViewGroup as ViewFlow).ScrollEnabled;
|
}
|
set {
|
(realViewGroup as ViewFlow).ScrollEnabled = value;
|
}
|
}
|
|
|
int pageIndex;
|
/// <summary>
|
/// 设置或者获取当前的界面索引
|
/// </summary>
|
/// <value>The index of the page.</value>
|
public int PageIndex {
|
get {
|
return (realViewGroup as ViewFlow).Position;
|
}
|
set
|
{
|
if (value < 0 || ChildrenCount < value)
|
{
|
return;
|
}
|
int beforePageIndex = pageIndex;
|
pageIndex = value;
|
if (!IsCanRefresh)
|
{
|
return;
|
}
|
|
(realViewGroup as ViewFlow).SetSelection(value);
|
}
|
}
|
|
/// <summary>
|
/// 增加子控件
|
/// </summary>
|
/// <param name="view">View.</param>
|
public override void AddChidren (View view)
|
{
|
view.Parent = this;
|
viewList.Add (view);
|
|
var button = new Android.Widget.Button (Application.Activity);
|
int roundRadius = DensityUtil.Dip2Px (5); // 圆角半径
|
var gd = new GradientDrawable ();//创建drawable
|
if(viewList.Count==1){
|
gd.SetColor(Android.Graphics.Color.GhostWhite);
|
}else{
|
gd.SetColor(Android.Graphics.Color.Gray);
|
}
|
gd.SetCornerRadius (roundRadius);
|
button.Background = gd;
|
var layoutParams = new Android.Widget.LinearLayout.LayoutParams (DensityUtil.Dip2Px (10), DensityUtil.Dip2Px (10));
|
layoutParams.RightMargin = DensityUtil.Dip2Px (10);
|
androidLinearLayout.AddView (button, layoutParams);
|
|
|
realViewGroup.AddView (view.AndroidView);
|
if (!IsCanRefresh) {
|
return;
|
}
|
if (2 <= viewList.Count) {
|
view.X = viewList [viewList.Count - 2].Right;
|
}
|
view.Refresh ();
|
//如果父控件是这种,就在最后补上一个控件
|
if (GetType () == typeof (VerticalScrolViewLayout)) {
|
int tempHeight = -10;
|
foreach (var temp in viewList) {
|
tempHeight += temp.Height;
|
}
|
realViewGroup.AddView (new Android.Widget.LinearLayout (Application.Activity) { Tag = "填充" }, new Android.Views.ViewGroup.LayoutParams (Android.Views.ViewGroup.LayoutParams.MatchParent, tempHeight < realViewGroup.LayoutParameters.Height ? realViewGroup.LayoutParameters.Height - tempHeight : 0));
|
}
|
|
if (view is ViewGroup) {
|
var tempViewGroup = (ViewGroup)view;
|
for (int i = 0; i < tempViewGroup.ChildrenCount; i++) {
|
tempViewGroup.GetChildren (i).Refresh ();
|
}
|
}
|
}
|
|
/// <summary>
|
/// 移除当前控件
|
/// </summary>
|
/// <param name="view">View.</param>
|
internal override void Remove(View view)
|
{
|
if (view == null)
|
{
|
return;
|
}
|
int index = viewList.FindIndex(obj => obj == view);
|
if (index < 0)
|
{
|
return;
|
}
|
|
viewList.Remove(view);
|
androidLinearLayout.RemoveViewAt(index);
|
realViewGroup.RemoveView(view.AndroidView);
|
view.Parent = null;
|
//new System.Threading.Thread(() =>
|
//{
|
// Application.RunOnMainThread(() =>
|
// {
|
(realViewGroup as ViewFlow).SetSelection(PageIndex);
|
// });
|
|
//})
|
//{ IsBackground = true }.Start();
|
}
|
|
/// <summary>
|
/// 移除所有的控件
|
/// </summary>
|
public override void RemoveAll ()
|
{
|
while (0 < viewList.Count) {
|
GetChildren(0)?.RemoveFromParent();
|
}
|
androidLinearLayout.RemoveAllViews ();
|
(realViewGroup as ViewFlow).SetSelection(0);
|
}
|
|
/// <summary>
|
/// 根据索引移除控件
|
/// </summary>
|
/// <param name="index">Index.</param>
|
public override void RemoveAt (int index)
|
{
|
if (viewList.Count - 1 < index || index < 0) {
|
return;
|
}
|
androidLinearLayout.RemoveViewAt (index);
|
GetChildren(index)?.RemoveFromParent();
|
}
|
|
public void onSwitched (Android.Views.View realView, int pageIndex)
|
{
|
for (int i = 0; i < androidLinearLayout.ChildCount; i++) {
|
var view = androidLinearLayout.GetChildAt (i);
|
var gradientDrawable = (GradientDrawable)view.Background;//创建drawable
|
if (i == pageIndex) {
|
gradientDrawable.SetColor (Android.Graphics.Color.GhostWhite);
|
} else {
|
gradientDrawable.SetColor (Android.Graphics.Color.Gray);
|
}
|
view.Background = gradientDrawable;
|
}
|
if (PageChange != null && 0 <= pageIndex) {
|
PageChange (this, pageIndex);
|
}
|
}
|
}
|
}
|
|
|
public class ViewFlow : Android.Widget.FrameLayout
|
{
|
public bool ScrollEnabled = true;
|
static int snapVelocity = 1000;
|
static int invalidScreen = -1;
|
static int touchStateRest = 0;
|
static int touchStateScrolling = 1;
|
public int Position
|
{
|
get
|
{
|
return position;
|
}
|
}
|
int position;
|
Scroller scroller;
|
VelocityTracker mVelocityTracker;
|
int touchState = touchStateRest;
|
float lastMotionX;
|
float lastMotionY;
|
int mTouchSlop;
|
int mMaximumVelocity;
|
int mCurrentScreen;
|
ViewSwitchListener mViewSwitchListener;
|
int mLastScrollDirection;
|
int mNextScreen;
|
|
|
/**
|
* Receives call backs when a new {@link View} has been scrolled to.
|
*/
|
public interface ViewSwitchListener
|
{
|
/**
|
* This method is called when a new View has been scrolled to.
|
*
|
* @param view
|
* the {@link View} currently in focus.
|
* @param position
|
* The position in the adapter of the {@link View} currently in focus.
|
*/
|
void onSwitched(View view, int position);
|
|
}
|
|
Shared.PageLayout pageLayout;
|
public ViewFlow(Context context, Shared.PageLayout view) : base(context)
|
{
|
pageLayout = view;
|
init();
|
}
|
|
void init()
|
{
|
scroller = new Scroller(Context);
|
var configuration = ViewConfiguration.Get(Context);
|
//最小的滑动距离
|
mTouchSlop = configuration.ScaledTouchSlop;
|
mMaximumVelocity = configuration.ScaledMaximumFlingVelocity;
|
}
|
|
ViewFlow childViewFlow
|
{
|
get
|
{
|
if (0 < ChildCount)
|
{
|
var viewGroup = GetChildAt(ChildCount - 1);
|
|
}
|
return null;
|
}
|
}
|
|
public override void RequestDisallowInterceptTouchEvent(bool disallowIntercept)
|
{
|
base.RequestDisallowInterceptTouchEvent(disallowIntercept);
|
disallowInterceptTouchEvent = disallowIntercept;
|
}
|
bool disallowInterceptTouchEvent;
|
|
bool isFirst;
|
public override bool OnInterceptTouchEvent(MotionEvent ev)
|
{
|
if (disallowInterceptTouchEvent||IsDelay)
|
{
|
return false;
|
}
|
Shared.HDLUtils.WriteLine($"PageLayout->OnInterceptTouchEvent:{Height} {ev.Action}");
|
//如果没有子控件或者不允许滑动就返回,不处理当前事件
|
if (ChildCount == 0 || !ScrollEnabled)
|
return false;
|
|
if (mVelocityTracker == null)
|
{
|
//初始化速度检测类
|
mVelocityTracker = VelocityTracker.Obtain();
|
}
|
mVelocityTracker.AddMovement(ev);
|
float x = ev.GetX();
|
float y = ev.GetY();
|
switch (ev.Action)
|
{
|
//每次点击都执行这里
|
case MotionEventActions.Down:
|
if (!scroller.IsFinished)
|
{
|
scroller.AbortAnimation();
|
snapToDestination(false);
|
}
|
//记录点击的最新X坐标
|
lastMotionX = x;
|
lastMotionY = y;
|
touchState = scroller.IsFinished ? touchStateRest : touchStateScrolling;
|
break;
|
//有子控件时执行这里
|
case MotionEventActions.Move:
|
var deltaX = (int)(lastMotionX - x);
|
var deltaY = (int)(lastMotionY - y);
|
//当前滑动是否已经超出了设定值
|
var xMoved = Math.Abs(deltaX) > mTouchSlop ;
|
if (xMoved && Math.Abs(deltaY) < Math.Abs(deltaX))
|
{
|
if (!isFirst)
|
{
|
touchState = touchStateScrolling;
|
}
|
isFirst = false;
|
}
|
if (touchState == touchStateScrolling)
|
{
|
//记录最新的坐标,因为接下来控件已经滑动了
|
lastMotionX = x;
|
|
int scrollX = ScrollX;
|
//向右滑动
|
if (deltaX < 0)
|
{
|
//之前已经滑动的X轴
|
if (scrollX > 0)
|
{
|
//向右滑动时显示出左边的界面,最低的长度为之前已经滑动的距离
|
ScrollBy(Math.Max(-scrollX, deltaX), 0);
|
}
|
}
|
//向左滑动
|
else if (deltaX > 0)
|
{
|
//最小的右边需要滑动的距离
|
int availableToScroll = GetChildAt(
|
ChildCount - 1).Right
|
- PaddingRight - HorizontalFadingEdgeLength
|
- scrollX - Width;
|
if (availableToScroll > 0)
|
{
|
ScrollBy(Math.Min(availableToScroll, deltaX), 0);
|
}
|
}
|
//当前事件自己处理了,子控制不需要处理当前这事件
|
return true;
|
}
|
break;
|
|
case MotionEventActions.Up:
|
if (touchState == touchStateScrolling)
|
{
|
|
mVelocityTracker.ComputeCurrentVelocity(1000, mMaximumVelocity);
|
var velocityX = mVelocityTracker.XVelocity;
|
|
if (velocityX > snapVelocity && mCurrentScreen > 0)
|
{
|
// Fling hard enough to move left
|
snapToScreen(mCurrentScreen - 1);
|
}
|
else if (velocityX < -snapVelocity
|
&& mCurrentScreen < ChildCount - 1)
|
{
|
// Fling hard enough to move right
|
snapToScreen(mCurrentScreen + 1);
|
}
|
else
|
{
|
snapToDestination();
|
}
|
|
if (mVelocityTracker != null)
|
{
|
mVelocityTracker.Recycle();
|
mVelocityTracker = null;
|
}
|
}
|
|
touchState = touchStateRest;
|
|
break;
|
case MotionEventActions.Cancel:
|
touchState = touchStateRest;
|
break;
|
}
|
return false;
|
}
|
|
public bool IsDelay;
|
public override bool OnTouchEvent(MotionEvent e)
|
{
|
if (IsDelay || disallowInterceptTouchEvent)
|
{
|
return false;
|
}
|
Shared.HDLUtils.WriteLine($"PageLayout->OnTouchEvent:{Height} {e.Action}");
|
|
if (ChildCount == 0 || !ScrollEnabled)
|
return false;
|
|
if (mVelocityTracker == null)
|
{
|
mVelocityTracker = VelocityTracker.Obtain();
|
}
|
mVelocityTracker.AddMovement(e);
|
|
var x = e.GetX();
|
var y = e.GetY();
|
switch (e.Action)
|
{
|
case MotionEventActions.Down:
|
if (!scroller.IsFinished)
|
{
|
scroller.AbortAnimation();
|
snapToDestination(false);
|
}
|
lastMotionX = x;
|
lastMotionY = y;
|
touchState = scroller.IsFinished ? touchStateRest
|
: touchStateScrolling;
|
|
break;
|
|
case MotionEventActions.Move:
|
var deltaX = (int)(lastMotionX - x);
|
var deltaY = (int)(lastMotionY - y);
|
//当前滑动是否已经超出了设定值
|
var xMoved = Math.Abs(deltaX) > mTouchSlop;
|
if (xMoved && Math.Abs(deltaY) < Math.Abs(deltaX))
|
{
|
if (!isFirst)
|
{
|
touchState = touchStateScrolling;
|
}
|
isFirst = false;
|
}
|
|
if (touchState == touchStateScrolling)
|
{
|
lastMotionX = x;
|
|
int scrollX = ScrollX;
|
if (deltaX < 0)
|
{
|
if (scrollX > 0)
|
{
|
ScrollBy(Math.Max(-scrollX, deltaX), 0);
|
}
|
}
|
else if (deltaX > 0)
|
{
|
int availableToScroll = GetChildAt(
|
ChildCount - 1).Right
|
- PaddingRight - HorizontalFadingEdgeLength
|
- scrollX - screenWidth;
|
if (availableToScroll > 0)
|
{
|
ScrollBy(Math.Min(availableToScroll, deltaX), 0);
|
}
|
}
|
return true;
|
}
|
break;
|
|
case MotionEventActions.Up:
|
if (touchState == touchStateScrolling)
|
{
|
mVelocityTracker.ComputeCurrentVelocity(1000, mMaximumVelocity);
|
var velocityX = mVelocityTracker.XVelocity;
|
|
if (velocityX > snapVelocity && mCurrentScreen > 0)
|
{
|
// Fling hard enough to move left
|
snapToScreen(mCurrentScreen - 1);
|
}
|
else if (velocityX < -snapVelocity
|
&& mCurrentScreen < ChildCount - 1)
|
{
|
// Fling hard enough to move right
|
snapToScreen(mCurrentScreen + 1);
|
}
|
else
|
{
|
snapToDestination();
|
}
|
|
if (mVelocityTracker != null)
|
{
|
mVelocityTracker.Recycle();
|
mVelocityTracker = null;
|
}
|
}
|
|
touchState = touchStateRest;
|
break;
|
case MotionEventActions.Cancel:
|
snapToDestination();
|
touchState = touchStateRest;
|
break;
|
}
|
return true;
|
}
|
|
/// <summary>
|
/// 子控件的宽度
|
/// </summary>
|
/// <value>The width of the child.</value>
|
int screenWidth
|
{
|
get
|
{
|
return MeasuredWidth;
|
}
|
}
|
/// <summary>
|
/// 滑动到指定的界面
|
/// </summary>
|
void snapToDestination(bool isDelay=true)
|
{
|
int whichScreen = (ScrollX + (screenWidth / 2)) / screenWidth;
|
|
snapToScreen(whichScreen,isDelay);
|
}
|
/// <summary>
|
/// 滑动到指定界面
|
/// </summary>
|
/// <param name="whichScreen">Which screen.</param>
|
void snapToScreen(int whichScreen,bool isDelay=true)
|
{
|
mLastScrollDirection = whichScreen - mCurrentScreen;
|
if (!scroller.IsFinished)
|
return;
|
|
whichScreen = Math.Max(0, Math.Min(whichScreen, ChildCount - 1));
|
Shared.HDLUtils.WriteLine($"snapToScreen:{whichScreen}");
|
mNextScreen = whichScreen;
|
|
int newX = whichScreen * screenWidth;
|
int delta = newX - ScrollX;
|
scroller.StartScroll(ScrollX, 0, delta, 0, isDelay ? Math.Abs(delta) * 2 : 0);
|
Invalidate();
|
}
|
|
public override void ComputeScroll()
|
{
|
if (scroller.ComputeScrollOffset())
|
{
|
ScrollTo(scroller.CurrX, scroller.CurrY);
|
PostInvalidate();
|
}
|
else if (mNextScreen != invalidScreen)
|
{
|
mCurrentScreen = Math.Max(0, Math.Min(mNextScreen, ChildCount - 1));
|
mNextScreen = invalidScreen;
|
postViewSwitched(mLastScrollDirection);
|
}
|
}
|
|
|
|
|
/**
|
* Scroll to the {@link View} in the view buffer specified by the index.
|
*
|
* @param indexInBuffer
|
* Index of the view in the view buffer.
|
*/
|
void setVisibleView(int indexInBuffer, bool uiThread)
|
{
|
mCurrentScreen = Math.Max(0, Math.Min(indexInBuffer, ChildCount - 1));
|
int dx = (mCurrentScreen * screenWidth) - scroller.CurrX;
|
scroller.StartScroll(scroller.CurrX, scroller.CurrY, dx,0, 0);
|
|
if (dx == 0)
|
OnScrollChanged(scroller.CurrX + dx, scroller.CurrY, scroller.CurrX + dx, scroller.CurrY);
|
|
if (uiThread)
|
Invalidate();
|
else
|
PostInvalidate();
|
}
|
|
/**
|
* Set the listener that will receive notifications every time the {code
|
* ViewFlow} scrolls.
|
*
|
* @param l
|
* the scroll listener
|
*/
|
public void setOnViewSwitchListener(ViewSwitchListener l)
|
{
|
mViewSwitchListener = l;
|
}
|
|
public void SetSelection(int position)
|
{
|
mNextScreen = invalidScreen;
|
scroller.ForceFinished(true);
|
var beforePosition = this.position;
|
position = Math.Max(position, 0);
|
position = Math.Min(position, ChildCount - 1);
|
if (position < 0)
|
{
|
return;
|
}
|
|
var currentView = GetChildAt(position);
|
this.position = position;
|
|
setVisibleView(this.position, true);
|
RequestLayout();
|
|
if (mViewSwitchListener != null && beforePosition != this.position)
|
{
|
mViewSwitchListener.onSwitched(currentView, this.position);
|
}
|
}
|
|
void postViewSwitched(int direction)
|
{
|
if (direction == 0)
|
return;
|
|
var beforePosition = position;
|
|
if (direction > 0)
|
{ // to the right
|
position++;
|
position = Math.Min(position, ChildCount - 1);
|
|
}
|
else
|
{ // to the left
|
position--;
|
position = Math.Max(position, 0);
|
}
|
|
setVisibleView(position, true);
|
RequestLayout();
|
|
if (mViewSwitchListener != null && beforePosition != position)
|
{
|
mViewSwitchListener.onSwitched(GetChildAt(position),position);
|
}
|
}
|
|
bool isHaveSameTypeParent
|
{
|
get
|
{
|
var parent = Parent;
|
while (parent != null)
|
{
|
if (parent is ViewFlow)
|
{
|
return true;
|
}
|
parent = parent.Parent;
|
}
|
return false;
|
}
|
|
}
|
|
public override bool DispatchTouchEvent(MotionEvent e)
|
{
|
//Shared.HDLUtils.WriteLine($"PageLayout->DispatchTouchEvent:{Height} {e.Action}");
|
if(e.Action== MotionEventActions.Down) {
|
//还原中断
|
RequestDisallowInterceptTouchEvent(false);
|
isFirst = true;
|
IsDelay = false;
|
}
|
if (isHaveSameTypeParent && !isScrolledToBottom)
|
{
|
//如果父控件和当前控件一样,并且没有滑动到底也没有da
|
Parent.RequestDisallowInterceptTouchEvent(true);
|
}
|
else
|
{
|
isScrolledToBottom = false;
|
Parent.RequestDisallowInterceptTouchEvent(false);
|
}
|
return base.DispatchTouchEvent(e);
|
}
|
|
private bool isScrolledToTop = true; // 初始化的时候设置一下值
|
private bool isScrolledToBottom;
|
|
protected override void OnOverScrolled(int scrollX, int scrollY, bool clampedX, bool clampedY)
|
{
|
base.OnOverScrolled(scrollX, scrollY, clampedX, clampedY);
|
if (scrollY == 0)
|
{
|
isScrolledToTop = clampedY;
|
isScrolledToBottom = false;
|
}
|
else
|
{
|
isScrolledToTop = false;
|
isScrolledToBottom = clampedY;
|
}
|
|
}
|
|
|
protected override void OnScrollChanged(int l, int t, int oldl, int oldt)
|
{
|
base.OnScrollChanged(l, t, oldl, oldt);
|
if ((int)Android.OS.Build.VERSION.SdkInt < 9)
|
{ // API 9及之后走onOverScrolled方法监听
|
if (ScrollY == 0)
|
{ // 小心踩坑1: 这里不能是getScrollY() <= 0
|
isScrolledToTop = true;
|
isScrolledToBottom = false;
|
}
|
else if (ScrollY + Height - PaddingTop - PaddingBottom == (0 < ChildCount ? GetChildAt(0).Height : Height))
|
{
|
// 小心踩坑2: 这里不能是 >=
|
// 小心踩坑3(可能忽视的细节2):这里最容易忽视的就是ScrollView上下的padding
|
isScrolledToBottom = true;
|
isScrolledToTop = false;
|
}
|
else
|
{
|
isScrolledToTop = false;
|
isScrolledToBottom = false;
|
}
|
}
|
// 有时候写代码习惯了,为了兼容一些边界奇葩情况,上面的代码就会写成<=,>=的情况,结果就出bug了
|
// 我写的时候写成这样:getScrollY() + getHeight() >= getChildAt(0).getHeight()
|
// 结果发现快滑动到底部但是还没到时,会发现上面的条件成立了,导致判断错误
|
// 原因:getScrollY()值不是绝对靠谱的,它会超过边界值,但是它自己会恢复正确,导致上面的计算条件不成立
|
// 仔细想想也感觉想得通,系统的ScrollView在处理滚动的时候动态计算那个scrollY的时候也会出现超过边界再修正的情况
|
}
|
}
|