有趣的自定义view —《聆雨》· 上下滑动面板

自定义可滑动面板效果如下:

“且听细雨,勿湿衣襟”  — 聆雨

    

一、效果要求

1)面板随手指上下滑动,要求流畅性;

2)面板下滑时,手指松开,回到原位;

3)面板上滑,距离不足时,回到原位;上滑之一定距离时,停在一个固定的地方,以便显示面板下面的内容;

4)上下滑动时的过渡回弹效果和面板透明度变化;

5)面板上的要求控制音频播放、展示天气等控制

二、实现难点及实现方法

1)面板本身连同内容随手指上下滑动 —— 自定义ViewGroup没跑了;

2)判断手指上下滑动,然后实现面板上下滑动 —— 重写ViewGroup的onTouchEvent()方法然后利用scroll的方法可实现滑动;

3)上下滑动要求回弹效果 —— 动画接入合适的插值器可以做到;

4)面板上实现一系列操作,包括音频控制,网络数据请求 —— 自定义FrameLayout,然后播放、请求网络等逻辑操作写到Fragment中,最后将Fragment,add进自定义view内即可,这种实现方式最为合理。

自定义View做与手指的交互,而功能的控制实现则放到Fragment中。MVC,View层只做展示,不做功能控制。

三、上代码,具体实现

按照上述需求,一步步实现:

1)首先自定义ViewGroup,且继承自FrameLayout;

public class ScrollPanel extends FrameLayout {

    public ScrollPanel(Context context) {
        super(context);
    }

    public ScrollPanel(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ScrollPanel(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

2)接着重写ScrollPanel的onTouchEvent()方法,用于控制面板随手指滑动;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        final float eventY = event.getY();

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE:
                if (isOpened && (getHeight() - getScrollY()) < eventY) {
                    return true;
                }

                if (isDragging) {
                    float offset = 0;
                    if (mLastEventY != 0) offset = mLastEventY - eventY;
                    setDrawingCacheEnabled(true);
                    //offset值较小 用于首次启动滑动;getScrollY()值较大 存储了offset和值    
                    scrollTo(0, (int) (getScrollY() + offset)); 
                    mLastEventY = eventY;
                } else {
                    mLastEventY = 0;
                }

                setAlpha(0.85f);
                isDragging = true;
                break;

            case MotionEvent.ACTION_DOWN:
                if (isOpened && (getHeight() - getScrollY()) < eventY) {
                    return false;
                } else {
                    mFirstEventY = eventY;
                }
                break;

            case MotionEvent.ACTION_UP:
                if (!isDragging) return false;

                if (mFirstEventY > eventY && Math.abs(mFirstEventY - eventY) > DEFAULT_SLIDE_DISTANCE) { //判断手指上滑,同时滑动大于一个域值
                //触发条件 移动到指定位置 - 动画结束时要停在指定区域 动画接收参数为手离开屏幕是的纵坐标 第二个长参数是个负值 即为ViewPager的高度
                    open(); 
                } else {
                //滑动不符合要求 回到原始区域 - 动画结束时返回原始状态 动画接收参数为手离开屏幕是的纵坐标 第二个长参数是0
                    close(); 
                }
                clearDragging();
                break;
        }
        return true;
    }

仔细看下onTouchEvent()中的逻辑处理:

①MotionEvent.ACTION_MASK 在Android中是应用于多点触摸操作 加上之后就可以处理多点触控的操作;

②MotionEvent.ACTION_DOWN时,用一个变量mLastEventY记录下手指首次触摸屏幕时点的纵坐标;

③MotionEvent.ACTION_MOVE时,获取手指move时的下一个触摸点的纵坐标,与mLastEventY记录的坐标值进行计算,获取两次手指移动的偏移量;接着调用scrollTo()实现面板滑动,接收的第一个参数为0,表示原位,第二个参数为getScrollY() + offset,表示面板偏移量。(注意,单独使用getScrollY() ,不会发生滑动;单独使用offset偏移量很小);最后在面板发生滑动之后,将ScrollPanel的透明度alpha设为0.85f;

