Android 从零开始打造一个 3D立体旋转容器

通过我们实现的效果图可以发现:

1.切换的时候是一个3D立体的效果

2.布局中的每一个Item可以自由切换,且无限循环滚动

要解决上面的效果,我们需要什么技术点呢?

1.要想实现一个3D效果,我们可以借助Android中的Camera、Matrix

2.要想实现滚动,毫无疑问,我们需要借助Scroller

当然一切看起来很简单,其实不然,除此之外,你还需要对于滑动冲突进行处理等等,下面我开始介绍啦。

这就是我们这次项目的大致

这里写图片描述

3.实现


因为我们是要打造一个容器类,所以肯定得继承自 ViewGroup

按照一般的思路,我们肯定是先要进行一些变量的申明,onMeasure,onLayout操作

private void init(Context context) {

mCamera = new Camera();

mMatrix = new Matrix();

if (mScroller == null) {

mScroller = new Scroller(context);

}

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

measureChildren(widthMeasureSpec, heightMeasureSpec);

mWidth = getMeasuredWidth();

mHeight = getMeasuredHeight();

//滑动到设置的StartScreen位置

scrollTo(0, mStartScreen * mHeight);

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

int childTop = 0;

for (int i = 0; i < getChildCount(); i++) {

View child = getChildAt(i);

if (child.getVisibility() != GONE) {

child.layout(0, childTop,

child.getMeasuredWidth(), childTop + child.getMeasuredHeight());

childTop = childTop + child.getMeasuredHeight();

}

}

}

完成这些操作后,我们需要在onTouchEvent中进行滑动事件的处理

3.1 完成无限循环滑动滚动

我们的item数量是有限的,如何实现无限循环滚动呢?很简单,以3个item为例子(分别为1,2,3),我们让屏幕显示的是2

如此反复,屏幕所在的位置始终是第2个item所在的位置,这样就实现了我们的无限循环滚动,向下滚动也是如此

这里写图片描述

@Override

public boolean onTouchEvent(MotionEvent event) {

if (mVelocityTracker == null) {

mVelocityTracker = VelocityTracker.obtain();

}

mVelocityTracker.addMovement(event);

float y = event.getY();

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

if (!mScroller.isFinished()) {

//当上一次滑动没有结束时,再次点击,强制滑动在点击位置结束

mScroller.setFinalY(mScroller.getCurrY());

mScroller.abortAnimation();

scrollTo(0, getScrollY());

}

mDownY = y;

break;

case MotionEvent.ACTION_MOVE:

int realDelta = (int) (mDownY - y);

mDownY = y;

if (mScroller.isFinished()) {

//因为要循环滚动

recycleMove(realDelta);

}

break;

case MotionEvent.ACTION_UP:

mVelocityTracker.computeCurrentVelocity(1000);

float yVelocity = mVelocityTracker.getYVelocity();

//滑动的速度大于规定的速度,或者向上滑动时,上一页页面展现出的高度超过1/2。则设定状态为State.ToPre

if (yVelocity > standerSpeed || ((getScrollY() + mHeight / 2) / mHeight < mStartScreen)) {

mState = State.ToPre;

} else if (yVelocity < -standerSpeed || ((getScrollY() + mHeight / 2) / mHeight > mStartScreen)) {

//滑动的速度大于规定的速度,或者向下滑动时,下一页页面展现出的高度超过1/2。则设定状态为State.ToNext

mState = State.ToNext;

} else {

mState = State.Normal;

}

//根据mState进行相应的变化

changeByState(yVelocity);

if (mVelocityTracker != null) {

mVelocityTracker.recycle();

mVelocityTracker = null;

}

break;

}

//返回true,消耗点击事件

return true;

}

当手从屏幕上移开时,我们来看下这个方法changeByState(yVelocity);

这里写图片描述

我们以mState = State.ToPre 为例子来说明

/**

  • mState = State.ToPre 时进行的动作

  • @param yVelocity 竖直方向的速度

*/

private void toPreAction(float yVelocity) {

int startY;

int delta;

int duration;

mState = State.ToPre;

addPre();//增加新的页面

//计算松手后滑动的item个数

int flingSpeedCount= (yVelocity - standerSpeed) > 0 ? (int) (yVelocity - standerSpeed) : 0;

addCount = flingSpeedCount/ flingSpeed + 1;

//mScroller开始的坐标

startY = getScrollY() + mHeight;

setScrollY(startY);

//mScroller 移动的距离

delta = -(startY - mStartScreen * mHeight) - (addCount - 1) * mHeight;

duration = (Math.abs(delta)) * 3;

mScroller.startScroll(0, startY, 0, delta, duration);

addCount–;

}

