using System; using Android.Views; using Android.Content; using Android.Widget; using Android.Graphics.Drawables; using static ViewFlow; namespace Shared { //已经全面检查了代码 /// /// 位置布局 /// public class PageLayout : ViewGroup, ViewSwitchListener { /// /// 视图高度 /// /// The height. 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; } } /// /// 视图宽度 /// /// The width. 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; } } /// /// 页面变化事件 /// public Action PageChange; AndroidLinearLayout androidLinearLayout; /// /// 构造函数 /// 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)); } /// /// 是否显示下面一排的点 /// /// true if is show point; otherwise, false. public bool IsShowPoint { get { return androidLinearLayout.Visibility == ViewStates.Visible; } set { androidLinearLayout.Visibility = value ? ViewStates.Visible : ViewStates.Invisible; } } /// /// 是否允许滑动 /// /// true if scroll enabled; otherwise, false. public bool ScrollEnabled { get { return (realViewGroup as ViewFlow).ScrollEnabled; } set { (realViewGroup as ViewFlow).ScrollEnabled = value; } } int pageIndex; /// /// 设置或者获取当前的界面索引 /// /// The index of the page. 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); } } /// /// 增加子控件 /// /// View. 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 (); } } } /// /// 移除当前控件 /// /// View. 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(); } /// /// 移除所有的控件 /// public override void RemoveAll () { while (0 < viewList.Count) { GetChildren(0)?.RemoveFromParent(); } androidLinearLayout.RemoveAllViews (); (realViewGroup as ViewFlow).SetSelection(0); } /// /// 根据索引移除控件 /// /// Index. 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; } /// /// 子控件的宽度 /// /// The width of the child. int screenWidth { get { return MeasuredWidth; } } /// /// 滑动到指定的界面 /// void snapToDestination(bool isDelay=true) { int whichScreen = (ScrollX + (screenWidth / 2)) / screenWidth; snapToScreen(whichScreen,isDelay); } /// /// 滑动到指定界面 /// /// Which screen. 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的时候也会出现超过边界再修正的情况 } }