④setDrawingCacheEnabled(true)用以优化,提高绘图速度,防止卡顿;

⑤MotionEvent.ACTION_UP时,进行手指滑动方向和滑动距离判断:若手指下滑,手指抬起时则调用close()方法关闭面板;若手指上滑,进一步作出判断,滑动距离足够时(本例中滑动距离自定义为DEFAULT_SLIDE_DISTANCE = 200),执行open()方法,调用动画,使得面板上滑至指定区域;滑动距离很少时,执行close()方法回到原位。

另外setDrawingCacheEnabled(true), 获取cache时会占有一定的内存在不用的时候要设为false。所以每次手指抬起时调用clearDragging(),用以关闭cache,同时归零一系列布尔值。

    private void clearDragging() {
        mLastEventY = 0;
        mFirstEventY = 0;
        isDragging = false;
        setDrawingCacheEnabled(false);
    }

3)上下滑动的过渡动画和透明度变化;

close()方法及对应的下滑动画:

    public void close() {
        setAnimationCacheEnabled(true);
        setAnimatorCloseSet().start();
    }

    private AnimatorSet setAnimatorCloseSet() {
        mCloseAnimator = ValueAnimator.ofInt(getScrollY(), SCROLL_TOP);
        mCloseAnimator.setInterpolator(new OvershootInterpolator());
        mCloseAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int height = (Integer) valueAnimator.getAnimatedValue();
                scrollTo(0, height);
            }
        });

        mFadeInAnimator = ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1f);

        mAnimatorCloseSet = new AnimatorSet();
        mAnimatorCloseSet.setDuration(DEFAULT_CLOSE_DURATION);
        mAnimatorCloseSet.play(mCloseAnimator).with(mFadeInAnimator);
        mAnimatorCloseSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (mSimplePanelListener != null) {
                    mSimplePanelListener.onClosed();
                }
                isOpened = false;
            }

            @Override
            public void onAnimationCancel(Animator animator) {
            }

            @Override
            public void onAnimationRepeat(Animator animator) {
            }
        });

        return mAnimatorCloseSet;
    }

①实现滑动的动画效果:close()方法调用的时机是在手指抬起,判断需要返回原位的时候执行,这里使用ValueAnimator,构造方法内传入两个参数——面板滑动的距离getScrollY()和面板复位值0,然后调用valueAnimator.getAnimatedValue()获取动画执行的当前点的值height ,传入scrollTo()方法里面,实现手指抬起时面板复位;

②实现面板滑动时的透明度变化,这里藉由属性动画ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1f)实现,将MotionEvent.ACTION_MOVE事件内改为0.85f透明度的面板透明度变为1f,完全不透明。

最后通过动画组合mAnimatorOpenSet的方式执行滑动动画和透明度变化动画。

open()方法及对应的上滑动画:

    public void open() {
        setAnimationCacheEnabled(true);
        setAnimatorOpenSet().start();
    }
    private AnimatorSet setAnimatorOpenSet() {
        height = getResources().getDisplayMetrics().heightPixels ;
        int scrollTo = (int) (height * ratio);
        mOpenAnimator = ValueAnimator.ofInt(getScrollY(), scrollTo);

        mOpenAnimator.setInterpolator(new OvershootInterpolator());
        mOpenAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int height = (Integer) valueAnimator.getAnimatedValue();
                scrollTo(0, height);
            }
        });
        //在自定义View中使用ObjectAnimator属性动画时,第一个参数传入this即可
        mFadeOutAnimator = ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 0.85f);

        mAnimatorOpenSet = new AnimatorSet();
        mAnimatorOpenSet.setDuration(DEFAULT_OPEN_DURATION);
        mAnimatorOpenSet.play(mOpenAnimator).with(mFadeOutAnimator);
        mAnimatorOpenSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (mSimplePanelListener != null) {
                    mSimplePanelListener.onOpened();
                }
                isOpened = true;
            }

            @Override
            public void onAnimationCancel(Animator animator) {
            }

            @Override
            public void onAnimationRepeat(Animator animator) {
            }
        });

        return mAnimatorOpenSet;
    }

