Android自定义ViewGroup实现淘宝商品详情页
作者:Danny_姜 发布时间:2022-10-28 00:13:01
最近公司在新版本上有一个需要,要在首页添加一个滑动效果,具体就是仿照X宝的商品详情页,拉到页面底部时有一个粘滞效果,如下图X东的商品详情页,如果用户继续向上拉的话就进入商品图文描述界面:
刚开始是想拿来主义,直接从网上找个现成的demo来用, 但是网上无一例外的答案都特别统一: 几乎全部是ScrollView中再套两个ScrollView,或者是一个LinearLayout中套两个ScrollView。 通过指定父view和子view的focus来切换滑动的处理界面---即通过view的requestDisallowInterceptTouchEvent方法来决定是哪一个ScrollView来处理滑动事件。
使用以上方法虽然可以解一时之渴, 但是存在几点缺陷:
1 扩展性不强 : 如果后续产品要求不止是两页滑动呢,是三页滑动呢, 难道要嵌3个ScrollView并通过N个判断来实现吗
2 兼容性不强 : 如果需要在某一个子页中需要处理左右滑动事件或者双指操作事件呢, 此方法就无法实现了
3 个人原因 : 个人喜欢自己掌握主动性,事件的处理自己来控制更靠谱一些(PS:就如同一份感情一样,需要细心去经营)
总和以上原因, 自己实现了一个ViewGroup,实现文章开头提到的效果, 废话不多说 直接上源码,以下只是部分主要源码,并对每一个方法都做了注释,可以参照注释理解。 文章最后对这个ViewGroup加了一点实现的细节以及如何使用此VIewGroup, 以及demo地址
package com.mcoy.snapscrollview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* @author jiangxinxing---mcoy in English
*
* 了解此ViewGroup之前, 有两点一定要做到心中有数
* 一个是对Scroller的使用, 另一个是对onInterceptTouchEvent和onTouchEvent要做到很熟悉
* 以下几个网站可以做参考用
* http://blog.csdn.net/bigconvience/article/details/26697645
* http://blog.csdn.net/androiddevelop/article/details/8373782
* http://blog.csdn.net/xujainxing/article/details/8985063
*/
public class McoySnapPageLayout extends ViewGroup {
。。。。
public interface McoySnapPage {
/**
* 返回page根节点
*
* @return
*/
View getRootView();
/**
* 是否滑动到最顶端
* 第二页必须自己实现此方法,来判断是否已经滑动到第二页的顶部
* 并决定是否要继续滑动到第一页
*/
boolean isAtTop();
/**
* 是否滑动到最底部
* 第一页必须自己实现此方法,来判断是否已经滑动到第二页的底部
* 并决定是否要继续滑动到第二页
*/
boolean isAtBottom();
}
public interface PageSnapedListener {
/**
* @mcoy
* 当从某一页滑动到另一页完成时的回调函数
*/
void onSnapedCompleted(int derection);
}
。。。。。。
/**
* 设置上下页面
* @param pageTop
* @param pageBottom
*/
public void setSnapPages(McoySnapPage pageTop, McoySnapPage pageBottom) {
mPageTop = pageTop;
mPageBottom = pageBottom;
addPagesAndRefresh();
}
private void addPagesAndRefresh() {
// 设置页面id
mPageTop.getRootView().setId(0);
mPageBottom.getRootView().setId(1);
addView(mPageTop.getRootView());
addView(mPageBottom.getRootView());
postInvalidate();
}
/**
* @mcoy add
* computeScroll方法会调用postInvalidate()方法, 而postInvalidate()方法中系统
* 又会调用computeScroll方法, 因此会一直在循环互相调用, 循环的终结点是在computeScrollOffset()
* 当computeScrollOffset这个方法返回false时,说明已经结束滚动。
*
* 重要:真正的实现此view的滚动是调用scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
*/
@Override
public void computeScroll() {
//先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
if (mScroller.getCurrY() == (mScroller.getFinalY())) {
if (mNextDataIndex > mDataIndex) {
mFlipDrection = FLIP_DIRECTION_DOWN;
makePageToNext(mNextDataIndex);
} else if (mNextDataIndex < mDataIndex) {
mFlipDrection = FLIP_DIRECTION_UP;
makePageToPrev(mNextDataIndex);
}else{
mFlipDrection = FLIP_DIRECTION_CUR;
}
if(mPageSnapedListener != null){
mPageSnapedListener.onSnapedCompleted(mFlipDrection);
}
}
//这里调用View的scrollTo()完成实际的滚动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//必须调用该方法,否则不一定能看到滚动效果
postInvalidate();
}
}
private void makePageToNext(int dataIndex) {
mDataIndex = dataIndex;
mCurrentScreen = getCurrentScreen();
}
private void makePageToPrev(int dataIndex) {
mDataIndex = dataIndex;
mCurrentScreen = getCurrentScreen();
}
public int getCurrentScreen() {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i).getId() == mDataIndex) {
return i;
}
}
return mCurrentScreen;
}
public View getCurrentView() {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i).getId() == mDataIndex) {
return getChildAt(i);
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see
* android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
* 重写了父类的onInterceptTouchEvent(),主要功能是在onTouchEvent()方法之前处理
* touch事件。包括:down、up、move事件。
* 当onInterceptTouchEvent()返回true时进入onTouchEvent()。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE)
&& (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_MOVE:
// 记录y与mLastMotionY差值的绝对值。
// yDiff大于gapBetweenTopAndBottom时就认为界面拖动了足够大的距离,屏幕就可以移动了。
final int yDiff = (int)(y - mLastMotionY);
boolean yMoved = Math.abs(yDiff) > gapBetweenTopAndBottom;
if (yMoved) {
if(MCOY_DEBUG) {
Log.e(TAG, "yDiff is " + yDiff);
Log.e(TAG, "mPageTop.isFlipToBottom() is " + mPageTop.isAtBottom());
Log.e(TAG, "mCurrentScreen is " + mCurrentScreen);
Log.e(TAG, "mPageBottom.isFlipToTop() is " + mPageBottom.isAtTop());
}
if(yDiff < 0 && mPageTop.isAtBottom() && mCurrentScreen == 0
|| yDiff > 0 && mPageBottom.isAtTop() && mCurrentScreen == 1){
Log.e("mcoy", "121212121212121212121212");
mTouchState = TOUCH_STATE_SCROLLING;
}
}
break;
case MotionEvent.ACTION_DOWN:
// Remember location of down touch
mLastMotionY = y;
Log.e("mcoy", "mScroller.isFinished() is " + mScroller.isFinished());
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST
: TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Release the drag
mTouchState = TOUCH_STATE_REST;
break;
}
boolean intercept = mTouchState != TOUCH_STATE_REST;
Log.e("mcoy", "McoySnapPageLayout---onInterceptTouchEvent return " + intercept);
return intercept;
}
/*
* (non-Javadoc)
*
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
* 主要功能是处理onInterceptTouchEvent()返回值为true时传递过来的touch事件
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e("mcoy", "onTouchEvent--" + System.currentTimeMillis());
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
if(mTouchState != TOUCH_STATE_SCROLLING){
// 记录y与mLastMotionY差值的绝对值。
// yDiff大于gapBetweenTopAndBottom时就认为界面拖动了足够大的距离,屏幕就可以移动了。
final int yDiff = (int) Math.abs(y - mLastMotionY);
boolean yMoved = yDiff > gapBetweenTopAndBottom;
if (yMoved) {
mTouchState = TOUCH_STATE_SCROLLING;
}
}
// 手指拖动屏幕的处理
if ((mTouchState == TOUCH_STATE_SCROLLING)) {
// Scroll to follow the motion event
final int deltaY = (int) (mLastMotionY - y);
mLastMotionY = y;
final int scrollY = getScrollY();
if(mCurrentScreen == 0){//显示第一页,只能上拉时使用
if(mPageTop != null && mPageTop.isAtBottom()){
scrollBy(0, Math.max(-1 * scrollY, deltaY));
}
}else{
if(mPageBottom != null && mPageBottom.isAtTop()){
scrollBy(0, deltaY);
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// 弹起手指后,切换屏幕的处理
if (mTouchState == TOUCH_STATE_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) velocityTracker.getYVelocity();
if (Math.abs(velocityY) > SNAP_VELOCITY) {
if( velocityY > 0 && mCurrentScreen == 1 && mPageBottom.isAtTop()){
snapToScreen(mDataIndex-1);
}else if(velocityY < 0 && mCurrentScreen == 0){
snapToScreen(mDataIndex+1);
}else{
snapToScreen(mDataIndex);
}
} else {
snapToDestination();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}else{
}
mTouchState = TOUCH_STATE_REST;
break;
default:
break;
}
return true;
}
private void clearOnTouchEvents(){
mTouchState = TOUCH_STATE_REST;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void snapToDestination() {
// 计算应该去哪个屏
final int flipHeight = getHeight() / 8;
int whichScreen = -1;
final int topEdge = getCurrentView().getTop();
if(topEdge < getScrollY() && (getScrollY()-topEdge) >= flipHeight && mCurrentScreen == 0){
//向下滑动
whichScreen = mDataIndex + 1;
}else if(topEdge > getScrollY() && (topEdge - getScrollY()) >= flipHeight && mCurrentScreen == 1){
//向上滑动
whichScreen = mDataIndex - 1;
}else{
whichScreen = mDataIndex;
}
Log.e(TAG, "snapToDestination mDataIndex = " + mDataIndex);
Log.e(TAG, "snapToDestination whichScreen = " + whichScreen);
snapToScreen(whichScreen);
}
private void snapToScreen(int dataIndex) {
if (!mScroller.isFinished())
return;
final int direction = dataIndex - mDataIndex;
mNextDataIndex = dataIndex;
boolean changingScreens = dataIndex != mDataIndex;
View focusedChild = getFocusedChild();
if (focusedChild != null && changingScreens) {
focusedChild.clearFocus();
}
//在这里判断是否已到目标位置~
int newY = 0;
switch (direction) {
case 1: //需要滑动到第二页
Log.e(TAG, "the direction is 1");
newY = getCurrentView().getBottom(); // 最终停留的位置
break;
case -1: //需要滑动到第一页
Log.e(TAG, "the direction is -1");
Log.e(TAG, "getCurrentView().getTop() is "
+ getCurrentView().getTop() + " getHeight() is "
+ getHeight());
newY = getCurrentView().getTop() - getHeight(); // 最终停留的位置
break;
case 0: //滑动距离不够, 因此不造成换页,回到滑动之前的位置
Log.e(TAG, "the direction is 0");
newY = getCurrentView().getTop(); //第一页的top是0, 第二页的top应该是第一页的高度
break;
default:
break;
}
final int cy = getScrollY(); // 启动的位置
Log.e(TAG, "the newY is " + newY + " cy is " + cy);
final int delta = newY - cy; // 滑动的距离,正值是往左滑<—,负值是往右滑—>
mScroller.startScroll(0, cy, 0, delta, Math.abs(delta));
invalidate();
}
}
McoySnapPage是定义在VIewGroup的一个接口, 比如说我们需要类似某东商品详情那样,有上下两页的效果。 那我就需要自己定义两个类实现这个接口,并实现接口的方法。getRootView需要返回当前页需要显示的布局内容;isAtTop需要返回当前页是否已经在顶端; isAtBottom需要返回当前页是否已经在底部
onInterceptTouchEvent和onTouchEvent决定当前的滑动状态, 并决定是有当前VIewGroup拦截touch事件还是由子view去消费touch事件
Demo地址
来源:https://blog.csdn.net/zxm317122667/article/details/47018357?utm_source=blogxgwz6


猜你喜欢
- 导入mybatis jar包右键pom.xml模拟springboot底层实现类1.定义接口@Mapperpublic interface
- 文件上传是开发中十分常见的功能,在servlet3.0之前,实现文件上传需要使用一些插件技术,比如:commons-fileuploadsm
- 建造者模式是Java中一种创建型设计模式,它的主要目的是将一个复杂对象的构建过程分解为多个简单对象的构建过程,并且使这些构建过程按照一定的顺
- 本文实例讲述了C#中Winform窗体Form的关闭按钮变灰色的方法,对C#程序设计有一定的借鉴价值,分享给大家供大家参考之用。具体方法如下
- 最近微框架spring-boot很火,笔者也跟风学习了一下,废话不多说,现给出一个读取配置文件的例子。首先,需要在pom文件中依赖以下jar
- java接口返回参数按照请求参数进行排序在项目实际开发中可能遇到过这种问题,接口请求参数顺序是[a,b,c],结果返回的数据是[bObjec
- 微信平台开放后倒是挺火的,许多第三方应用都想试下,毕竟可以利用微信建立起来的关系链来拓展自己的应用还是挺不错的,可以节约很多在社交方面的开销
- 在项目中引入springcloud中的gateway时报以下错误Description:Parameter 0 of method modi
- 直接上代码新建DecimalInputTextWatcher类继承TextWatcher (代码可直接复制使用) import androi
- 本文实例讲述了C#使用winform简单导出Excel的方法。分享给大家供大家参考,具体如下:using Excel;在项目中引入Excel
- 本文实例讲述了Android开发使用HttpURLConnection进行网络编程。分享给大家供大家参考,具体如下:——HttpURLCon
- 先给大家展示下效果图,对第三方开源 android tickplusdrawable相关知识感兴趣的朋友一起学习吧。Android tick
- 本文实例讲述了Android开发实现跟随手指的小球效果。分享给大家供大家参考,具体如下:配置DrawView类用于绘制小球public cl
- 将Excel转为PDF格式时,通常情况下转换出来的PDF页面都是默认的宽度大小;如果Excel表格数据的设计或布局比较宽或者数据内较少的情况
- 目录前言步入正题类的加载过程:1.加载2.验证3.准备4.解析5.初始化类加载器源码总结前言学生时代应抱着问题去学习一门语言,例如:在学习j
- 本文实例讲述了Android之复选框对话框用法。分享给大家供大家参考。具体如下:main.xml布局文件<?xml version=&
- 最近同事问我有没有有关于技术的电子书,我打开电脑上的小书库,但是邮件发给他太大了,公司又禁止用文件夹共享,于是花半天时间写了个小的文件上传程
- 写在前面: 线程堆栈应该是多线程类应用程序非功能问题定位的最有效手段,可以说是杀手锏。线程堆栈最擅长与分析如下类型问题:系统无缘无故CPU过
- 前言相信对于RxJava,大家应该都很熟悉,他最核心的两个字就是异步,诚然,它对异步的处理非常的出色,但是异步绝对不等于并发,更不等于线程安
- 从功能上说,可以分为两部分,分布式功能和数据功能。分布式功能主要是节点集群及集群附属功能如restful借口、集群性能检测功能等,数据功能主