首页 > 代码库 > Android翻页效果原理实现之曲线的实现

Android翻页效果原理实现之曲线的实现

尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!

炮兵技术分享镇楼

上一节我们通过引入折线实现了页面的折叠翻转效果,有了前面两节的基础呢其实曲线的实现可以变得非常简单,为什么这么说呢?因为曲线无非就是在折线的基础上对Path加入了曲线的实现,进而只是影响了我们的Region区域,而其他的什么事件啊、滑动计算啊之类的几乎都是不变的对吧,说白了就是对现有的折线View进行update改造,虽然是改造,但是我们该如何下手呢?首先我们来看看现实中翻页的效果应该是怎样的呢?如果大家身边有书或本子甚至一张纸也行,尝试以不同的方式去翻动它,你会发现除了我们前面两节曾提到过的一些限制外,还有一些special的现象:

一、翻起来的区域从侧面来看是一个有弧度的区域,如图所示侧面图:

技术分享

而我们将按照第一节中的约定忽略这部分弧度的表现,因为从正俯视的角度我们压根看不到弧度的效果,So~我们强制让其与页面平行:

技术分享

二、根据拖拽点距离页面高度的不同,我们可以得到不同的卷曲度:

技术分享

而其在我们正俯视点的表现则是曲线的弧度不同:

技术分享

同样的,我们按照第一节的约定,为了简化问题,我们将拖拽点距离页面的高度视为一个定值使在我们正俯视点表现的曲线起点从距离控件交点1/4处开始:

技术分享

三、如上一节末所说,在弯曲的区域图像也会有相似的扭曲效果

OK,大致的一个分析就是这样,我们根据分析结果可以得出下面的一个分析图:

技术分享

由上图配合我们上面的分析我们可知:DB = 1/4OB,FA = 1/4OA,而点F和点D分别为两条曲线(如无特殊声明,我们所说的曲线均为贝赛尔曲线,下同)的起点(当然你也可以说是终点无所谓),这时,我们以点A、B为曲线的控制点并以其为端点分别沿着x轴和y轴方向作线段AG、BC,另AG = AF、BC = BD,并令点G、C分别为曲线的终点,这样,我们的这两条二阶贝塞尔曲线就非常非常的特殊,例如上图中的曲线DC,它是由起始点D、C和控制点B构成,而BD = BC,也就是说三角形BDC是的等腰三角形,进一步地说就是曲线DC的两条控制杆力臂相等,进一步地我们可以推断出曲线DC的顶点J必定在直线DC的中垂线上,更进一步地我们可以根据《自定义控件其实很简单5/12》所说的二阶贝塞尔曲线公式得出当且仅当t = 0.5时曲线的端点刚好会在顶点J上,由此我们可以非常非常简单地得到曲线的顶点坐标。好了,YY归YY我们还是要回归到具体的操作中来,首先,我们要计算出点G、F、D、C的坐标值,这四点坐标也相当easy,就拿F点坐标来说,我们过点F分别作OM、AM的垂线:

技术分享

因为FA = 1/4OA,那么我们可以得到F点的x坐标Fx = a + 3/4MA,y坐标Fy = b + 3/4OM,而G点的x坐标Gx = a + MA - 1/4x;其他两点D、C就不多扯了,那么在代码中如何体现呢?首先,为了便于观察效果,我们先注释掉图片的绘制:

/*
 * 如果坐标点在原点(即还没发生触碰时)则绘制第一页
 */
if (mPointX == 0 && mPointY == 0) {
	// canvas.drawBitmap(mBitmaps.get(mBitmaps.size() - 1), 0, 0, null);
	return;
}

// 省略大量代码

//drawBitmaps(canvas);
并绘制线条:

canvas.drawPath(mPath, mPaint);
在上一节中我们在生成Path时将情况分为了两种:

if (sizeLong > mViewHeight) {
	//…………………………
} else {
	//…………………………
}
同样,我们也分开处理两种情况,那么针对sizeLong > mViewHeight的时候此时控件顶部的曲线效果已经是看不到了,我们只需考虑底部的曲线效果:

// 计算曲线起点
float startXBtm = btmX2 - CURVATURE * sizeShort;
float startYBtm = mViewHeight;

// 计算曲线终点
float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
float endYBtm = mPointY + (1 - CURVATURE) * mL;