然后会进入addPre方法中

/**

  • 把最后一个item移动到第一个item位置

*/

private void addPre() {

mCurScreen = ((mCurScreen - 1) + getChildCount()) % getChildCount();

int childCount = getChildCount();

View view = getChildAt(childCount - 1);

removeViewAt(childCount - 1);

addView(view, 0);

if (iStereoListener != null) {

iStereoListener.toPre(mCurScreen);

}

}

最后mScroller.startScroll(0, startY, 0, delta, duration); 开始执行。

执行的过程中会回调这个函数方法computeScroll

这里写图片描述

完成到这一步,我们的无限滑动滚动就算是完成了

3.2 实现3D切换效果。

正常情况下,我们自定义ViewGroup并不需要重写dispatchDraw 方法。

而这里我们则需要重写

@Override

protected void dispatchDraw(Canvas canvas) {

if (!isAdding && isCan3D) {

//当开启3D效果并且当前状态不属于 computeScroll中 addPre() 或者addNext()

//如果不做这个判断,addPre() 或者addNext()时页面会进行闪动一下

//我当时写的时候就被这个坑了,后来通过log判断,原来是computeScroll中的onlayout,和子Child的draw触发的顺序导致的。

//知道原理的朋友希望可以告知下

for (int i = 0; i < getChildCount(); i++) {

drawScreen(canvas, i, getDrawingTime());

}

} else {

isAdding = false;

super.dispatchDraw(canvas);

}

}

好,我们来drawScreen这个方法

private void drawScreen(Canvas canvas, int i, long drawingTime) {

int curScreenY = mHeight * i;

//屏幕中不显示的部分不进行绘制

if (getScrollY() + mHeight < curScreenY) {

return;

}

if (curScreenY < getScrollY() - mHeight) {

return;

}

float centerX = mWidth / 2;

float centerY = (getScrollY() > curScreenY) ? curScreenY + mHeight : curScreenY;

float degree = mAngle * (getScrollY() - curScreenY) / mHeight;

if (degree > 90 || degree < -90) {

return;

}

canvas.save();

mCamera.save();

mCamera.rotateX(degree);

mCamera.getMatrix(mMatrix);

mCamera.restore();

mMatrix.preTranslate(-centerX, -centerY);

mMatrix.postTranslate(centerX, centerY);

canvas.concat(mMatrix);

drawChild(canvas, getChildAt(i), drawingTime);

canvas.restore();

}

这里面的关键就在于

mCamera.rotateX(degree);

mMatrix.preTranslate(-centerX, -centerY);

mMatrix.postTranslate(centerX, centerY);

对于Camera我们知道我们整个布局都是平铺的,为什么会产生3D的效果呢?原因就是这个Camera类,人如其名,它就相当于一个相机,它对物体进行拍照。我们把相机正对物体拍摄,拍摄出的效果就是平面的,当我们把相机旋转了90度再来拍摄原来物体,物体就相当于旋转了90度。

Camera拍摄完毕后,然后把拍摄的参数值传到Matrix中,Matrix再和Canvas绑定,由Canvas进行绘制。最终显示在屏幕中。

那么preTranslate,postTranslate又是怎么一回事呢?

很简单,我们知道坐标系是以(0,0)作为参照点的。现在我们对拍摄的对象进行的缩放变形操作是在物体的中心。我们需要把物体的中心先移动到(0,0)位置,最后再移动到物体原来中心位置即可。

具体的大家可以参考下这篇文章

http://blog.youkuaiyun.com/rav009/article/details/7763223 ( Android postTranslate和preTranslate的理解)

不过对于Camera的坐标系我还有一点点疑问,我准备有机会写一篇关于Camera和Matrix文章。

3.3 滑动事件冲突的处理(请先查看更新说明)

完成上面两个步骤,那么我们就算Over了吗?

不!还有很重要的一点,就是事件冲突的处理。 举个例子:我们把手放到我们的容器上,系统怎么知道我们这个滑动事件是给容器还是要给容器的子类的呢?

(给容器自己,则进行滑动的操作,给容器的子类,则容器的子类可以进行点击事件的判断处理)

对于这种情况,我就很大度啦,全部交给容器子类处理!子类不要,OK,那容器你自己拿来玩吧。

————之所以不走寻常路:交给容器处理,容器不需要再交给子类

原因在于:容器拿到滑动事件只需要做滑动操作,而子类则不同,它有点击事件需要判断,一个容器有很多子类,而很多子类只有一个共同的容器,如果把控制权交给容器,那么容器怎么可能能够判断得出不同的子类到底需不需要这个滑动事件呢?所以,既然这么麻烦,那么统统交给子类处理。

