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的时候也会出现超过边界再修正的情况
}
}