yannecer / NCalendar

一款安卓日历,仿miui,钉钉,华为的日历,万年历、365、周日历,月日历,月视图、周视图滑动切换,农历,节气,Andriod Calendar , MIUI Calendar,小米日历
Apache License 2.0
1.59k stars 314 forks source link

5.0以下系统报错问题 #36

Closed shipan closed 5 years ago

shipan commented 5 years ago

将NCalendar类修改 主要实现NestedScrollingParent 类中 onNestedScrollAccepted onNestedScroll getNestedScrollAxes 方法

public abstract class NCalendar extends FrameLayout implements NestedScrollingParent, OnCalendarStateChangedListener, OnDateChangedListener, OnMonthAnimatorListener {

protected WeekCalendar weekCalendar;
protected MonthCalendar monthCalendar;

protected int weekHeight;//周日历的高度
protected int monthHeight;//月日历的高度,是日历整个的高

protected int STATE;//默认月
private int lastSate;//防止状态监听重复回调
private OnCalendarChangedListener onCalendarChangedListener;

protected ChildLayout childLayout;//NCalendar内部包含的直接子view,直接子view并不一定是NestScrillChild

protected Rect monthRect;//月日历大小的矩形
protected Rect weekRect;//周日历大小的矩形 ,用于判断点击事件是否在日历的范围内

private boolean isWeekHold;//是否需要周状态定住
private NestedScrollingParentHelper mParentHelper;

public NCalendar(@NonNull Context context) {
    this(context, null);

}

public NCalendar(@NonNull Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

private void init() {
    mParentHelper = new NestedScrollingParentHelper(this);
}
public NCalendar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
    setMotionEventSplittingEnabled(false);
    Attrs attrss = AttrsUtil.getAttrs(context, attrs);

    int duration = attrss.duration;
    monthHeight = attrss.monthCalendarHeight;
    STATE = attrss.defaultCalendar;
    weekHeight = monthHeight / 5;
    isWeekHold = attrss.isWeekHold;

    weekCalendar = new WeekCalendar(context, attrss);
    monthCalendar = new MonthCalendar(context, attrss, duration, this);
    childLayout = new ChildLayout(getContext(), attrs, monthHeight, duration, this);

    monthCalendar.setOnDateChangedListener(this);
    weekCalendar.setOnDateChangedListener(this);

    childLayout.setBackgroundColor(attrss.bgChildColor);

    setCalenadrState(STATE);

    post(new Runnable() {
        @Override
        public void run() {
            monthRect = new Rect(0, 0, monthCalendar.getWidth(), monthCalendar.getHeight());
            weekRect = new Rect(0, 0, weekCalendar.getWidth(), weekCalendar.getHeight());
        }
    });

}

/**
 * 根据ChildLayout的自动滑动结束的状态来设置月周日历的状态
 * 依据ChildLayout的状态来设置日历的状态
 *
 * @param isMonthState
 */
@Override
public void onCalendarStateChanged(boolean isMonthState) {
    if (isMonthState) {
        setCalenadrState(Attrs.MONTH);
    } else {
        setCalenadrState(Attrs.WEEK);
    }
}

/**
 * xml文件加载结束,添加月,周日历和child到NCalendar中
 */
@Override
protected void onFinishInflate() {
    super.onFinishInflate();

    if (getChildCount() != 1) {
        throw new RuntimeException("NCalendar中的只能有一个直接子view");
    }
    childLayout.addView(getChildAt(0), new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

    addView(monthCalendar, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, monthHeight));
    addView(weekCalendar, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, weekHeight));
    addView(childLayout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    ViewGroup.LayoutParams childLayoutLayoutParams = childLayout.getLayoutParams();
    childLayoutLayoutParams.height = getMeasuredHeight() - weekHeight;

    //需要再调一次父类的方法?真机不调用首次高度不对,为何?
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //super.onLayout(changed, l, t, r, b); //调用父类的该方法会造成 快速滑动月日历同时快速上滑recyclerview造成月日历的残影
    int monthCalendarTop;
    int childLayoutTop;
    if (STATE == Attrs.MONTH) {
        monthCalendarTop = monthCalendar.getTop();
        childLayoutTop = childLayout.getTop() == 0 ? monthHeight : childLayout.getTop();
    } else {
        monthCalendarTop = getMonthTopOnWeekState();
        childLayoutTop = childLayout.getTop() == 0 ? weekHeight : childLayout.getTop();
    }

    int measuredWidth = getMeasuredWidth();
    weekCalendar.layout(0, 0, measuredWidth, weekHeight);
    monthCalendar.layout(0, monthCalendarTop, measuredWidth, monthHeight + monthCalendarTop);
    childLayout.layout(0, childLayoutTop, measuredWidth, childLayout.getMeasuredHeight() + childLayoutTop);

}

/**
 * 根据条件设置日历的月周状态,并回调状态变化
 *
 * @param state
 */
private void setCalenadrState(int state) {

    if (state == Attrs.WEEK) {
        STATE = Attrs.WEEK;
        weekCalendar.setVisibility(VISIBLE);
    } else {
        STATE = Attrs.MONTH;
        weekCalendar.setVisibility(INVISIBLE);
    }

    if (onCalendarChangedListener != null && lastSate != state) {
        onCalendarChangedListener.onCalendarStateChanged(STATE == Attrs.MONTH);
    }

    lastSate = state;
}

/**
 * 自动滑动到适当的位置
 */
private void autoScroll() {

    int childLayoutTop = childLayout.getTop();

    if (STATE == Attrs.MONTH && monthHeight - childLayoutTop < weekHeight) {
        onAutoToMonthState();
    } else if (STATE == Attrs.MONTH && monthHeight - childLayoutTop >= weekHeight) {
        onAutoToWeekState();
    } else if (STATE == Attrs.WEEK && childLayoutTop < weekHeight * 2) {
        onAutoToWeekState();
    } else if (STATE == Attrs.WEEK && childLayoutTop >= weekHeight * 2) {
        onAutoToMonthState();
    }
}

/**
 * 月日历和周日历的日期变化回调,每次日期变化都会回调,用于不同状态下,设置另一个日历的日期
 *
 * @param baseCalendar 日历本身
 * @param date         当前选中的时间
 * @param isDraw       是否绘制 此处选择都绘制,默认不选中,不适用鱼月周切换
 */
@Override
public void onDateChanged(BaseCalendar baseCalendar, NDate date, boolean isDraw) {

    if (baseCalendar instanceof MonthCalendar && STATE == Attrs.MONTH) {
        //月日历变化,改变周的选中
        weekCalendar.jumpDate(date.localDate, true);
        if (onCalendarChangedListener != null) {
            onCalendarChangedListener.onCalendarDateChanged(date);
        }

    } else if (baseCalendar instanceof WeekCalendar && STATE == Attrs.WEEK) {
        //周日历变化,改变月的选中
        monthCalendar.jumpDate(date.localDate, true);
        post(new Runnable() {
            @Override
            public void run() {
                //此时需要重新请求布局,不然会闪烁变成原来的状态,
                // post是因为在前面得到当前view是再post中完成,如果不这样直接获取位置信息,会出现老的数据,不能获取正确的数据
                requestLayout();
            }
        });
        if (onCalendarChangedListener != null) {
            onCalendarChangedListener.onCalendarDateChanged(date);
        }
    }
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {

    return true;
}

@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
    mParentHelper.onNestedScrollAccepted(child, target, axes);

}

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}