①实现的逻辑同下滑是一样的,只不过,要注意,手指上滑超过一定距离时,此时面板不是复位,而是停在一个固定的地方不动,将面板下面的内容展示出来,本例中面板下面是一个ViewPager+Fragment实现的内容布局,所以停住的位置应该刚好放下ViewPager。

②这里我们通过 height = getResources().getDisplayMetrics().heightPixels  获取手机屏幕的高度,然后乘以一个系数值0.382,同时在Activity代码里将ViewPager的高度也动态的设置这个值,那么就可以实现高度刚好合适展示内容的需求。

③动画使用的插值器是 OvershootInterpolator - 结束偏移插值器,即动画完成时还滑动一小段距离,可实现过渡滑动效果。

自定义View — ScrollPanel 完整实现如下

package com.chinstyle.scrollpanel;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.animation.OvershootInterpolator;
import android.widget.FrameLayout;

/**
 * 作者    Chin_style
 * 时间    2018/12/1
 * 文件    ScrollPanel
 * 描述    ①MotionEvent.ACTION_MASK 在Android中是应用于多点触摸操作 加上之后就可以处理多点触控的操作;
 *        ②利用Scroll类实现自定义view滑动的标准写法 scrollTo(0, (int) (getScrollY() + offset)); 后者是发动机(首次滑动) 前者是存储动力源(持续存储滑动值)
 *        ③关闭动画mOpenAnimator = ValueAnimator.ofInt(getScrollY(), scrollTo); 打开动画mOpenAnimator = ValueAnimator.ofInt(getScrollY(), scrollTo);
 *        都是利用getScrollY()获取滑动偏移量,第二个参数要么是起始值0,要么是固定值- 按照一定比例取view的Height;
 *        ④核心思想:move时滑动在move中定义好;滑动结束时,操作在up事件中定义好,利用动画实现对应的逻辑。
 *        ⑤动画使用的插值器为 OvershootInterpolator
 *        ⑥onTouchEvent中move事件,使用Scroll类来实现滑动效果,亦可以使用Scroller类进行优化 实现有过渡的滑动效果。
 *        ⑦setDrawingCacheEnabled(true)用以优化,提高绘图速度,防止卡顿 获取cache时会占有一定的内存在不用的时候要设为false。
 * 致谢    Thank you for your advice.
 */
public class ScrollPanel extends FrameLayout {
    private static final int SCROLL_TOP = 0;
    private static final float DEFAULT_SLIDE_DISTANCE = 200;
    public float ratio = (float) 0.382;
    private static int DEFAULT_CLOSE_DURATION = 250;
    private static int DEFAULT_OPEN_DURATION = 300;

    private boolean isOpened = false;
    private boolean isDragging = false;
    private float mLastEventY = 0;
    private float mFirstEventY = 0;

    private ValueAnimator mOpenAnimator;
    private ObjectAnimator mFadeOutAnimator;
    private AnimatorSet mAnimatorOpenSet;

    private ValueAnimator mCloseAnimator;
    private AnimatorSet mAnimatorCloseSet;
    private ObjectAnimator mFadeInAnimator;
    private SimplePanelListener mSimplePanelListener;

    private int height;
    public ScrollPanel(Context context) {
        super(context);
    }

