郭霖找人,手把手教你实现-App-360-度旋转看车效果,还学不会吗?

学习宝典

对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

//io.reactivex.Completable : 我用来封装单张图片的下载操作
val actionList = ArrayList()
//motorImageList是List,元素是36张图的网络地址
motorImageList.forEachIndexed { index, data ->
if (index == 0) //第一张图先展示,用于占位
actionList.add(getFirstImage(data))
else //其他图片先下载
actionList.add(getSingleImage(data))
}

//RxJava2
Completable.merge(actionList)//下载操作合并起来统一处理
.subscribeOn(Schedulers.io())//子线程操作
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())//最后回到主线程
.subscribe(object : CompletableObserver {
override fun onComplete() {
loadComplete()//资源下载完成了
}

override fun onError(e: Throwable) {
//这里表示出错了,可以告诉业务这功能凉了,咱也不提供reload机制…
}

override fun onSubscribe(d: Disposable) {
//disposableHelper 为 io.reactivex.disposables.CompositeDisposable
//可以在Activity的onDestroy时取消,这样可以防止异步导致内存泄漏
disposableHelper.addDisposable(d)
}
})
}

getFirstImage(data) 与 getSingleImage(data) 均使用Glide来 加载/下载 图片:

//第一张图直接展示到ImageView占位
private fun getFirstImage(url: String) =
Completable.create {
Glide.with(rotateView)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.into(rotateView) //into操作其实会自动切回主线程!
it.onComplete()
}.subscribeOn(AndroidSchedulers.mainThread())//这个必须在主线程啊

//其他图片走下载逻辑
private fun getSingleImage(url: String) =
Completable.create {
Glide.with(rotateView).asFile()//作为文件存起来
.load(url)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.submit()
it.onComplete()
}.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())

Glide中 asFile() 简单介绍下:

/**

  • Attempts to always load a {@link File} containing the resource, either using a file path
  • obtained from the media store (for local images/videos), or using Glide’s disk cache (for
  • remote images/videos).
  • For remote content, prefer {@link #downloadOnly()}.

  • @return A new request builder for obtaining File paths to content.
    */
    @NonNull
    @CheckResult
    public RequestBuilder asFile() {
    return as(File.class).apply(skipMemoryCacheOf(true));
    }

注释大意:asFile() 用于本地媒体库或者Glide硬盘缓存加载,远程资源建议使用downloadOnly() 方法,那么我们就来看看 downloadOnly() :

}

/**

  • Attempts always load the resource into the cache and return the {@link File} containing the
  • cached source data.
  • This method is designed to work for remote data that is or will be cached using {@link

  • com.bumptech.glide.load.engine.DiskCacheStrategy#DATA}. As a result, specifying a {@link
  • com.bumptech.glide.load.engine.DiskCacheStrategy} on this request is generally not recommended.
  • @return A new request builder for downloading content to cache and returning the cache File.
    */
    @NonNull
    @CheckResult
    public RequestBuilder downloadOnly() {
    return as(File.class).apply(DOWNLOAD_ONLY_OPTIONS);
    }

/**

  • A helper method equivalent to calling {@link #downloadOnly()} ()} and then {@link
  • RequestBuilder#load(Object)} with the given model.
  • @return A new request builder for loading a {@link Drawable} using the given model.
    */
    @NonNull
    @CheckResult
    public RequestBuilder download(@Nullable Object model) {
    return downloadOnly().load(model);

}

啊哈,还有个 download(@Nullable Object model) 方法,直接取代 asFile().load(url).diskCacheStrategy(DiskCacheStrategy.DATA) 不就行了么,一句话搞定啊!

一般情况下的确是的,但是让我们来看一看 DOWNLOAD_ONLY_OPTIONS :

private static final RequestOptions DOWNLOAD_ONLY_OPTIONS =
diskCacheStrategyOf(DiskCacheStrategy.DATA).priority(Priority.LOW).skipMemoryCache(true);