// 计算曲线控制点
float controlXBtm = btmX2;
float controlYBtm = mViewHeight;

// 计算曲线顶点
float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;

/*
 * 生成带曲线的四边形路径
 */
mPath.moveTo(startXBtm, startYBtm);
mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPath.lineTo(mPointX, mPointY);
mPath.lineTo(topX1, 0);
mPath.lineTo(topX2, 0);
mPath.lineTo(bezierPeakXBtm, bezierPeakYBtm);
该部分的实际效果如下:

技术分享

PS:为了便于大家对参数的理解,我对每一个点的坐标都重新给予了一个引用其命名也浅显易懂,实际过程可以省略这一步简化代码

而当sizeLong <= mViewHeight时这时候不但底部有曲线效果,右侧也有:

/*
 * 计算参数
 */
float leftY = mViewHeight - sizeLong;
float btmX = mViewWidth - sizeShort;

// 计算曲线起点
float startXBtm = btmX - CURVATURE * sizeShort;
float startYBtm = mViewHeight;
float startXLeft = mViewWidth;
float startYLeft = leftY - CURVATURE * sizeLong;

/*
 * 限制左侧曲线起点
 */
if (startYLeft <= 0) {
	startYLeft = 0;
}

/*
 * 限制右侧曲线起点
 */
if (startXBtm <= 0) {
	startXBtm = 0;
}

// 计算曲线终点
float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
float endYBtm = mPointY + (1 - CURVATURE) * mL;
float endXLeft = mPointX + (1 - CURVATURE) * mK;
float endYLeft = mPointY - (1 - CURVATURE) * (sizeLong - mL);

// 计算曲线控制点
float controlXBtm = btmX;
float controlYBtm = mViewHeight;
float controlXLeft = mViewWidth;
float controlYLeft = leftY;

// 计算曲线顶点
float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;
float bezierPeakXLeft = 0.25F * startXLeft + 0.5F * controlXLeft + 0.25F * endXLeft;
float bezierPeakYLeft = 0.25F * startYLeft + 0.5F * controlYLeft + 0.25F * endYLeft;

/*
 * 生成带曲线的三角形路径
 */
mPath.moveTo(startXBtm, startYBtm);
mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPath.lineTo(mPointX, mPointY);
mPath.lineTo(endXLeft, endYLeft);
mPath.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);
效果如下:

技术分享

Path有了,我们就该考虑如何将其转换为Region,在这个过程中呢又一个问题,曲线路径不像上一节的直线路径我们可以轻易获得其范围区域,因为我们的折叠区域其实应该是这样的:

技术分享

如图所示红色路径区域,这部分区域则是我们折叠的区域,而事实上我们为了计算方便将整条二阶贝赛尔曲线都绘制了出来,也就是说我们的Path除了红色线条部分还包含了蓝色线条部分对吧,那么问题来了,如何将这两部分“做掉”呢?其实方法很多,我们可以在计算的时候就只生成半条曲线,这是方法一我们利用纯计算的方式,记得我在该系列文章开头曾说过翻页效果的实现可以有两种方式,一种是纯计算而另一种则是利用图形的组合思想,如何组合呢?这里对于区域的计算我们就不用纯计算的方式了,我们尝试用图形组合来试试。首先我们将Path转为Region看看是什么样的:

Region region = computeRegion(mPath);
canvas.clipRegion(region);
canvas.drawColor(Color.RED);
// canvas.drawPath(mPath, mPaint);
效果如下:

技术分享

可以看到我们没有封闭的Path形成的Region效果,事实呢跟我们需要的区域差距有点大,首先上下两个月半圆是多余的,其次目测少了一块对吧:

技术分享

如上图蓝色的那块,那么我们该如何把这块“补”回来呢?利用图形组合的思想,我们设法为该Region补一块矩形:

技术分享

然后差集掉两个月半圆不就成了?这部分代码改动较大,我先贴代码再说吧:

