Android高阶转场动画-ShareElement完全攻略
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
看完本文你能学到什么:
1、ShareElement是什么以及基本用法
2、理解ShareElement是如何运作的
3、掌握ShareElement的进阶用法(Fresco、Glide、RecyclerView&ViewPager图片视频混合的情况下如何实现ShareElement动画)
4、一个封装好可以简单实现以上ShareElement动画的开源库 YcShareElement(https://github.com/yellowcath/YcShareElement)
- 什么是ShareElement
- ShareElement应用场景
- 如何实现ShareElement
- Activity A
- Activity B
- ShareElement完整流程
- SharedElementCallback
- 1、moveSharedElementsToOverlay()
- 2、setSharedElementState()
- 3、setOriginalSharedElementState()
- 返回流程
- 自定义Transition
- YcShareElement
- 普通页面使用步骤
- 1、打开WindowContentTransition开关
- 2、生成Bundle,然后startActivity
- 3、新的页面里设置并启动Transition
- 图片&视频页面使用步骤
- 1、打开WindowContentTransition开关
- 2、生成Bundle,然后startActivity
- 3、Activity B设置Transtion动画
- 4、Activity B的ViewPager加载好之后启动Transition
- 5、Activity B实现finishAfterTransition()函数
- 6、Activity A实现onActivityReenter()函数
- 如何扩展支持自定义View的Transition动画
- 1、确定所需参数
- 2、继承ViewStateSaver,获取所需参数
- 3、自定义Transition
- 4、将自定义的Transition加入到YcShareElement
- 普通页面使用步骤
- 广告时间
什么是ShareElement
ShareElement即两个Activity(或Fragment)之间切换时的共享元素,如下图,可以看到,选中的联系人头像和名字直接很自然地过渡到了下一页的位置,这两个就是本次切换动画的ShareElement
ShareElement这一套也能实现同一个Activity(Fragment)内部的复杂切换动画,不过因为在Activity内部做动画有太多现成的手段,所以本文不涉及这方面内容
ShareElement应用场景
以我个人的观点,ShareElement最好的应用场景之一就是现在的以图片、视频为主的内容流APP。下面是我司应用了ShareElement的app与某app的用户浏览体验对比
如何实现ShareElement
或许很多人第一次看到类似这种MaterialDesign里炫酷的界面切换效果时,也会有和我一样的疑惑, 这么炫酷的效果是怎么实现的?两个Activity之间怎么能切换的如此自然? 实际上,这样的效果单凭开发者自己确实很难实现,幸运的是,在Api21之后,官方提供了一套现成的工具来帮我们实现这个功能,核心就是以下四个函数:
Window.setEnterTransition()Window.setExitTransition()Window.setSharedElementEnterTransition()Window.setSharedElementExitTransition()
这里我们先以一个简单的仿官方联系人效果的Demo介绍下实现ShareElement的基本流程
Activity A
public class ContactsActivity extends Activity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {/***1、打开FEATURE_CONTENT_TRANSITIONS开关(可选),这个开关默认是打开的*/requestWindowFeature(Window.FEATURE_CONTENT_TRANSITIONS); /***2、设置除ShareElement外其它View的退出方式(左边滑出)*/getWindow().setExitTransition(new Slide(Gravity.LEFT));super.onCreate(savedInstanceState);...}@Overridepublic void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {.../***3、设置两个Activity的共享元素的TransitionName,*两个Activity的共享元素必须设置同样的TransitionName*/ViewCompat.setTransitionName(avatarImg,"avatar:"+item.name);ViewCompat.setTransitionName(nameTxt,"name:"+item.name);}private void gotoDetailActivity(Contacts contacts, final View avatarImg, final View nameTxt) {Intent intent = new Intent(ContactActivity.this,DetailActivity.class);Pair<View,String> pair1 = new Pair<>((View)avatarImg,ViewCompat.getTransitionName(avatarImg));Pair<View,String> pair2 = new Pair<>((View)nameTxt,ViewCompat.getTransitionName(nameTxt));/***4、生成带有共享元素的Bundle,这样系统才会知道这几个元素需要做动画*/ActivityOptionsCompat activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(ContactActivity.this, pair1, pair2);ActivityCompat.startActivity(ContactActivity.this,intent,activityOptionsCompat.toBundle());}
}
Activity B
public class DetailActivity extends Activity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_detail);ImageView avatarImg = findViewById(R.id.avatar);TextView nameTxt = findViewById(R.id.name);Contacts item = getIntent().getParcelableExtra(ContactsActivity.KEY_CONTACTS);/*** 1、设置相同的TransitionName*/ViewCompat.setTransitionName(avatarImg,"avatar:"+item.name);ViewCompat.setTransitionName(nameTxt,"name:"+item.name);/*** 2、设置WindowTransition,除指定的ShareElement外,其它所有View都会执行这个Transition动画*/getWindow().setEnterTransition(new Fade());getWindow().setExitTransition(new Fade());/*** 3、设置ShareElementTransition,指定的ShareElement会执行这个Transiton动画*/TransitionSet transitionSet = new TransitionSet();transitionSet.addTransition(new ChangeBounds());transitionSet.addTransition(new ChangeTransform());transitionSet.addTarget(avatarImg);transitionSet.addTarget(nameTxt);getWindow().setSharedElementEnterTransition(transitionSet);getWindow().setSharedElementExitTransition(transitionSet);}
}
运行一下看效果
可以看到,头像和名字位置是很顺利的过渡了,但是名字的大小和颜色并没有和之前的官方demo一样完美过渡,这是因为官方默认提供的Transition动画只有以下几个:
ChangeBounds:View的大小与位置动画
ChangeTransform:View的缩放与旋转动画
ChangeClipBounds:View的裁剪区域(View.getClipBounds())动画
ChangeScroll:处理View的scrollX与scrollY属性
ChangeImageTransform:处理ImageView的ScaleType属性(这个在实际项目中有网络图片时不好用,后文有解决方案)
可以看到并没有对TextView的字体大小和颜色做处理
俗话说得好,自己动手丰衣足食,我们来自定义一个Transition动画
public class ChangeTextTransition extends Transition {@Overridepublic void captureStartValues(TransitionValues transitionValues) {}@Overridepublic void captureEndValues(TransitionValues transitionValues) {}@Overridepublic Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues){return super.createAnimator(sceneRoot, startValues, endValues);}
}
Transition的设计思路是,每一个Transition类负责整个动画的一部分,在这个例子里,TextView的平移和大小变化已经由ChangeBounds实现了,因此我们自定义的Transition只需要实现字体大小和颜色的动画就行了
可以看到,自定义Transition需要实现三个函数,要达到我们想要的效果,需要:
1、在captureStartValues里获取到TextView在Activity A里的状态(字体和颜色)
2、在captureEndValues里获取到TextView在Activity B里的状态(字体和颜色)
3、在createAnimator里利用获取到的初始和结束状态创建一个Animator
最简单的方法就是在创建ChangeTextTransition的时候传入相应的参数,不过缺点是:
1、进入和退出时需要不同的参数
2、如果有多个TextView都需要做动画怎么办?有多少传多少参数?
3、不够优雅 :)
想要解决以上缺点,就需要了解ShareElement动画的完整流程
ShareElement完整流程
要实现自定义的ShareElement动画,一切的重点都在于Activity对外暴露的回调SharedElementCallback
SharedElementCallback
你可以通过以下两个函数设置这个回调
activity.setExitSharedElementCallback(callback)
activity.setEnterSharedElementCallback(callback)
SharedElementCallback有以下7个回调,最麻烦的是,这几个回调在进入和退出时的调用顺序是不一致的
SharedElementCallback是一个抽象类,所有回调都有默认实现
/***最先调用,用于动画开始前替换ShareElements,比如在Activity B翻过若干页大图之后,返回Activity A*的时候需要缩小回到对应的小图,就需要在这里进行替换*/public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {}/***表示ShareElement已经全部就位,可以开始动画了*/public void onSharedElementsArrived(List<String> sharedElementNames, List<View> sharedElements, OnSharedElementsReadyListener listener) {}/***在之前的步骤里(onMapSharedElements)被从ShareElements列表里除掉的View会在此回调,*不处理的话默认进行alpha动画消失*/public void onRejectSharedElements(List<View> rejectedSharedElements) {}/***在这里会把ShareElement里值得记录的信息存到为Parcelable格式,以发送到Activity B*默认处理规则是ImageView会特殊记录Bitmap、ScaleType、Matrix,其它View只记录大小和位置*/public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, RectF screenBounds) {}/***在这里会把Activity A传过来的Parcelable数据,重新生成一个View,这个View的大小和位置会与Activity A里的*ShareElement一致,*/public View onCreateSnapshotView(Context context, Parcelable snapshot) {}public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {}public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {}
下图展示了从Activity A切换到Activity B,SharedElementCallback被调用的时序
查看原图
图里我标了几个值得注意的点:
1、moveSharedElementsToOverlay()
protected void moveSharedElementsToOverlay() {...ViewGroup decor = getDecor();if (decor != null) {...for (int i = 0; i < numSharedElements; i++) {View view = mSharedElements.get(i);if (view.isAttachedToWindow()) {...GhostView.addGhost(view, decor, tempMatrix);...}}}}
ViewOverlay在Android4.3加入,其父类是ViewGroup,如果想在一个View最上层展示一些东西,可以调用View.getOverlay(),然后调用ViewOverlay.add(drawable)或者ViewOverlay.getOverlayView().addView()函数添加到ViewOverlay.
GhostView可以在不改变一个View的Parent的情况下,把View渲染到另一个ViewGroup里面去.
moveSharedElementsToOverlay()函数实质就是把ShareElementView渲染到整个Activity的最上层(DecorView的ViewOverlay), 这样在做动画时ShareElementView就不会被任何别的东西遮挡住.
2、setSharedElementState()
这里需要提一点,在这个Demo里,整个ShareElement动画过程中,做动画的都只有Activity B里的ShareElement,Activity A里的ShareElement唯一的作用就是提供位置大小等参数,然后这些参数在setSharedElementState()函数里被设置到Activity B里对应的View上.
private void setSharedElementState(View view, String name, Bundle transitionArgs,Matrix tempMatrix, RectF tempRect, int[] decorLoc) {...if (view instanceof ImageView) {...imageView.setScaleType(scaleType);if (scaleType == ImageView.ScaleType.MATRIX) {float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX);tempMatrix.setValues(matrixValues);imageView.setImageMatrix(tempMatrix);}}....view.setLeft(0);view.setTop(0);view.setRight(Math.round(width));view.setBottom(Math.round(height));...view.measure(widthSpec, heightSpec);view.layout(x, y, x + width, y + height);}
可以看见,如果不是ImageView,系统只处理了大小位置的信息,这也是我们前面的动画里为什么名字的过渡效果那么不自然,因为系统压根就没管字体大小和颜色之类的东西.
(如果是进入动画)在设置好信息之后,会先调用SharedElementCallback.onSharedElementStart,然后就是Transition.captureStartValues()
3、setOriginalSharedElementState()
protected static void setOriginalSharedElementState(ArrayList<View> sharedElements,ArrayList<SharedElementOriginalState> originalState) {for (int i = 0; i < originalState.size(); i++) {View view = sharedElements.get(i);SharedElementOriginalState state = originalState.get(i);if (view instanceof ImageView && state.mScaleType != null) {ImageView imageView = (ImageView) view;imageView.setScaleType(state.mScaleType);if (state.mScaleType == ImageView.ScaleType.MATRIX) {imageView.setImageMatrix(state.mMatrix);}}view.setElevation(state.mElevation);view.setTranslationZ(state.mTranslationZ);int widthSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredWidth,View.MeasureSpec.EXACTLY);int heightSpec = View.MeasureSpec.makeMeasureSpec(state.mMeasuredHeight,View.MeasureSpec.EXACTLY);view.measure(widthSpec, heightSpec);view.layout(state.mLeft, state.mTop, state.mRight, state.mBottom);}}
在Transition.captureStartValues()之后,接着setOriginalSharedElementState()函数会恢复view在Activity B里的状态, 再调用Transition.captureEndValues().
这时候动画的起始和结束状态的已经获得了,TransitionManager就会在onPreDraw()的回调里执行Transiton.playTransition(), 这里面会调用Transition.createAnimator()函数,然后执行这个Animator.这时候ShareElement动画就真正开始了.
返回流程
返回流程这里就不详细分析了,直接给出各个回调的调用顺序
ActivityB.onMapSharedElements()
->ActivityA.onMapSharedElements()
->ActivityA.onCaptureSharedElementSnapshot()
->ActivityB.onCreateSnapshotView()
->ActivityB.onSharedElementEnd()
->ActivityB.onSharedElementStart() //你没有看错,就是先End再Start
->ActivityB.onSharedElementsArrived()
->ActivityA.onSharedElementsArrived()
->ActivityA.onRejectSharedElements()
->ActivityA.onCreateSnapshotView()
->ActivityA.onSharedElementStart()
->ActivityA.onSharedElementEnd()
自定义Transition
由上面的分析可以得出,要实现TextView的Transition,需要以下步骤
查看原图
实际代码可参考ChangeTextTransition
YcShareElement
demo里用了
GSYVideoPlayer展示视频
Fresco、Glide展示图片
YcShareElement提供了两个demo,一个是上面的联系人demo,另一个实现了图片、视频混合的列表页与详情页之间的ShareElement动画,如下图
这里面的关键点如下:
1、Glide图片的ShareElement动画
ImageView在动画过程中要经历默认背景色->小缩略图->大图三个阶段,如何在这三个阶段里做到无缝切换
参考:ChangeOnlineImageTransition
2、Fresco图片的ShareElement动画
Fresco提供了内置的DraweeTransition,但是如果设置了缩略图,图片就会变形,并且必须在构造函数里提供动画起始的ScaleType信息,简单的情况很好用,在复杂的情况下不太友好
参考:AdvancedDraweeTransition
3、从列表的Webp动图到详情页的视频ShareElement动画
这个在实现了以上两点之后其实就很简单了,实际上就是视频的封面图做动画
普通页面使用步骤
1、打开WindowContentTransition开关
YcShareElement.enableContentTransition(getApplication());
由于这个开关默认是打开的,因此这一句是可选的,担心遇到奇葩手机关掉这个开关的可以调用
2、生成Bundle,然后startActivity
private void gotoDetailActivity(){Intent intent = new Intent(this, DetailActivity.class);Bundle bundle = YcShareElement.buildOptionsBundle(ContactActivity.this, new IShareElements() {@Overridepublic ShareElementInfo[] getShareElements() {return new ShareElementInfo[]{new ShareElementInfo(mAvatarImg),new ShareElementInfo(mNameTxt, new TextViewStateSaver())};}});ActivityCompat.startActivity(ContactActivity.this, intent, bundle);}
3、新的页面里设置并启动Transition
public class DetailActivity extends Activity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {...YcShareElement.setEnterTransition(this, new IShareElements() {@Overridepublic ShareElementInfo[] getShareElements() {return new ShareElementInfo[]{new ShareElementInfo(avatarImg),new ShareElementInfo(nameTxt, new TextViewStateSaver())};}});YcShareElement.startTransition(this);}
}
YcShareElement.setEnterTransition()默认会暂停Activity的Transtion动画,直到调用YcShareElement.startTransition(), 在这种不需要等待ShareElement加载的简单页面,可以将第三个参数传false,就不会暂停ActivityB的Transition动画了,如下
protected void onCreate(@Nullable Bundle savedInstanceState) {...YcShareElement.setEnterTransition(this, new IShareElements() {@Overridepublic ShareElementInfo[] getShareElements() {return new ShareElementInfo[]{new ShareElementInfo(avatarImg),new ShareElementInfo(nameTxt, new TextViewStateSaver())};}},false);}
效果如下:
图片&视频页面使用步骤
1、打开WindowContentTransition开关
YcShareElement.enableContentTransition(getApplication());
2、生成Bundle,然后startActivity
Bundle options = YcShareElement.buildOptionsBundle(getActivity(), this);startActivityForResult(intent, REQUEST_CONTENT, options);
3、Activity B设置Transtion动画
protected void onCreate(@Nullable Bundle savedInstanceState) {YcShareElement.setEnterTransition(this, this);...}
4、Activity B的ViewPager加载好之后启动Transition
@Overridepublic void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {...加载数据...YcShareElement.postStartTransition(getActivity());}
这时候进入动画就执行完毕了,接下来要处理滑动若干页之后返回列表页的情况
5、Activity B实现finishAfterTransition()函数
@Overridepublic void finishAfterTransition() {YcShareElement.finishAfterTransition(this, this);super.finishAfterTransition();}
6、Activity A实现onActivityReenter()函数
@Overridepublic void onActivityReenter(int resultCode, Intent data) {super.onActivityReenter(resultCode, data);YcShareElement.onActivityReenter(this, resultCode, data, new IShareElementSelector() {@Overridepublic void selectShareElements(List<ShareElementInfo> list) {//将列表页滑动到变更后的ShareElement的位置mFragment.selectShareElement(list.get(0));}});}
如何扩展支持自定义View的Transition动画
这里以Fresco为例介绍如何进行扩展
1、确定所需参数
首先确定SimpleDraweeView做Transtion动画需要的参数,即ActualImageScaleType
2、继承ViewStateSaver,获取所需参数
public class FrescoViewStateSaver extends ViewStateSaver {@Overrideprotected void captureViewInfo(View view, Bundle bundle) {if (view instanceof GenericDraweeView) {int actualScaleTypeInt = scaleTypeToInt(((GenericDraweeView)view).getHierarchy().getActualImageScaleType())bundle.putInt("scaleType",actualScaleTypeInt);}}public ScalingUtils.ScaleType getScaleType(Bundle bundle) {int scaleType = bundle.getInt("scaleType", 0);return intToScaleType(scaleType);}
}
3、自定义Transition
public class AdvancedDraweeTransition extends Transition {private ScalingUtils.ScaleType mFromScale;private ScalingUtils.ScaleType mToScale;public AdvancedDraweeTransition() {addTarget(GenericDraweeView.class);}@Overridepublic void captureStartValues(TransitionValues transitionValues) {...ShareElementInfo shareElementInfo = ShareElementInfo.getFromView(transitionValues.view);mFromScale = ((FrescoViewStateSaver) shareElementInfo.getViewStateSaver()).getScaleType(viewInfo);...}@Overridepublic void captureEndValues(TransitionValues transitionValues) {...ShareElementInfo shareElementInfo = ShareElementInfo.getFromView(transitionValues.view);mToScale = ((FrescoViewStateSaver) shareElementInfo.getViewStateSaver()).getScaleType(viewInfo);...}@Overridepublic Animator createAnimator(ViewGroup sceneRoot,TransitionValues startValues,TransitionValues endValues) {..ValueAnimator animator = ValueAnimator.ofFloat(0, 1);animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float fraction = (float) animation.getAnimatedValue();scaleType.setValue(fraction);if (draweeView.getHierarchy().getActualImageScaleType() != scaleType) {draweeView.getHierarchy().setActualImageScaleType(scaleType);}}});...return animator;}
}
4、使用自定义的Transition
public class FrescoShareElementTransitionfactory extends DefaultShareElementTransitionFactory {@Overrideprotected TransitionSet buildShareElementsTransition(List<View> shareViewList) {TransitionSet transitionSet = super.buildShareElementsTransition(shareViewList);transitionSet.addTransition(new AdvancedDraweeTransition());return transitionSet;}
}
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {...YcShareElement.setEnterTransitions(this, this,true,new FrescoShareElementTransitionfactory());...}
广告时间
在文末安利一下我的另外几个开源库,欢迎大家来提issue、star、fork
PhotoMovie:高仿抖音照片电影功能
VideoProcessor:用硬编码实现视频的快慢放、倒流及混音功能
SVideoRecorder:硬编码短视频录制,支持分段录制、所见即所得
Android高阶转场动画-ShareElement完全攻略相关推荐
- Android 进阶——性能优化之电量优化全攻略及实战小结(二)
文章大纲 引言 一.在低电耗模式和应用待机模式下进行测试 1.在低电耗模式下测试您的应用 2.在应用待机模式下测试您的应用 3.列入白名单的可接受用例 4.确定当前充电状态 5.监控充电状态变化 6. ...
- android 转场动画 监听,Android 中的转场动画及兼容处理
Android 中的动画有很多,除了在一个界面上使用帧动画.属性动画将一个或多个 View 进行动画处理以外,还可以用于两个界面之间过渡.跳转.在 Android 5.0 之前,我们已经有了 over ...
- Windows平板装Android双系统及建立UEFI引导全攻略(windows8.1+android6+phoenix)
我的windows平板型号是dell venue 11 pro 5130,CPU是Intel baytrail z3770,原系统是windows8.1 32位. 写本文的时候,时间是2016.7. ...
- Android面试宝典(一位优秀应届生面试经验,很多Android高阶问题)
写一个专属自己的面试宝典: 小巫写这篇面试宝典,主要是为了积累经验和记录自己的求职之路.小巫并不是一种教导别人怎么去面试的心态来写这篇文章的,我只是想把自己的经历与大家分享,或许你能在其中发现一些小巫 ...
- Android高仿新浪微博点赞动画
效果图 效果很赞吧!其实源码也很简单,加个动画而已,我就不讲解了,源码我贴出来,大家看看吧 <scaleandroid:duration="250"android:fillA ...
- android 高仿华为充电动画
public class BubbleViscosity extends SurfaceView implements SurfaceHolder.Callback, Runnable {privat ...
- 在淘宝怎么避免买到android 三星i9300高仿机,购机防骗全攻略 教你鉴别真假三星Galaxy S3(i9300)的区别...
前言:之前我们曾经发表过一篇关于高仿三星i9300的图赏文章<编辑也分不清 带你看100%高仿三星i9300>,今天我们找来了一台真的i9300来进行对比,一方面看一下高仿机器究竟有多像真 ...
- Android:最全面详细的性能优化攻略(含内存优化、内存泄漏、绘制优化、布局优化、图片优化、APK优化、多线程优化、列表优化等)
前言:佛教中有一句话:初学者的心态,拥有初学者心态是件了不起的事情.真正的大师永远怀有一颗学徒的心. 一.概述 在Android中,性能优化是细分领域中最难且也是知识面涉及最深和最广的方向之一. 更快 ...
- android车载支持格式,安卓全面屏适配攻略(适配超长车载主机)
前言 2017年9月,拜腾的横空出世,打破了车载主机界一直以来的沉寂,各大媒体也是不吝词藻的对它的超长中控屏进行了大肆的报道.这个时候作为同为车机供应者的诸位友商心里却不那么的平静,恨不得在发布会现场 ...
最新文章
- net平台c#语言如何实现支付宝payto接口
- 安装配置Statspack
- cve-2016-6662 mysql远程代码执行/权限提升 漏洞
- Android 7.0 Gallery图库源码分析2 - 分析启动流程
- 抗疫进展:多家科研机构联合筛选出五种可能有效的抗病毒药物
- csgo 机器人模式_分享一个休闲模式机器人Bug
- python中property干什么用的?
- 微软收购yahoo,不知道是好事还是坏事!?
- 国内外优秀的计算机视觉团队汇总
- 几款用于电能质量测量的芯片
- 陈睿:B站用户用创作展示传统文化之美
- 保龄球记分程序c语言,老鸟救命~关于保龄球记分规则的程序
- cidaemon.exe
- 1.Lunix系统安装及重置root密码
- CF1146G Zoning Restrictions 最小割
- 棋牌游戏进入游戏房间流程
- vr数字景区应运而生,vr云游带商家线上转型
- [陈鹏导师精益项目实战]华东区电子行业精益生产一期项目总结暨二期动员大会顺利召开
- 目前android版本最好,当前主流的7个手机系统,你认为哪个最好用?
- 高分子材料S阻燃剂的分类及参考配方
热门文章
- JavaScript期末大作业 基于HTML+CSS+JavaScript技术制作web前端开发个人博客(48页)
- 我的第二个Python趣味项目,来了!
- 蓝桥杯单片机NE555编程
- 为什么中文不能用来编程呢?难道中文比英语差?看完长见识了
- 蓝桥杯各模块使用总结 时钟DS1302 ,ADC DAC PCF8591 ,温度传感器DS18B20 存储AT24C02
- 东北大学软件学院C语言程序设计大作业:餐厅管理系统
- 2020,焦虑与希望
- Redis和美团自研的Squirrel,Cellar
- PB 图片选择验证码
- 计算机运筹学pdf,【计算题专题7】运筹学计算上(典型考题思路讲解).pdf