//返回值:是否消费了fling
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
    return false;
}

@Override
public int getNestedScrollAxes() {
    return mParentHelper.getNestedScrollAxes();
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    //跟随手势滑动
    gestureMove(dy, consumed);
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    //只有都在都在周状态下,才允许子View Fling滑动
    return !(childLayout.isWeekState() && monthCalendar.isWeekState());
}

@Override
public void onStopNestedScroll(View target) {

    //该方法手指抬起的时候回调,此时根据此刻的位置,自动滑动到相应的状态,
    //如果已经在对应的位置上,则不执行动画,
    if (monthCalendar.isMonthState() && childLayout.isMonthState() && STATE == Attrs.WEEK) {
        setCalenadrState(Attrs.MONTH);
    } else if (monthCalendar.isWeekState() && childLayout.isWeekState() && STATE == Attrs.MONTH) {
        setCalenadrState(Attrs.WEEK);
    } else if (!childLayout.isMonthState() && !childLayout.isWeekState()) {
        //不是周状态也不是月状态时,自动滑动
        autoScroll();
    }
}

/**
 * 手势滑动的逻辑,做了简单处理,2种状态,都以ChildLayout滑动的状态判断
 * 1、向上滑动未到周状态
 * 2、向下滑动未到月状态
 *
 * @param dy
 * @param consumed
 */
protected void gestureMove(int dy, int[] consumed) {

    if (dy > 0 && !childLayout.isWeekState()) {
        monthCalendar.offsetTopAndBottom(-getGestureMonthUpOffset(dy));
        childLayout.offsetTopAndBottom(-getGestureChildUpOffset(dy));
        if (consumed != null) consumed[1] = dy;
    } else if (dy < 0 && isWeekHold && childLayout.isWeekState()) {
        //不操作,

    } else if (dy < 0 && !childLayout.isMonthState() && !childLayout.canScrollVertically(-1)) {
        monthCalendar.offsetTopAndBottom(getGestureMonthDownOffset(dy));
        childLayout.offsetTopAndBottom(getGestureChildDownOffset(dy));
        if (consumed != null) consumed[1] = dy;
    }

    onSetWeekVisible(dy);
}

/**
 * 月日历执行自动滑动动画的回调
 * 用来控制周日历的显示还是隐藏
 *
 * @param offset
 */
@Override
public void onMonthAnimatorChanged(int offset) {
    onSetWeekVisible(offset);
}