if (sizeLong > mViewHeight) {
	// 计算……额……按图来AN边~
	float an = sizeLong - mViewHeight;

	// 三角形AMN的MN边
	float largerTrianShortSize = an / (sizeLong - (mViewHeight - mPointY)) * (mViewWidth - mPointX);

	// 三角形AQN的QN边
	float smallTrianShortSize = an / sizeLong * sizeShort;

	/*
	 * 计算参数
	 */
	float topX1 = mViewWidth - largerTrianShortSize;
	float topX2 = mViewWidth - smallTrianShortSize;
	float btmX2 = mViewWidth - sizeShort;

	// 计算曲线起点
	float startXBtm = btmX2 - CURVATURE * sizeShort;
	float startYBtm = mViewHeight;

	// 计算曲线终点
	float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
	float endYBtm = mPointY + (1 - CURVATURE) * mL;

	// 计算曲线控制点
	float controlXBtm = btmX2;
	float controlYBtm = mViewHeight;

	// 计算曲线顶点
	float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
	float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;

	/*
	 * 生成带曲线的四边形路径
	 */
	mPath.moveTo(startXBtm, startYBtm);
	mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
	mPath.lineTo(mPointX, mPointY);
	mPath.lineTo(topX1, 0);
	mPath.lineTo(topX2, 0);

	/*
	 * 替补区域Path
	 */
	mPathTrap.moveTo(startXBtm, startYBtm);
	mPathTrap.lineTo(topX2, 0);
	mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
	mPathTrap.close();

	/*
	 * 底部月半圆Path
	 */
	mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
	mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
	mPathSemicircleBtm.close();

	/*
	 * 生成包含折叠和下一页的路径
	 */
	//暂时没用省略掉

	// 计算月半圆区域
	mRegionSemicircle = computeRegion(mPathSemicircleBtm);
} else {
	/*
	 * 计算参数
	 */
	float leftY = mViewHeight - sizeLong;
	float btmX = mViewWidth - sizeShort;

	// 计算曲线起点
	float startXBtm = btmX - CURVATURE * sizeShort;
	float startYBtm = mViewHeight;
	float startXLeft = mViewWidth;
	float startYLeft = leftY - CURVATURE * sizeLong;

	// 计算曲线终点
	float endXBtm = mPointX + (1 - CURVATURE) * (tempAM);
	float endYBtm = mPointY + (1 - CURVATURE) * mL;
	float endXLeft = mPointX + (1 - CURVATURE) * mK;
	float endYLeft = mPointY - (1 - CURVATURE) * (sizeLong - mL);

	// 计算曲线控制点
	float controlXBtm = btmX;
	float controlYBtm = mViewHeight;
	float controlXLeft = mViewWidth;
	float controlYLeft = leftY;

	// 计算曲线顶点
	float bezierPeakXBtm = 0.25F * startXBtm + 0.5F * controlXBtm + 0.25F * endXBtm;
	float bezierPeakYBtm = 0.25F * startYBtm + 0.5F * controlYBtm + 0.25F * endYBtm;
	float bezierPeakXLeft = 0.25F * startXLeft + 0.5F * controlXLeft + 0.25F * endXLeft;
	float bezierPeakYLeft = 0.25F * startYLeft + 0.5F * controlYLeft + 0.25F * endYLeft;

	/*
	 * 限制右侧曲线起点
	 */
	if (startYLeft <= 0) {
		startYLeft = 0;
	}

	/*
	 * 限制底部左侧曲线起点
	 */
	if (startXBtm <= 0) {
		startXBtm = 0;
	}

	/*
	 * 根据底部左侧限制点重新计算贝塞尔曲线顶点坐标
	 */
	float partOfShortLength = CURVATURE * sizeShort;
	if (btmX >= -mValueAdded && btmX <= partOfShortLength - mValueAdded) {
		float f = btmX / partOfShortLength;
		float t = 0.5F * f;

		float bezierPeakTemp = 1 - t;
		float bezierPeakTemp1 = bezierPeakTemp * bezierPeakTemp;
		float bezierPeakTemp2 = 2 * t * bezierPeakTemp;
		float bezierPeakTemp3 = t * t;

		bezierPeakXBtm = bezierPeakTemp1 * startXBtm + bezierPeakTemp2 * controlXBtm + bezierPeakTemp3 * endXBtm;
		bezierPeakYBtm = bezierPeakTemp1 * startYBtm + bezierPeakTemp2 * controlYBtm + bezierPeakTemp3 * endYBtm;
	}

	/*
	 * 根据右侧限制点重新计算贝塞尔曲线顶点坐标
	 */
	float partOfLongLength = CURVATURE * sizeLong;
	if (leftY >= -mValueAdded && leftY <= partOfLongLength - mValueAdded) {
		float f = leftY / partOfLongLength;
		float t = 0.5F * f;

		float bezierPeakTemp = 1 - t;
		float bezierPeakTemp1 = bezierPeakTemp * bezierPeakTemp;
		float bezierPeakTemp2 = 2 * t * bezierPeakTemp;
		float bezierPeakTemp3 = t * t;

		bezierPeakXLeft = bezierPeakTemp1 * startXLeft + bezierPeakTemp2 * controlXLeft + bezierPeakTemp3 * endXLeft;
		bezierPeakYLeft = bezierPeakTemp1 * startYLeft + bezierPeakTemp2 * controlYLeft + bezierPeakTemp3 * endYLeft;
	}

	/*
	 * 替补区域Path
	 */
	mPathTrap.moveTo(startXBtm, startYBtm);
	mPathTrap.lineTo(startXLeft, startYLeft);
	mPathTrap.lineTo(bezierPeakXLeft, bezierPeakYLeft);
	mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
	mPathTrap.close();

	/*
	 * 生成带曲线的三角形路径
	 */
	mPath.moveTo(startXBtm, startYBtm);
	mPath.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
	mPath.lineTo(mPointX, mPointY);
	mPath.lineTo(endXLeft, endYLeft);
	mPath.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);

	/*
	 * 生成底部月半圆的Path
	 */
	mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
	mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
	mPathSemicircleBtm.close();

	/*
	 * 生成右侧月半圆的Path
	 */
	mPathSemicircleLeft.moveTo(endXLeft, endYLeft);
	mPathSemicircleLeft.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);
	mPathSemicircleLeft.close();

	/*
	 * 生成包含折叠和下一页的路径
	 */
	//暂时没用省略掉

	/*
	 * 计算底部和右侧两月半圆区域
	 */
	Region regionSemicircleBtm = computeRegion(mPathSemicircleBtm);
	Region regionSemicircleLeft = computeRegion(mPathSemicircleLeft);

	// 合并两月半圆区域
	mRegionSemicircle.op(regionSemicircleBtm, regionSemicircleLeft, Region.Op.UNION);
}