交给子类处理,则容器中onInterceptTouchEvent需要做如下操作

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

return false;

}

return true;

}

而子类(用CustomEdittext为例)的dispatchTouchEvent需要做如下判断

@Override

public boolean dispatchTouchEvent(MotionEvent event) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

getParent().requestDisallowInterceptTouchEvent(true);

break;

case MotionEvent.ACTION_MOVE:

if (!isContain(event)) {

//子类不需要,交给容器自己处理

getParent().requestDisallowInterceptTouchEvent(false);

setFocusable(false);

} else {

//子类自己做操作

setFocusableInTouchMode(true);

}

break;

case MotionEvent.ACTION_UP:

break;

}

return super.dispatchTouchEvent(event);

}

在isContain中,我做的是点击的坐标是否在Edittext中,在则拦截,子类处理,不在,则交给父类容器

private boolean isContain(MotionEvent event) {

region.set(rect);

if (region.contains((int) event.getX(), (int) event.getY())) {

return true;

}

return false;

}

当然交给子类这样也导致了一个问题,就是我如果需要给容器中的子类进行点击事件,则都需要自定义一个View(例如上面的CustomEdittext 继承自Edittext)。

例如我就自定义了三个View,不过还是很简单的,几分钟的事就搞定了(在自定义View中dispatchTouchEvent进行判断)。

具体的可以参考代码。

这里写图片描述

更新说明 2016-8/5

滑动冲突之前我是把控制权交给了子类,这里https://github.com/Y-bao 这位作者提交的pull

request中将事件冲突交给了父类(StereoView)

,我这边通过了pull,我觉得写得挺好的,把点击事件的控制权转移给父类,就不需要自定义View。

如果你还想查看控制权转移给子类的代码(我之前的),可以点击这里

3.4 点击水纹波效果

细心的人会发现,我这里还有个RippleView。

没错这就是点击后有水纹波的效果。

Android本身可以在XML中用ripple实现,不过是Android 5.0以上,个人觉得兼容性不太好,就自己随便写了一个简易的,哈哈,效率不能保证,各位看客看看就好啦。

4.应用


4.1 定义的方法

使用方法也和其他的没有什么区别,我这里自定义了几个方法,我这里说明下。

自定义的方法

### PaddlePaddle 3.0 Fluid 的相关信息 PaddlePaddle 是百度开源的一个深度学习框架,而 Fluid 是其动态图机制的一部分。在 PaddlePaddle 2.x 版本之后,官方逐渐统一动静态图接口,并引入了更灵活的编程范式[^1]。 #### 训练配置准备 通过 `paddle.Model.prepare` 方法可以完成模型训练前的各项配置工作。这一步骤涉及以下几个核心组件: - **优化器**: 使用 `paddle.optimizer` 提供的各种优化算法来定义如何调整参数。 - **损失函数**: 利用 `paddle.nn.Loss` 中预置的或者自定义的损失计算逻辑。 - **评估指标**: 借助 `paddle.metric` 定义用于衡量模型表现的具体标准。 这些模块共同构成了完整的训练流程支持体系。 #### 性能调优与分布式实践 对于追求高效训练场景下的用户来说,《分布式深度学习最佳入门(踩坑)指南》提供了丰富的实践经验分享[^2]。尽管该文档主要围绕 OneFlow 展开讨论,但其中提到的一些通用原则同样适用于 PaddlePaddle 用户群体。例如,在大规模集群环境下实现高效的通信策略以及合理分配计算资源等方面的知识点具有较高的借鉴价值。 另外值得注意的是,随着版本迭代升级,具体API可能会有所变化,请始终参照最新官方文档获取最准确的信息。 ```python import paddle from paddle.vision.models import resnet18 from paddle.vision.datasets import Cifar10 from paddle.metric import Accuracy model = paddle.Model(resnet18()) optim = paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters()) loss_fn = paddle.nn.CrossEntropyLoss() acc_metric = Accuracy() train_dataset = Cifar10(mode=&#39;train&#39;) test_dataset = Cifar10(mode=&#39;test&#39;) model.prepare(optimizer=optim, loss=loss_fn, metrics=[acc_metric]) model.fit(train_data=train_dataset, eval_data=test_dataset, epochs=5, batch_size=64) ``` 上述代码片段展示了基于 PaddlePaddle 构建并运行一个简单图像分类任务的过程实例。 ### 更新日志查询方式 如果希望查找特定功能或改动记录,则可以通过访问 GitHub 上对应的 release notes 页面来进行详细了解。此外,“我的收藏” 功能允许快速定位感兴趣的内容项;只需利用页面右上角提供的搜索框输入关键词即可找到相关内容链接[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值