    public ScrollPanel(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ScrollPanel(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        final float eventY = event.getY();

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE:
                if (isOpened && (getHeight() - getScrollY()) < eventY) {
                    return true;
                }

                if (isDragging) {
                    float offset = 0;
                    if (mLastEventY != 0) offset = mLastEventY - eventY;
                    setDrawingCacheEnabled(true);
                    scrollTo(0, (int) (getScrollY() + offset)); //offset值较小 用于首次启动滑动;getScrollY()值较大 存储了offset和值
                    mLastEventY = eventY;
                } else {
                    mLastEventY = 0;
                }

                setAlpha(0.85f);
                isDragging = true;
                break;

            case MotionEvent.ACTION_DOWN:
                if (isOpened && (getHeight() - getScrollY()) < eventY) {
                    return false;
                } else {
                    mFirstEventY = eventY;
                }
                break;


            case MotionEvent.ACTION_UP:
                if (!isDragging) return false;

                if (mFirstEventY > eventY && Math.abs(mFirstEventY - eventY) > DEFAULT_SLIDE_DISTANCE) { //判断手指上滑,同时滑动大于一个域值
                    open(); //触发条件 移动到指定位置 - 动画结束时要停在指定区域 动画接收参数为手离开屏幕是的纵坐标 第二个长参数是个负值 即为ViewPager的高度
                } else {
                    close(); //滑动不符合要求 回到原始区域 - 动画结束时返回原始状态 动画接收参数为手离开屏幕是的纵坐标 第二个长参数是0
                }

                clearDragging();
                break;
        }

        return true;
    }

    private void clearDragging() {
        mLastEventY = 0;
        mFirstEventY = 0;
        isDragging = false;
        setDrawingCacheEnabled(false);
    }

    public void open() {
        setAnimationCacheEnabled(true);
        setAnimatorOpenSet().start();
    }

    public void close() {
        setAnimationCacheEnabled(true);
        setAnimatorCloseSet().start();
    }

    private AnimatorSet setAnimatorOpenSet() {
        height = getResources().getDisplayMetrics().heightPixels ;
        int scrollTo = (int) (height * ratio);
        mOpenAnimator = ValueAnimator.ofInt(getScrollY(), scrollTo);

        mOpenAnimator.setInterpolator(new OvershootInterpolator());
        mOpenAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int height = (Integer) valueAnimator.getAnimatedValue();
                scrollTo(0, height);
            }
        });
        //在自定义View中使用ObjectAnimator属性动画时,第一个参数传入this即可
        mFadeOutAnimator = ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 0.85f);

        mAnimatorOpenSet = new AnimatorSet();
        mAnimatorOpenSet.setDuration(DEFAULT_OPEN_DURATION);
        mAnimatorOpenSet.play(mOpenAnimator).with(mFadeOutAnimator);
        mAnimatorOpenSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (mSimplePanelListener != null) {
                    mSimplePanelListener.onOpened();
                }
                isOpened = true;
            }

            @Override
            public void onAnimationCancel(Animator animator) {
            }

            @Override
            public void onAnimationRepeat(Animator animator) {
            }
        });

        return mAnimatorOpenSet;
    }

    private AnimatorSet setAnimatorCloseSet() {
        mCloseAnimator = ValueAnimator.ofInt(getScrollY(), SCROLL_TOP);
        mCloseAnimator.setInterpolator(new OvershootInterpolator());
        mCloseAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int height = (Integer) valueAnimator.getAnimatedValue();
                scrollTo(0, height);
            }
        });

        mFadeInAnimator = ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1f);

        mAnimatorCloseSet = new AnimatorSet();
        mAnimatorCloseSet.setDuration(DEFAULT_CLOSE_DURATION);
        mAnimatorCloseSet.play(mCloseAnimator).with(mFadeInAnimator);
        mAnimatorCloseSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (mSimplePanelListener != null) {
                    mSimplePanelListener.onClosed();
                }
                isOpened = false;
            }

            @Override
            public void onAnimationCancel(Animator animator) {
            }

            @Override
            public void onAnimationRepeat(Animator animator) {
            }
        });

        return mAnimatorCloseSet;
    }

    public boolean isOpened() {
        return isOpened;
    }

    public void setSlideRatio(float r) {
        this.ratio = r;
    }

    public float getSlideRatio() {
        return ratio;
    }

    public void addSimplePanelListener(SimplePanelListener listener) {
        mSimplePanelListener = listener;
    }

    public interface SimplePanelListener {
        abstract public void onOpened();

        abstract public void onClosed();
    }
}