// 根据Path生成的折叠区域
Region regioFlod = computeRegion(mPath);

// 替补区域
Region regionTrap = computeRegion(mPathTrap);

// 令折叠区域与替补区域相加
regioFlod.op(regionTrap, Region.Op.UNION);

// 从相加后的区域中剔除掉月半圆的区域获得最终折叠区域
regioFlod.op(mRegionSemicircle, Region.Op.DIFFERENCE);

/*
 * 根据裁剪区域填充画布
 */
canvas.clipRegion(regioFlod);
canvas.drawColor(Color.RED);
200行的代码我们就做了一件事就是正确计算Path,同样我们还是按照之前的分了两种情况来计算,第一种情况sizeLong > mViewHeight时,我们先计算替补的这块区域:

技术分享

如上代码46-49行

/*
 * 替补区域Path
 */
mPathTrap.moveTo(startXBtm, startYBtm);
mPathTrap.lineTo(topX2, 0);
mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
mPathTrap.close();
然后计算底部的月半圆Path:

技术分享

对应代码54-56行

/*
 * 底部月半圆Path
 */
mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPathSemicircleBtm.close();
将当前折叠区域和替补区域相加再减去月半圆Path区域我们就可以得到正确的折叠区域,对应代码64行和192-201行:

// 计算月半圆区域
mRegionSemicircle = computeRegion(mPathSemicircleBtm);

// ………………中间省略巨量代码………………

// 根据Path生成的折叠区域
Region regioFlod = computeRegion(mPath);

// 替补区域
Region regionTrap = computeRegion(mPathTrap);

// 令折叠区域与替补区域相加
regioFlod.op(regionTrap, Region.Op.UNION);

// 从相加后的区域中剔除掉月半圆的区域获得最终折叠区域
regioFlod.op(mRegionSemicircle, Region.Op.DIFFERENCE);
该情况下我们的折叠区域是酱紫的:

技术分享

两一种情况则稍微复杂些,除了要计算底部,我们还要计算右侧的月半圆Path区域,代码165-174:

/*
 * 生成底部月半圆的Path
 */