private int dowmY;
private int downX;
private int lastY;//上次的y
private int verticalY = 50;//竖直方向上滑动的临界值,大于这个值认为是竖直滑动
private boolean isFirstScroll = true; //第一次手势滑动,因为第一次滑动的偏移量大于verticalY,会出现猛的一划,这里只对第一次滑动做处理

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            dowmY = (int) ev.getY();
            downX = (int) ev.getX();
            lastY = dowmY;
            break;
        case MotionEvent.ACTION_MOVE:
            int y = (int) ev.getY();
            int absY = Math.abs(dowmY - y);
            boolean inCalendar = isInCalendar(downX, dowmY);
            if (absY > verticalY && inCalendar) {
                //onInterceptTouchEvent返回true,触摸事件交给当前的onTouchEvent处理
                return true;
            }
            break;
    }
    return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:

            int y = (int) event.getY();
            int dy = lastY - y;

            if (isFirstScroll) {
                // 防止第一次的偏移量过大
                if (dy > verticalY) {
                    dy = dy - verticalY;
                } else if (dy < -verticalY) {
                    dy = dy + verticalY;
                }
                isFirstScroll = false;
            }

            // 跟随手势滑动
            gestureMove(dy, null);
            lastY = y;
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            isFirstScroll = true;
            autoScroll();
            break;
    }
    return true;
}

/**
 * 点击事件是否在日历的范围内
 *
 * @param x
 * @param y
 * @return
 */
private boolean isInCalendar(int x, int y) {
    if (STATE == Attrs.MONTH) {
        return monthRect.contains(x, y);
    } else {
        return weekRect.contains(x, y);
    }
}

/**
 * 滑动过界处理 ,如果大于最大距离就返回最大距离
 *
 * @param offset    当前滑动的距离
 * @param maxOffset 当前滑动的最大距离
 * @return
 */
protected int getOffset(int offset, int maxOffset) {
    if (offset > maxOffset) {
        return maxOffset;
    }
    return offset;
}

public void setOnCalendarChangedListener(OnCalendarChangedListener onCalendarChangedListener) {
    this.onCalendarChangedListener = onCalendarChangedListener;
}

/**
 * 自动回到月的状态 包括月日历和chilayout
 */
protected abstract void onAutoToMonthState();

/**
 * 自动回到周的状态 包括月日历和chilayout
 */
protected abstract void onAutoToWeekState();

/**
 * 设置weekCalendar的显示隐藏,该方法会在手势滑动和自动滑动的的时候一直回调
 */
protected abstract void onSetWeekVisible(int dy);

/**
 * 周状态下 月日历的顶部距离父view顶部的值 是个负值
 * 用于在 onLayout 方法中摆放月日历的位置
 *
 * @return
 */
protected abstract int getMonthTopOnWeekState();

/**
 * 月日历根据手势向上移动的距离
 *
 * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动
 * @return 根据不同日历的交互,计算不同的滑动值
 */
protected abstract int getGestureMonthUpOffset(int dy);

/**
 * Child根据手势向上移动的距离
 *
 * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动
 * @return 根据不同日历的交互,计算不同的滑动值
 */
protected abstract int getGestureChildUpOffset(int dy);

/**
 * 月日历根据手势向下移动的距离
 *
 * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动
 * @return 根据不同日历的交互,计算不同的滑动值
 */
protected abstract int getGestureMonthDownOffset(int dy);

/**
 * Child根据手势向下移动的距离
 *
 * @param dy 当前滑动的距离 dy>0向上滑动,dy<0向下滑动
 * @return 根据不同日历的交互,计算不同的滑动值
 */
protected abstract int getGestureChildDownOffset(int dy);

/**
 * 跳转日期
 *
 * @param formatDate
 */
public void jumpDate(String formatDate) {

    if (STATE == Attrs.MONTH) {
        monthCalendar.jumpDate(formatDate);
    } else {
        weekCalendar.jumpDate(formatDate);
    }
}

/**
 * 回到今天
 */
public void toToday() {
    if (STATE == Attrs.MONTH) {
        monthCalendar.toToday();
    } else {
        weekCalendar.toToday();
    }
}

/**
 * 自动滑动到周视图
 */
public void toWeek() {
    if (STATE == Attrs.MONTH) {
        onAutoToWeekState();
    }
}

/**
 * 自动滑动到月视图
 */
public void toMonth() {
    if (STATE == Attrs.WEEK) {
        onAutoToMonthState();
    }
}

/**
 * 设置小圆点
 *
 * @param pointList
 */
public void setPointList(List<String> pointList) {
    weekCalendar.setPointList(pointList);
    monthCalendar.setPointList(pointList);
}

/**
 * 获取当前日历的状态
 * Attrs.MONTH==月视图    Attrs.WEEK==周视图
 *
 * @return
 */
public int getState() {
    return STATE;
}

public void toNextPager() {
    if (STATE == Attrs.MONTH) {
        monthCalendar.toNextPager();
    } else {
        weekCalendar.toNextPager();
    }
}

public void toLastPager() {
    if (STATE == Attrs.MONTH) {
        monthCalendar.toLastPager();
    } else {
        weekCalendar.toLastPager();
    }
}

}

yannecer commented 5 years ago

@shipan 已修改,多谢