android开机动画bootanimation
bootanim服务 源码:frameworks/base/cmds/bootanimation/bootanim.rc service bootanim /system/bin/bootanimation class core user graphics group graphics audio disabled oneshot
bootanim服务可执行程序是/system/bin/bootanimation,定义为core类型的核心服务,所有者是graphics, 组是graphics audio默认是disable的,即开机时不会自动启动,oneshot只启动一次,也就是该服务被异常 销毁也不会重启。
之所以设置为disable是因为bootanim服务是依赖于系统的显示服务的,只有当显示相关的服务起来后才能启动 也就是surfaceFlinger服务起来后会拉起bootanim服务显示开机动画。
surfaceFlinger服务 源码:frameworks/native/services/surfaceflinger/surfaceflinger.rc service surfaceflinger /system/bin/surfaceflinger class core user system group graphics drmrpc readproc onrestart restart zygote writepid /sys/fs/cgroup/stune/foreground/tasks
surfaceFlinger是负责显示相关的服务,是非常核心的服务必须一直保持存活的 源码:frameworks/native/cmds/servicemanager/servicemanager.rc service servicemanager /system/bin/servicemanager class core user system group system readproc critical onrestart restart healthd onrestart restart zygote onrestart restart audioserver onrestart restart media onrestart restart surfaceflinger onrestart restart inputflinger onrestart restart drm onrestart restart cameraserver writepid /dev/cpuset/system-background/tasks
init进程读取init.rc就会去解析服务,surfaceflinger也是定义的服务之一,调用 /system/bin/surfaceflinger 启动surfaceflinger服务,服务的入口是main函数,下面对surfaceflinger进行分析:
源码:frameworks/native/services/surfaceflinger 入口文件main_surfaceflinger.cpp int main(int, char**) { signal(SIGPIPE, SIG_IGN); // When SF is launched in its own process, limit the number of // binder threads to 4. ProcessState::self()->setThreadPoolMaxThreadCount(4); //binder服务池大小限制,最多同时4个线程
// start the thread pool sp ps(ProcessState::self()); ps->startThreadPool(); //启动线程池监听连接
// instantiate surfaceflinger sp flinger = new SurfaceFlinger(); //新建一个实例
setpriority(PRIO_PROCESS, 0, PRIORITY_URGENT_DISPLAY); //权限设置
set_sched_policy(0, SP_FOREGROUND); //调度设置
#ifdef ENABLE_CPUSETS // Put most SurfaceFlinger threads in the system-background cpuset // Keeps us from unnecessarily using big cores // Do this after the binder thread pool init set_cpuset_policy(0, SP_SYSTEM); #endif
// initialize before clients can connect flinger->init(); //初始化这里就会启动开机动画
// publish surface flinger sp sm(defaultServiceManager()); sm->addService(String16(SurfaceFlinger::getServiceName()), flinger, false);
// publish GpuService sp gpuservice = new GpuService(); sm->addService(String16(GpuService::SERVICE_NAME), gpuservice, false);
// run surface flinger in this thread flinger->run();
return 0; }
源码:SurfaceFlinger.cpp void SurfaceFlinger::init() { ... // set initial conditions (e.g. unblank default device) initializeDisplays(); //初始化显示设备
// start boot animation startBootAnim(); //启动开机动画 }
void SurfaceFlinger::startBootAnim() { #ifdef MTK_AOSP_ENHANCEMENT // dynamic disable/enable boot animation checkEnableBootAnim(); #else // start boot animation property_set("service.bootanim.exit", "0"); //为了防止这个属性被设为1,不管现在值怎样都设置为0 property_set("ctl.start", "bootanim"); //启动开机动画 #endif }
下面分析init property service怎样将属性设置转为对应的动作 在init.cpp中 int main(int argc, char** argv) { ... start_property_service(); //启动属性管理服务 ... } start_property_service函数在property_service.cpp中定义: start_property_service void start_property_service() { property_set_fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0666, 0, 0, NULL); if (property_set_fd == -1) { ERROR("start_property_service socket creation failed: %s\n", strerror(errno)); exit(1); } listen(property_set_fd, 8); //socket监听binder服务请求,处理相关属性设置动作 register_epoll_handler(property_set_fd, handle_property_set_fd); //epoll监听处理,处理回调函数是handle_property_set_fd }
handle_property_set_fd static void handle_property_set_fd() { ... switch(msg.cmd) { ...
if (!is_legal_property_name(msg.name, strlen(msg.name))) { ERROR("sys_prop: illegal property name. Got: \"%s\"\n", msg.name); close(s); return; } getpeercon(s, &source_ctx); if (memcmp(msg.name,"ctl.",4) == 0) { // Keep the old close-socket-early behavior when handling // ctl.* properties. close(s); if (check_control_mac_perms(msg.value, source_ctx, &cr)) { ... handle_control_message((char*) msg.name + 4, (char*) msg.value); } else { ... } } else { if (check_mac_perms(msg.name, source_ctx, &cr)) { ... property_set((char*) msg.name, (char*) msg.value); //普通属性,只是做设置值处理 } else { } // Note: bionic's property client code assumes that the // property server will not close the socket until *AFTER* // the property is written to memory. close(s); } freecon(source_ctx); break; default: ... }
handle_control_message void handle_control_message(const std::string& msg, const std::string& name) { Service* svc = ServiceManager::GetInstance().FindServiceByName(name); //根据只有一个全局Service存储所有定义的服务,后面可以根据名字查找服务 if (svc == nullptr) { ERROR("no such service '%s'\n", name.c_str()); return; } if (msg == "start") { svc->Start(); //启动服务 } else if (msg == "stop") { svc->Stop(); //停止服务 } else if (msg == "restart") { svc->Restart(); //重启服务 } else { ERROR("unknown control msg '%s'\n", msg.c_str()); } }
ServiceManager 1.FindServiceByName Service* ServiceManager::FindServiceByName(const std::string& name) const { auto svc = std::find_if(services_.begin(), services_.end(), [&name] (const std::unique_ptr& s) { return name == s->name(); }); if (svc != services_.end()) { return svc->get(); } return nullptr; } ServiceManager.h中定义services_向量数组 std::vector> services_;

2.Start bool Service::Start() { ... if (!seclabel_.empty()) { scon = seclabel_; } else { ... rc = security_compute_create(mycon, fcon, string_to_security_class("process"), &ret_scon); ... if (rc == 0 && scon == mycon) { ERROR("Service %s does not have a SELinux domain defined.\n", name_.c_str()); //selinux安全检查,在init.rc中定义了服务不能启动会提示这个错误 ... }
pid_t pid = fork(); if (pid == 0) { std::vector strs; for (const auto& s : args_) { strs.push_back(const_cast(s.c_str())); } strs.push_back(nullptr); if ( execve(args_[0].c_str(), (char**) &strs[0], (char**) ENV) < 0) { //启动子进程执行服务程序 ERROR("cannot execve('%s'): %s\n", args_[0].c_str(), strerror(errno)); } _exit(127); }
NotifyStateChange("running");
}
NotifyStateChange void Service::NotifyStateChange(const std::string& new_state) const { if ((flags_ & SVC_EXEC) != 0) { // 'exec' commands don't have properties tracking their state. return; } std::string prop_name = StringPrintf("init.svc.%s", name_.c_str()); //init.svc.bootanim if (prop_name.length() >= PROP_NAME_MAX) { // If the property name would be too long, we can't set it. ERROR("Property name \"init.svc.%s\" too long; not setting to %s\n", name_.c_str(), new_state.c_str()); return; }
property_set(prop_name.c_str(), new_state.c_str()) ;//设置init.svc.bootanim 为running }

3. Stop void Service::Stop() { StopOrReset(SVC_DISABLED); }
void Service::StopOrReset(int how) { /* The service is still SVC_RUNNING until its process exits, but if it has * already exited it shoudn't attempt a restart yet. */ flags_ &= ~(SVC_RESTARTING | SVC_DISABLED_START);
if ((how != SVC_DISABLED) && (how != SVC_RESET) && (how != SVC_RESTART)) { /* Hrm, an illegal flag. Default to SVC_DISABLED */ how = SVC_DISABLED; } /* if the service has not yet started, prevent * it from auto-starting with its class */ if (how == SVC_RESET) { flags_ |= (flags_ & SVC_RC_DISABLED) ? SVC_DISABLED : SVC_RESET; } else { flags_ |= how; }
if (pid_) {//如果服务进程已经启动才会有pid_ NOTICE("Service '%s' is being killed...\n", name_.c_str()); kill(-pid_, SIGKILL); //杀死服务进程 NotifyStateChange("stopping"); // 设置init.svc.bootanim 为stopping } else { NotifyStateChange("stopped");// 设置init.svc.bootanim 为stopped } }
4.Restart void Service::Restart() { if (flags_ & SVC_RUNNING) { /* Stop, wait, then start the service. */ StopOrReset(SVC_RESTART); } else if (!(flags_ & SVC_RESTARTING)) { /* Just start the service since it's not running. */ Start(); } /* else: Service is restarting anyways. */ }
经过上面分析,我们已经知道了开机init进程解析init.rc注册bootanim服务到surfaceFlinger服务启动startBootanim到属性服务如何将 属性转化为对应的动作即启动子进程执行 /system/bin/bootanimation整个过程,下面分析bootanimation程序是怎样启动动画的
bootanimation 源码:frameworks/base/cmds/bootanimation/ 入口bootanimation_main.cpp int main(int argc, char** argv) { setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY);
char value[PROPERTY_VALUE_MAX]; property_get("debug.sf.nobootanimation", value, "0"); //调试状态可通过设置属性debug.sf.nobootanimation值为1禁止开机动画 int noBootAnimation = atoi(value); ALOGI_IF(noBootAnimation, "boot animation disabled"); if (!noBootAnimation) { sp proc(ProcessState::self()); ProcessState::self()->startThreadPool();
bool setBoot = true; bool setRotated = false; bool sePaly = true; if(argc > 1){ if(!strcmp(argv[1],"shut")) setBoot = false; } if(argc > 2){ if(!strcmp(argv[2],"nomp3")) sePaly = false; } if(argc > 3){ if(!strcmp(argv[3],"rotate")) setRotated = true; } ALOGD("[BootAnimation %s %d]setBoot=%d,sePaly=%d,setRotated=%d",__FUNCTION__,__LINE__,setBoot,sePaly,setRotated); char volume[PROPERTY_VALUE_MAX]; property_get("persist.sys.mute.state", volume, "-1"); //属性persist.sys.mute.state设置音量 int nVolume = -1; nVolume = atoi(volume); ALOGD("[BootAnimation %s %d]nVolume=%d",__FUNCTION__,__LINE__,nVolume); if(nVolume == 0 || nVolume == 1 ){ sePaly = false; } ALOGD("before new BootAnimation..."); ALOGD("[BootAnimation %s %d]before new BootAnimation...",__FUNCTION__,__LINE__); sp boot = new BootAnimation(setBoot,sePaly,setRotated); //智能指针sp,使用后自动释放 ALOGD("joinThreadPool..."); ALOGD("[BootAnimation %s %d]before joinThreadPool...",__FUNCTION__,__LINE__); IPCThreadState::self()->joinThreadPool(); //等待开机动画线程结束 } ALOGD("[BootAnimation %s %s %d]end",__FILE__,__FUNCTION__,__LINE__); return 0; }
BootAnimation 源码:BootAnimation.cpp BootAnimation::BootAnimation(bool bSetBootOrShutDown, bool bSetPlayMP3,bool bSetRotated) : Thread(false), mZip(NULL) { ALOGD("[BootAnimation %s %d]",__FUNCTION__,__LINE__); mSession = new SurfaceComposerClient(); bBootOrShutDown = bSetBootOrShutDown; bShutRotate = bSetRotated; bPlayMP3 = bSetPlayMP3; mProgram = 0; bETC1Movie = false; mBootVideoPlayType = BOOT_VIDEO_PLAY_FULL; mBootVideoPlayState = MEDIA_NOP; ALOGD("[BootAnimation %s %d]bBootOrShutDown=%d,bPlayMP3=%d,bShutRotate=%d",__FUNCTION__,__LINE__,bBootOrShutDown,bPlayMP3,bShutRotate); }
在BootAnimation.h中,我们可以看到 class BootAnimation : public Thread, public IBinder::DeathRecipient { ... private: virtual bool threadLoop(); virtual status_t readyToRun(); virtual void onFirstRef(); ... } BootAnimation继承于Thread,而Thread继承RefBase类,并且BootAnimation覆写了onFirstRef,readyToRun,threadLoop函数, 结合智能指针知识,在创建对象BootAnimation时候会首先调用onFirstRef()函数 void BootAnimation::onFirstRef() { status_t err = mSession->linkToComposerDeath(this); ALOGE_IF(err, "linkToComposerDeath failed (%s) ", strerror(-err)); if (err == NO_ERROR) { run("BootAnimation", PRIORITY_DISPLAY); //这里会调用onFirstRef()函数 } }
status_t BootAnimation::readyToRun() { if (bBootOrShutDown) { initBootanimationZip(); //开机动画 } else { initShutanimationZip(); //关机动画 } if ((mZip != NULL)&&!(mZipFileName.isEmpty())) { ZipEntryRO desc = mZip->findEntryByName("desc.txt"); //desc.txt文件描述了如何播放动画,如图片目录,播放次数和播放帧率 uint16_t method; mZip->getEntryInfo(desc, &method, NULL, NULL, NULL, NULL, NULL); mZip->releaseEntry(desc); if (method == ZipFileRO::kCompressStored) { bETC1Movie = false; } else { bETC1Movie = true; } } mAssets.addDefaultAssets();
sp dtoken(SurfaceComposerClient::getBuiltInDisplay( ISurfaceComposer::eDisplayIdMain)); DisplayInfo dinfo; status_t status = SurfaceComposerClient::getDisplayInfo(dtoken, &dinfo); ... //下面主要初始化Surface相关的类 sp dtoken(SurfaceComposerClient::getBuiltInDisplay( ISurfaceComposer::eDisplayIdMain)); DisplayInfo dinfo; status_t status = SurfaceComposerClient::getDisplayInfo(dtoken, &dinfo); if (status) return -1; /// M: The tablet rotation maybe 90/270 degrees, so set the lcm config for tablet SurfaceComposerClient::setDisplayProjection(dtoken, DisplayState::eOrientationDefault, Rect(dinfo.w, dinfo.h), Rect(dinfo.w, dinfo.h));
// create the native surface sp control = session()->createSurface(String8("BootAnimation"), //创建BootAnimation surface dinfo.w, dinfo.h, PIXEL_FORMAT_RGB_565);
SurfaceComposerClient::openGlobalTransaction(); control->setLayer(0x2000010); SurfaceComposerClient::closeGlobalTransaction();
sp s = control->getSurface(); ... /*opengl渲染相关初始化*/ EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
ALOGD("initialize opengl and egl"); EGLBoolean eglret = eglInitialize(display, 0, 0);
//下面是加密相关处理,部分厂家放在initBootanimationZip处理 }

#define GLOBAL_DEVICE_BOOTANIM_OPTR_NAME "persist.operator.optr" #define REGIONAL_BOOTANIM_FILE_NAME "persist.bootanim.logopath"
void BootAnimation::initBootanimationZip() {
.... f (property_get(GLOBAL_DEVICE_BOOTANIM_OPTR_NAME, OPTR, NULL) > 0){ //获取bootanimation.zip文件的路径 for(int i=0;i /*接下来对zip包进行解密*/ property_get("vold.decrypt", decrypt, ""); .. }

threadLoop 根据Thread的周期函数在调用完readyToRun函数后就会调用threadLoop()函数 bool BootAnimation::threadLoop() { bool r; // We have no bootanimation file, so we use the stock android logo // animation. sp mediaplayer; //开机音乐设置,播放 const char* resourcePath = initAudioPath(); status_t mediastatus = NO_ERROR; if (resourcePath != NULL) { bPlayMP3 = true; ALOGD("sound file path: %s", resourcePath); mediaplayer = new MediaPlayer(); mediastatus = mediaplayer->setDataSource(NULL, resourcePath, NULL);
sp listener = new BootVideoListener(this); mediaplayer->setListener(listener);
if (mediastatus == NO_ERROR) { ALOGD("mediaplayer is initialized"); Parcel* attributes = new Parcel(); attributes->writeInt32(AUDIO_USAGE_MEDIA); //usage attributes->writeInt32(AUDIO_CONTENT_TYPE_MUSIC); //audio_content_type_t attributes->writeInt32(AUDIO_SOURCE_DEFAULT); //audio_source_t attributes->writeInt32(0); //audio_flags_mask_t attributes->writeInt32(1); //kAudioAttributesMarshallTagFlattenTags of mediaplayerservice.cpp attributes->writeString16(String16("BootAnimationAudioTrack")); // tags mediaplayer->setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, *attributes); mediaplayer->setAudioStreamType(AUDIO_STREAM_MUSIC); mediastatus = mediaplayer->prepare(); } if (mediastatus == NO_ERROR) { ALOGD("media player is prepared"); mediastatus = mediaplayer->start(); }
}else{ bPlayMP3 = false; }
if ((mZip == NULL)&&(mZipFileName.isEmpty())) { //如果开机动画不存在的情况 r = android(); //加载原生的ANDROID开机图片 } else { if (!bETC1Movie) { ALOGD("threadLoop() movie()"); r = movie(); //解析动画,原生开机动画,ANDROID } else { ALOGD("threadLoop() ETC1movie()"); r = ETC1movie(); //解析动画,定制的开机动画,bootanimation.zip中desc.txt文件描述动画播放 } } if (resourcePath != NULL) { if (mediastatus == NO_ERROR) { ALOGD("mediaplayer was stareted successfully, now it is going to be stoped"); mediaplayer->stop(); mediaplayer->disconnect(); mediaplayer.clear(); } } eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(mDisplay, mContext); eglDestroySurface(mDisplay, mSurface); mFlingerSurface.clear(); mFlingerSurfaceControl.clear(); eglTerminate(mDisplay); IPCThreadState::self()->stopProcess(); //停止线程 return r; }
ETC1movie 先看个desc.txt例子 420 232 20 p 1 0 part0 p 0 10 part1 1355263
第一行表示420*232 20表示1秒播放多少帧(帧率) 第二行p表示动画,也有用c表示的,1表示播放1次,0表示这部分播完到下部分的时间间隔,part0表示播放的图片目录 第三行p(旧版本的)表示动画,也有用c表示(新版本),0表示无限循环播放,10表示两次播放两个part之间间隔的帧数是10,间隔时间是10*1/20秒,只作用于part1
bool BootAnimation::ETC1movie() { ZipEntryRO desc = mZip->findEntryByName("desc.txt"); //如果找不到desc.txt则无法播放动画 ALOGE_IF(!desc, "couldn't find desc.txt"); if (!desc) { return false; } ... Animation animation;
// Parse the description file for (;;) { const char* endl = strstr(s, "\n"); if (!endl) break; String8 line(s, endl - s); const char* l = line.string(); int fps, width, height, count, pause; char path[ANIM_ENTRY_NAME_MAX]; char color[7] = "000000"; // default to black if unspecified char pathType; if (sscanf(l, "%d %d %d", &width, &height, &fps) == 3) { //解析desc.txt第一行 ALOGD("> w=%d, h=%d, fps=%d", width, height, fps); // add log animation.width = width; animation.height = height; animation.fps = fps; } else if (sscanf(l, "%c %d %d %s", &pathType, &count, &pause, path) == 4) { //解析desc.txt第一行后面的行 ALOGD("> type=%c, count=%d, pause=%d, path=%s", pathType, count, pause, path); // add log Animation::Part part; part.playUntilComplete = pathType == 'c'; part.count = count; part.pause = pause; part.path = path; animation.parts.add(part); //添加值animation中 if (!parseColor(color, part.backgroundColor)) { ALOGD("> invalid color '#%s'", color); part.backgroundColor[0] = 0.0f; part.backgroundColor[1] = 0.0f; part.backgroundColor[2] = 0.0f; } }
s = ++endl; }
/*读取对应的图片*/ ZipEntryRO entry; char name[ANIM_ENTRY_NAME_MAX]; while ((entry = mZip->nextEntry(cookie)) != NULL) { const int foundEntryName = mZip->getEntryFileName(entry, name, ANIM_ENTRY_NAME_MAX); if (foundEntryName > ANIM_ENTRY_NAME_MAX || foundEntryName == -1) { ALOGE("Error fetching entry file name"); continue; }
const String8 entryName(name); const String8 path(entryName.getPathDir()); const String8 leaf(entryName.getPathLeaf()); if (leaf.size() > 0) { for (size_t j=0 ; j mZip->endIteration(cookie); for (size_t i=0 ; i ... //下面真正开始播放动画 for (int r=0 ; !part.count || r//根据前面解析的part进行循环 // Exit any non playuntil complete parts immediately if(exitPending() && !part.playUntilComplete) { //ALOGD("[BootAnimation %s %d]part.playUntilComplete=%d", __FUNCTION__, __LINE__ ,part.playUntilComplete); break; } glClearColor( part.backgroundColor[0], part.backgroundColor[1], part.backgroundColor[2], 1.0f); for (size_t j=0 ; j//每个part图片播放 const Animation::Frame& frame(part.frames[j]); nsecs_t lastFrame = systemTime(); //ALOGD("[BootAnimation %s %d]i=%d,r=%d,j=%d,lastFrame=%lld(%lld ms),file=%s",__FUNCTION__,__LINE__,i,r,j,lastFrame,ns2ms(lastFrame),frame.name.string()); if (r > 0) { glBindTexture(GL_TEXTURE_2D, frame.tid); } else { if (part.count != 1) { glGenTextures(1, &frame.tid); glBindTexture(GL_TEXTURE_2D, frame.tid); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } initTexture(frame.fullPath.string()); }
static GLfloat quadVertex[] = { -1.0f, 1.0f, 0.0f, // Position 0 0.0f, 0.0f, // TexCoord 0 -1.0f, -1.0f, 0.0f, // Position 1 0.0f, 1.0f, // TexCoord 1 1.0f, -1.0f, 0.0f, // Position 2 1.0f, 1.0f, // TexCoord 2 1.0f, 1.0f, 0.0f, // Position 3 1.0f, 0.0f // TexCoord 3 };
static GLushort quadIndex[] = { 0, 1, 2, 0, 2, 3 }; 3, GL_FLOAT, false, 5*sizeof(GL_FLOAT), quadVertex);
glVertexAttribPointer(mAttribTexCoord, 2, GL_FLOAT, false, 5*sizeof(GL_FLOAT), &quadVertex[3]); glEnableVertexAttribArray(mAttribPosition); glEnableVertexAttribArray(mAttribTexCoord);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, quadIndex); eglSwapBuffers(mDisplay, mSurface);
nsecs_t now = systemTime(); nsecs_t delay = frameDuration - (now - lastFrame); //ALOGD("[BootAnimation %s %d]%lld,delay=%lld",__FUNCTION__,__LINE__,ns2ms(now - lastFrame), ns2ms(delay)); lastFrame = now;
if (delay > 0) { struct timespec spec; spec.tv_sec = (now + delay) / 1000000000; spec.tv_nsec = (now + delay) % 1000000000; int err; do { err = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &spec, NULL); //播放的帧率 } while (err<0 && errno == EINTR); } if(!bPlayMP3){ checkExit(); }else{ if(mBootVideoPlayState == MEDIA_PLAYBACK_COMPLETE || mBootVideoPlayState == MEDIA_ERROR) { checkExit(); //检查是否需要退出动画 } } }
usleep(part.pause * ns2us(frameDuration)); //两个part之间的时间间隔
// For infinite parts, we've now played them at least once, so perhaps exit if(exitPending() && !part.count) { ALOGD("[BootAnimation %s %d]break,exitPending()=%d,part.count=%d",__FUNCTION__,__LINE__,exitPending(),part.count); break; } } // free the textures for this part if (part.count != 1) { for (size_t j=0 ; j














更多相关文章

  1. Android系统启动-SystemServer下篇
  2. 直播源码Android实现 曲线路径动画
  3. android的service中在后台弹出提示框
  4. Android(安卓)上传文件到XP
  5. Android如何从服务器获取图片
  6. android中translate动画
  7. 补间动画--平移动画XML
  8. android开机启动的问题,android:installLocation
  9. 2016年3月1日Android实习笔记

随机推荐

  1. 二:android的helloworld和初步认识
  2. Android(安卓)NDK开发(windows)
  3. Android(安卓)之约束布局
  4. Android安装卸载apk包
  5. Timer、TimerTask的简单应用及如何解决多
  6. Android(安卓)Studio如何更换包名(包括代
  7. android:taskAffinity使用详解
  8. 深入理解Activity启动模式
  9. android studio 修改包名,双开一起安装
  10. ROSJava安装和在Android中使用ROS