mPathSemicircleBtm.moveTo(startXBtm, startYBtm);
mPathSemicircleBtm.quadTo(controlXBtm, controlYBtm, endXBtm, endYBtm);
mPathSemicircleBtm.close();

/*
 * 生成右侧月半圆的Path
 */
mPathSemicircleLeft.moveTo(endXLeft, endYLeft);
mPathSemicircleLeft.quadTo(controlXLeft, controlYLeft, startXLeft, startYLeft);
mPathSemicircleLeft.close();
替补区域的计算,147-151:
/*
 * 替补区域Path
 */
mPathTrap.moveTo(startXBtm, startYBtm);
mPathTrap.lineTo(startXLeft, startYLeft);
mPathTrap.lineTo(bezierPeakXLeft, bezierPeakYLeft);
mPathTrap.lineTo(bezierPeakXBtm, bezierPeakYBtm);
mPathTrap.close();
区域的转换,184-188:
/*
 * 计算底部和右侧两月半圆区域
 */
Region regionSemicircleBtm = computeRegion(mPathSemicircleBtm);
Region regionSemicircleLeft = computeRegion(mPathSemicircleLeft);

// 合并两月半圆区域
mRegionSemicircle.op(regionSemicircleBtm, regionSemicircleLeft, Region.Op.UNION);
最终的计算跟上面第一种情况一样,效果如下:

技术分享

结合两种情况,我们可以得到下面的效果:

技术分享

然后,我们需要计算“下一页”的区域,同样,根据上一节我们的讲解,我们先获取折叠区域和下一页区域之和再减去折叠区域就可以得到下一页的区域:

mRegionNext = computeRegion(mPathFoldAndNext);
mRegionNext.op(mRegionFold, Region.Op.DIFFERENCE);
绘制效果如下:

技术分享

最后,我们结合上两节,注入数据:

/**
 * 绘制位图数据
 * 
 * @param canvas
 *            画布对象
 */
private void drawBitmaps(Canvas canvas) {
	// 绘制位图前重置isLastPage为false
	isLastPage = false;

	// 限制pageIndex的值范围
	mPageIndex = mPageIndex < 0 ? 0 : mPageIndex;
	mPageIndex = mPageIndex > mBitmaps.size() ? mBitmaps.size() : mPageIndex;

	// 计算数据起始位置
	int start = mBitmaps.size() - 2 - mPageIndex;
	int end = mBitmaps.size() - mPageIndex;

	/*
	 * 如果数据起点位置小于0则表示当前已经到了最后一张图片
	 */
	if (start < 0) {
		// 此时设置isLastPage为true
		isLastPage = true;

		// 并显示提示信息
		showToast("This is fucking lastest page");

		// 强制重置起始位置
		start = 0;
		end = 1;
	}

	/*
	 * 计算当前页的区域
	 */
	canvas.save();
	canvas.clipRegion(mRegionCurrent);
	canvas.drawBitmap(mBitmaps.get(end - 1), 0, 0, null);
	canvas.restore();

	/*
	 * 计算折叠页的区域
	 */
	canvas.save();
	canvas.clipRegion(mRegionFold);

	canvas.translate(mPointX, mPointY);

	/*
	 * 根据长短边标识计算折叠区域图像
	 */
	if (mRatio == Ratio.SHORT) {
		canvas.rotate(90 - mDegrees);
		canvas.translate(0, -mViewHeight);
		canvas.scale(-1, 1);
		canvas.translate(-mViewWidth, 0);
	} else {
		canvas.rotate(-(90 - mDegrees));
		canvas.translate(-mViewWidth, 0);
		canvas.scale(1, -1);
		canvas.translate(0, -mViewHeight);
	}

	canvas.drawBitmap(mBitmaps.get(end - 1), 0, 0, null);
	canvas.restore();

	/*
	 * 计算下一页的区域
	 */
	canvas.save();
	canvas.clipRegion(mRegionNext);
	canvas.drawBitmap(mBitmaps.get(start), 0, 0, null);
	canvas.restore();
}
最终效果如下:

技术分享

该部分的代码就不贴出了,大部分跟上一节相同,因为过两天要去旅游时间略紧这节略讲得粗糙,不过也没什么太大的改动,如果大家有不懂的地方可以留言或群里@哥,下一节我们将尝试实现翻页时图像扭曲的效果。

源码地址:传送门

Android翻页效果原理实现之曲线的实现