Em… priority(Priority.LOW) 这个我无法接受,毕竟36张图片下载不能排到最后啊!至于为啥我没使用 Priority.HIGH ,是因为我觉得正常优先级就够了,目前业务情况加不加没啥区别。

diskCacheStrategyOf(DiskCacheStrategy.DATA) 大概如下所说:

DiskCacheStrategy.NONE :表示不缓存任何内容。
DiskCacheStrategy.DATA :表示只缓存原始图片。
DiskCacheStrategy.RESOURCE :表示只缓存转换过后的图片。
DiskCacheStrategy.ALL :表示既缓存原始图片,也缓存转换过后的图片。
DiskCacheStrategy.AUTOMATIC :表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)。

最后看一下 submit() :

/**

  • Returns a future that can be used to do a blocking get on a background thread.
  • This method defaults to {@link Target#SIZE_ORIGINAL} for the width and the height. However,

  • since the width and height will be overridden by values passed to {@link
  • RequestOptions#override(int, int)}, this method can be used whenever {@link RequestOptions}
  • with override values are applied, or whenever you want to retrieve the image in its original
  • size.
  • @see #submit(int, int)
  • @see #into(Target)
    */
    @NonNull
    public FutureTarget submit() {
    return submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
    }

submit() 这个需要异步调用,内部调用可以指定宽高的方法 submit(int, int) ,Target.SIZE_ORIGINAL 表示使用资源的原始宽高。值得一提的是这个方法会被 RequestOptions#override(int, int) 覆盖宽高。

好了,整个操作下来图片下载就完成了。我们不需要自己缓存资源到本地,完全使用了Glide的缓存机制。

当然有一点得说下,Glide本身基于 DiskLruCache机制 ,如果用户不经常查看这个图,资源是会被清理了。我认为这种情况可以不用考虑,下次这段操作再下载就完事儿了。

#####自动旋转

图片准备完毕了,是时候自动旋转一下,告诉用户我们这个是可以滑动展示的!直接上代码:

private var anim: ValueAnimator? = null

private fun loadComplete() {
actionistener?.onSourceReady()//回调业务,资源准备完毕

//android.animation.IntEvaluator
anim = ValueAnimator.ofObject(IntEvaluator(), 1, motorImageList.size)
anim?.duration = 1800
anim?.addUpdateListener {
val value = it.animatedValue as Int
if (currentIndex != value) { //这个value是会重复的
currentIndex = if (value >= motorImageList.size) {//到达上界
0 //因为从1开始的,所以这里用0表示结束
} else {
value
}
Glide.with(rotateView).load(motorImageList[currentIndex]).dontAnimate().placeholder(rotateView.drawable).into(rotateView)

if (currentIndex == 0) {// 0表示结束了
isSourceReady = true //这个内部标记资源加载完毕了
initTimer() //这个下面再说,嘿嘿!
}
}
}
anim?.start()
}

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
//RxJava 的释放
disposableHelper.dispose()
timerDisposable?.dispose()
//动画记得要释放…
anim?.removeAllUpdateListeners()
anim?.cancel()
}

由于动画这个考验数学功底,我明显不行啊😭!所以我就简单搞了搞属性动画,1800毫秒内取一下 1到图片数量(我们APP是36)的数字(其实就是图片List的index),然后使用Glide加载一下图片。

为啥从1开始,因为我们使用了第一张图片占位了(index 为 0),所以就不参与动画计时了。

dontAnimate() 这里使用的本意是禁止图片切换时的动画效果,不过我看源码貌似是禁止Gif的动画,不过写了不嫌多。

placeholder(rotateView.drawable) 这个才是 精髓 啊,使用当前ImageView的图片进行占位,这样视觉效果才会连贯,不然图片切换时会出现闪烁!

滑动旋转

重要的滑动展示来了,先看我们的自定义的 ImageView :

class RotateImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ImageView(context, attrs, defStyleAttr) {

//RotateController 自定义的控制器,下载逻辑就在它里面完成的
val controller: RotateController = RotateController(this, context)

override fun onTouchEvent(event: MotionEvent?): Boolean {
return controller.onTouchEvent(event)//事件交给控制器处理
}
}

这个看起来比较简单,让我们看下 controller.onTouchEvent(event) :

fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
if (it.action == MotionEvent.ACTION_UP || it.action == MotionEvent.ACTION_CANCEL) {
accumulate = 0
}
}

//让“爸爸”View不要打断触摸事件,不然我们的ImageView可能接收不到了
rotateView.parent?.requestDisallowInterceptTouchEvent(true)
//android.view.GestureDetector
return gestureDetector.onTouchEvent(event)
}

在 MotionEvent.ACTION_UP 与 MotionEvent.ACTION_CANCEL 时候把 accumulate 置为0,这个变量下面详细说明。

先让我们看一下资源准备好之后的 initTimer 方法:

private fun initTimer() {
timerDisposable = Observable.interval(40, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableObserver<Long?>() {
override fun onNext(time: Long) {
if (accumulate > 0) {
accumulate–
addIndex()
} else if (accumulate < 0) {
accumulate++
reduceIndex()
}
}

override fun onError(e: Throwable) {
//出错了,功能凉了,该咋咋滴吧…
}

override fun onComplete() {}
})
}

这里直接用RxJava开启了 40毫秒 的定时器(其他方式的定时器也行),40毫秒是我试验下来选的一个差不多的值。

当 accumulate 大于0时,我们将 accumulate 减1,并且展示 后一张 图片,看下 addIndex():

private fun addIndex() {
if (isSourceReady) {//资源准备好了,如果没准备好,则不处理
currentIndex++ //当前图片的index,这里加1,准备展示下一张图

if (currentIndex >= motorImageList.size) //如果index大于等于图片总数
currentIndex = 0
//Glide展示图片
Glide.with(rotateView).load(motorImageList[currentIndex]).dontAnimate().placeholder(rotateView.drawable).into(rotateView)
}
}

当 accumulate 小于0时,我们将 accumulate 加1,并且展示 前一张 图片,看下 reduceIndex():

private fun reduceIndex() {
if (isSourceReady) {
currentIndex–

if (currentIndex < 0)
currentIndex = motorImageList.size - 1

Glide.with(rotateView).load(motorImageList[currentIndex]).dontAnimate().placeholder(rotateView.drawable).into(rotateView)
}
}

accumulate 等于0时,不做任何操。这也就是上面在 MotionEvent.ACTION_UP 与 MotionEvent.ACTION_CANCEL 时候把 accumulate 置为0,表示手指离开屏幕,立即停止图片滑动!

所以 accumulate 用来存储还剩几张图需要播放 :

正数:表示向后等待展示的数量
负数:表示向前等待展示的数量
0 :表示保持当前图片不懂

而我们 定时器的作用就是每隔一段时间,去读取 accumulate 的值

只要 accumulate 不为0,就表示一直有 前一帧/后一帧 需要展示。每隔40毫秒就会执行换 前一张/后一张 的图片操作。
accumulate 等于0,就表示一直是当前的图片

那么我们什么时候操作 accumulate 呢?

在android.view.GestureDetector 处理手势的时候:

gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener {
override fun onShowPress(e: MotionEvent?) {
//用不到
}

override fun onSingleTapUp(e: MotionEvent?): Boolean {
//单击事件,这个我司业务用来跳转横屏展示
actionistener?.onClick()
return true

尾声

如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《2019-2021字节跳动Android面试历年真题解析》

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

id源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-jpKZMI58-1715475178789)]

《2019-2021字节跳动Android面试历年真题解析》

[外链图片转存中…(img-Xsut2xyE-1715475178789)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值