四、如何使用ScrollPanel

ScrollPanel 继承自FrameLayout,且四个构造方法都实现了,这里我们按照刚开始的项目分析来实现:

1)在Activity的XML布局中静态的添加进ScrollPanel ;

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tab="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/dark_blue"
    tools:context=".RainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerInParent="true"
            android:layout_marginTop="90dp"
            android:gravity="center">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:lineSpacingExtra="10sp"
                android:text="@string/rain_poetry"
                android:textColor="@color/berry_blue"
                android:textSize="14sp" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/activity_main_bottom_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:orientation="vertical">

            <android.support.v4.view.ViewPager
                android:id="@+id/activity_main_view_pager"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:overScrollMode="never">

            </android.support.v4.view.ViewPager>
        </LinearLayout>
    </RelativeLayout>

    <com.love.rain.view.SimplePanel
        android:id="@+id/activity_main_panel_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible">

    </com.love.rain.view.SimplePanel>
</FrameLayout>

根布局是FrameLayout,上面一层放入自定义ViewScrollPanel,下面一层放入一个TextView(展示古诗)和ViewPager;

2)Activity代码里进行调用,将Fragmentadd进ScrollPanel;

getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.activity_main_panel_layout, mFrontPanelFragment)
                .commit();

同时调整ViewPager的高度,用以配合ScrollPanel进行内容展示,height和ratio同ScrollPanel中设置的:

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mLinearLayout.getLayoutParams();
        params.height = (int) (height*ratio);
        mLinearLayout.setLayoutParams(params);

总结一下:

手滑动时,面板随手指的滑动是通过重写onTouchEvent()方法,利用view的scrollTo()方法实现面板滑动;

手抬起时,面板滑动到指定位置是通过属性动画ValueAnimator实现的(用ObjectorAnimator当然也行)

最后,代码已上传至github , https://github.com/shenbuqingyun/ScrollPanel

谢谢阅读!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Kotlin自定义View中,可以通过重写`onInterceptTouchEvent`方法来限制子滑动控件的滑动。在这个方法中,你可以判断是否要拦截事件,并返回`true`或`false`来决定是否拦截事件。如果返回`true`,则表示拦截事件,子滑动控件将无法滑动;如果返回`false`,则表示不拦截事件,子滑动控件可以正常滑动。 下面是一个示例,展示如何在自定义View中限制子滑动控件的滑动。这个示例中创建了一个`CustomView`类,它包含一个`RecyclerView`作为子视图。我们想要在用户水平滑动`CustomView`时,防止`RecyclerView`的水平滑动,只允许垂直滑动: ``` class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private var initialX = 0f private var initialY = 0f private val recyclerView: RecyclerView init { LayoutInflater.from(context).inflate(R.layout.custom_view, this, true) recyclerView = findViewById(R.id.recyclerView) } override fun onInterceptTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { initialX = event.x initialY = event.y return false } MotionEvent.ACTION_MOVE -> { val dx = abs(event.x - initialX) val dy = abs(event.y - initialY) return dy > dx } else -> return super.onInterceptTouchEvent(event) } } } ``` 在`onInterceptTouchEvent`方法中,我们首先记录了用户按下手指时的坐标。然后,在用户移动手指时,我们计算水平和垂直方向上的滑动距离,并比较它们。如果垂直方向上的滑动距离大于水平方向上的滑动距离,则返回`true`,表示拦截事件,防止`RecyclerView`的滑动。否则,返回`false`,表示不拦截事件,`RecyclerView`可以正常滑动。 需要注意的是,在这个示例中,我们使用了`LayoutInflater`来从XML布局文件中获取`RecyclerView`视图。如果你使用了不同的方式来创建子视图,请相应地修改初始化代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值