Repository: ganyao114/VA_Doc Branch: master Commit: 3b8284e005a4 Files: 627 Total size: 2.0 MB Directory structure: gitextract_o4ouir9u/ ├── .gitignore ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── OaceOaT8w5Xda6wa │ ├── java/ │ │ └── io/ │ │ └── virtualapp/ │ │ ├── VApp.java │ │ ├── VCommends.java │ │ ├── abs/ │ │ │ ├── BasePresenter.java │ │ │ ├── BaseView.java │ │ │ ├── Callback.java │ │ │ ├── Value.java │ │ │ ├── nestedadapter/ │ │ │ │ ├── RecyclerViewAdapterWrapper.java │ │ │ │ └── SmartRecyclerAdapter.java │ │ │ ├── percent/ │ │ │ │ └── PercentLinearLayout.java │ │ │ ├── reflect/ │ │ │ │ └── ReflectException.java │ │ │ └── ui/ │ │ │ ├── VActivity.java │ │ │ ├── VFragment.java │ │ │ └── VUiKit.java │ │ ├── delegate/ │ │ │ ├── MyAppRequestListener.java │ │ │ ├── MyComponentDelegate.java │ │ │ ├── MyPhoneInfoDelegate.java │ │ │ └── MyTaskDescriptionDelegate.java │ │ ├── effects/ │ │ │ ├── ExplosionAnimator.java │ │ │ └── ExplosionField.java │ │ ├── home/ │ │ │ ├── FlurryROMCollector.java │ │ │ ├── HomeActivity.java │ │ │ ├── HomeContract.java │ │ │ ├── HomePresenterImpl.java │ │ │ ├── ListAppActivity.java │ │ │ ├── ListAppContract.java │ │ │ ├── ListAppFragment.java │ │ │ ├── ListAppPresenterImpl.java │ │ │ ├── LoadingActivity.java │ │ │ ├── adapters/ │ │ │ │ ├── AppPagerAdapter.java │ │ │ │ ├── CloneAppListAdapter.java │ │ │ │ ├── LaunchpadAdapter.java │ │ │ │ └── decorations/ │ │ │ │ └── ItemOffsetDecoration.java │ │ │ ├── ads/ │ │ │ │ └── AdScheduler.java │ │ │ ├── models/ │ │ │ │ ├── AddAppButton.java │ │ │ │ ├── AppData.java │ │ │ │ ├── AppInfo.java │ │ │ │ ├── AppInfoLite.java │ │ │ │ ├── EmptyAppData.java │ │ │ │ ├── MultiplePackageAppData.java │ │ │ │ └── PackageAppData.java │ │ │ ├── platform/ │ │ │ │ ├── PlatformInfo.java │ │ │ │ └── WechatPlatformInfo.java │ │ │ └── repo/ │ │ │ ├── AppDataSource.java │ │ │ ├── AppRepository.java │ │ │ └── PackageAppDataStorage.java │ │ ├── splash/ │ │ │ └── SplashActivity.java │ │ ├── vs/ │ │ │ └── VSManagerActivity.java │ │ └── widgets/ │ │ ├── BallGridBeatIndicator.java │ │ ├── BallPulseIndicator.java │ │ ├── BaseView.java │ │ ├── CardStackAdapter.java │ │ ├── CardStackLayout.java │ │ ├── CircularAnim.java │ │ ├── DragSelectRecyclerView.java │ │ ├── DragSelectRecyclerViewAdapter.java │ │ ├── EatBeansView.java │ │ ├── Indicator.java │ │ ├── LabelView.java │ │ ├── LauncherIconView.java │ │ ├── LoadingIndicatorView.java │ │ ├── MarqueeTextView.java │ │ ├── MaterialRippleLayout.java │ │ ├── RippleButton.java │ │ ├── ShadowProperty.java │ │ ├── ShadowViewDrawable.java │ │ ├── Shimmer.java │ │ ├── ShimmerViewBase.java │ │ ├── ShimmerViewHelper.java │ │ ├── TwoGearsView.java │ │ └── ViewHelper.java │ └── res/ │ ├── drawable/ │ │ ├── blue_circle.xml │ │ ├── fab_bg.xml │ │ ├── home_bg.xml │ │ ├── icon_bg.xml │ │ ├── sel_clone_app_btn.xml │ │ ├── sel_guide_btn.xml │ │ ├── shape_clone_app_btn.xml │ │ └── shape_clone_app_btn_pressed.xml │ ├── layout/ │ │ ├── activity_clone_app.xml │ │ ├── activity_home.xml │ │ ├── activity_install.xml │ │ ├── activity_loading.xml │ │ ├── activity_splash.xml │ │ ├── activity_users.xml │ │ ├── fragment_list_app.xml │ │ ├── item_app.xml │ │ ├── item_clone_app.xml │ │ ├── item_launcher_app.xml │ │ └── item_user.xml │ ├── menu/ │ │ └── user_menu.xml │ ├── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── integers.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── values-zh-rCN/ │ └── strings.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── lib/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── aidl/ │ │ ├── android/ │ │ │ ├── accounts/ │ │ │ │ ├── IAccountAuthenticator.aidl │ │ │ │ ├── IAccountAuthenticatorResponse.aidl │ │ │ │ └── IAccountManagerResponse.aidl │ │ │ ├── app/ │ │ │ │ ├── IActivityManager/ │ │ │ │ │ └── ContentProviderHolder.aidl │ │ │ │ ├── IServiceConnection.aidl │ │ │ │ ├── IStopUserCallback.aidl │ │ │ │ └── job/ │ │ │ │ ├── IJobCallback.aidl │ │ │ │ └── IJobService.aidl │ │ │ ├── content/ │ │ │ │ ├── IIntentReceiver.aidl │ │ │ │ ├── ISyncAdapter.aidl │ │ │ │ ├── ISyncContext.aidl │ │ │ │ ├── ISyncStatusObserver.aidl │ │ │ │ └── pm/ │ │ │ │ ├── IPackageDataObserver.aidl │ │ │ │ ├── IPackageDeleteObserver2.aidl │ │ │ │ ├── IPackageInstallObserver.aidl │ │ │ │ ├── IPackageInstallObserver2.aidl │ │ │ │ ├── IPackageInstallerCallback.aidl │ │ │ │ └── IPackageInstallerSession.aidl │ │ │ └── net/ │ │ │ ├── IConnectivityManager.aidl │ │ │ └── wifi/ │ │ │ └── IWifiScanner.aidl │ │ └── com/ │ │ └── lody/ │ │ └── virtual/ │ │ ├── client/ │ │ │ └── IVClient.aidl │ │ ├── os/ │ │ │ └── VUserInfo.aidl │ │ ├── remote/ │ │ │ ├── AppTaskInfo.aidl │ │ │ ├── InstallResult.aidl │ │ │ ├── InstalledAppInfo.aidl │ │ │ ├── PendingIntentData.aidl │ │ │ ├── PendingResultData.aidl │ │ │ ├── Problem.aidl │ │ │ ├── ReceiverInfo.aidl │ │ │ ├── VDeviceInfo.aidl │ │ │ └── VParceledListSlice.aidl │ │ └── server/ │ │ ├── IAccountManager.aidl │ │ ├── IActivityManager.aidl │ │ ├── IAppManager.aidl │ │ ├── IBinderDelegateService.aidl │ │ ├── IDeviceInfoManager.aidl │ │ ├── IJobScheduler.aidl │ │ ├── INotificationManager.aidl │ │ ├── IPackageInstaller.aidl │ │ ├── IPackageInstallerSession.aidl │ │ ├── IPackageManager.aidl │ │ ├── IUserManager.aidl │ │ ├── IVirtualStorageService.aidl │ │ ├── interfaces/ │ │ │ ├── IAppRequestListener.aidl │ │ │ ├── IIntentFilterObserver.aidl │ │ │ ├── IPackageObserver.aidl │ │ │ ├── IProcessObserver.aidl │ │ │ ├── IServiceFetcher.aidl │ │ │ └── IUiCallback.aidl │ │ └── pm/ │ │ └── installer/ │ │ ├── SessionInfo.aidl │ │ └── SessionParams.aidl │ ├── java/ │ │ ├── android/ │ │ │ └── content/ │ │ │ ├── SyncStatusInfo.java │ │ │ └── pm/ │ │ │ └── PackageParser.java │ │ ├── com/ │ │ │ └── lody/ │ │ │ └── virtual/ │ │ │ ├── Build.java │ │ │ ├── GmsSupport.java │ │ │ ├── client/ │ │ │ │ ├── NativeEngine.java │ │ │ │ ├── VClientImpl.java │ │ │ │ ├── core/ │ │ │ │ │ ├── CrashHandler.java │ │ │ │ │ ├── InstallStrategy.java │ │ │ │ │ ├── InvocationStubManager.java │ │ │ │ │ └── VirtualCore.java │ │ │ │ ├── env/ │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── DeadServerException.java │ │ │ │ │ ├── SpecialComponentList.java │ │ │ │ │ └── VirtualRuntime.java │ │ │ │ ├── fixer/ │ │ │ │ │ ├── ActivityFixer.java │ │ │ │ │ ├── ComponentFixer.java │ │ │ │ │ └── ContextFixer.java │ │ │ │ ├── hook/ │ │ │ │ │ ├── base/ │ │ │ │ │ │ ├── BinderInvocationProxy.java │ │ │ │ │ │ ├── BinderInvocationStub.java │ │ │ │ │ │ ├── Inject.java │ │ │ │ │ │ ├── LogInvocation.java │ │ │ │ │ │ ├── MethodBox.java │ │ │ │ │ │ ├── MethodInvocationProxy.java │ │ │ │ │ │ ├── MethodInvocationStub.java │ │ │ │ │ │ ├── MethodProxy.java │ │ │ │ │ │ ├── ReplaceCallingPkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceLastPkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceLastUidMethodProxy.java │ │ │ │ │ │ ├── ReplaceSequencePkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceSpecPkgMethodProxy.java │ │ │ │ │ │ ├── ReplaceUidMethodProxy.java │ │ │ │ │ │ ├── ResultStaticMethodProxy.java │ │ │ │ │ │ ├── SkipInject.java │ │ │ │ │ │ └── StaticMethodProxy.java │ │ │ │ │ ├── delegate/ │ │ │ │ │ │ ├── AppInstrumentation.java │ │ │ │ │ │ ├── ComponentDelegate.java │ │ │ │ │ │ ├── InstrumentationDelegate.java │ │ │ │ │ │ ├── PhoneInfoDelegate.java │ │ │ │ │ │ └── TaskDescriptionDelegate.java │ │ │ │ │ ├── providers/ │ │ │ │ │ │ ├── DownloadProviderHook.java │ │ │ │ │ │ ├── ExternalProviderHook.java │ │ │ │ │ │ ├── InternalProviderHook.java │ │ │ │ │ │ ├── ProviderHook.java │ │ │ │ │ │ └── SettingsProviderHook.java │ │ │ │ │ ├── proxies/ │ │ │ │ │ │ ├── account/ │ │ │ │ │ │ │ └── AccountManagerStub.java │ │ │ │ │ │ ├── alarm/ │ │ │ │ │ │ │ └── AlarmManagerStub.java │ │ │ │ │ │ ├── am/ │ │ │ │ │ │ │ ├── ActivityManagerStub.java │ │ │ │ │ │ │ ├── HCallbackStub.java │ │ │ │ │ │ │ └── MethodProxies.java │ │ │ │ │ │ ├── appops/ │ │ │ │ │ │ │ └── AppOpsManagerStub.java │ │ │ │ │ │ ├── appwidget/ │ │ │ │ │ │ │ └── AppWidgetManagerStub.java │ │ │ │ │ │ ├── audio/ │ │ │ │ │ │ │ └── AudioManagerStub.java │ │ │ │ │ │ ├── backup/ │ │ │ │ │ │ │ └── BackupManagerStub.java │ │ │ │ │ │ ├── bluetooth/ │ │ │ │ │ │ │ └── BluetoothStub.java │ │ │ │ │ │ ├── clipboard/ │ │ │ │ │ │ │ └── ClipBoardStub.java │ │ │ │ │ │ ├── connectivity/ │ │ │ │ │ │ │ └── ConnectivityStub.java │ │ │ │ │ │ ├── content/ │ │ │ │ │ │ │ └── ContentServiceStub.java │ │ │ │ │ │ ├── context_hub/ │ │ │ │ │ │ │ └── ContextHubServiceStub.java │ │ │ │ │ │ ├── display/ │ │ │ │ │ │ │ └── DisplayStub.java │ │ │ │ │ │ ├── dropbox/ │ │ │ │ │ │ │ └── DropBoxManagerStub.java │ │ │ │ │ │ ├── graphics/ │ │ │ │ │ │ │ └── GraphicsStatsStub.java │ │ │ │ │ │ ├── imms/ │ │ │ │ │ │ │ └── MmsStub.java │ │ │ │ │ │ ├── input/ │ │ │ │ │ │ │ ├── InputMethodManagerStub.java │ │ │ │ │ │ │ └── MethodProxies.java │ │ │ │ │ │ ├── isms/ │ │ │ │ │ │ │ └── ISmsStub.java │ │ │ │ │ │ ├── isub/ │ │ │ │ │ │ │ └── ISubStub.java │ │ │ │ │ │ ├── job/ │ │ │ │ │ │ │ └── JobServiceStub.java │ │ │ │ │ │ ├── libcore/ │ │ │ │ │ │ │ ├── LibCoreStub.java │ │ │ │ │ │ │ └── MethodProxies.java │ │ │ │ │ │ ├── location/ │ │ │ │ │ │ │ └── LocationManagerStub.java │ │ │ │ │ │ ├── media/ │ │ │ │ │ │ │ ├── router/ │ │ │ │ │ │ │ │ └── MediaRouterServiceStub.java │ │ │ │ │ │ │ └── session/ │ │ │ │ │ │ │ └── SessionManagerStub.java │ │ │ │ │ │ ├── mount/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── MountServiceStub.java │ │ │ │ │ │ ├── network/ │ │ │ │ │ │ │ └── NetworkManagementStub.java │ │ │ │ │ │ ├── notification/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── NotificationManagerStub.java │ │ │ │ │ │ ├── persistent_data_block/ │ │ │ │ │ │ │ └── PersistentDataBlockServiceStub.java │ │ │ │ │ │ ├── phonesubinfo/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── PhoneSubInfoStub.java │ │ │ │ │ │ ├── pm/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ └── PackageManagerStub.java │ │ │ │ │ │ ├── power/ │ │ │ │ │ │ │ └── PowerManagerStub.java │ │ │ │ │ │ ├── restriction/ │ │ │ │ │ │ │ └── RestrictionStub.java │ │ │ │ │ │ ├── search/ │ │ │ │ │ │ │ └── SearchManagerStub.java │ │ │ │ │ │ ├── shortcut/ │ │ │ │ │ │ │ └── ShortcutServiceStub.java │ │ │ │ │ │ ├── telephony/ │ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ │ ├── TelephonyRegistryStub.java │ │ │ │ │ │ │ └── TelephonyStub.java │ │ │ │ │ │ ├── user/ │ │ │ │ │ │ │ └── UserManagerStub.java │ │ │ │ │ │ ├── vibrator/ │ │ │ │ │ │ │ └── VibratorStub.java │ │ │ │ │ │ ├── wifi/ │ │ │ │ │ │ │ └── WifiManagerStub.java │ │ │ │ │ │ ├── wifi_scanner/ │ │ │ │ │ │ │ ├── GhostWifiScannerImpl.java │ │ │ │ │ │ │ └── WifiScannerStub.java │ │ │ │ │ │ └── window/ │ │ │ │ │ │ ├── MethodProxies.java │ │ │ │ │ │ ├── WindowManagerStub.java │ │ │ │ │ │ └── session/ │ │ │ │ │ │ ├── BaseMethodProxy.java │ │ │ │ │ │ └── WindowSessionPatch.java │ │ │ │ │ ├── secondary/ │ │ │ │ │ │ ├── HackAppUtils.java │ │ │ │ │ │ ├── ProxyServiceFactory.java │ │ │ │ │ │ ├── ServiceConnectionDelegate.java │ │ │ │ │ │ └── StubBinder.java │ │ │ │ │ └── utils/ │ │ │ │ │ └── MethodParameterUtils.java │ │ │ │ ├── interfaces/ │ │ │ │ │ └── IInjector.java │ │ │ │ ├── ipc/ │ │ │ │ │ ├── ActivityClientRecord.java │ │ │ │ │ ├── LocalProxyUtils.java │ │ │ │ │ ├── ProviderCall.java │ │ │ │ │ ├── ServiceManagerNative.java │ │ │ │ │ ├── VAccountManager.java │ │ │ │ │ ├── VActivityManager.java │ │ │ │ │ ├── VDeviceManager.java │ │ │ │ │ ├── VJobScheduler.java │ │ │ │ │ ├── VNotificationManager.java │ │ │ │ │ ├── VPackageManager.java │ │ │ │ │ └── VirtualStorageManager.java │ │ │ │ ├── natives/ │ │ │ │ │ └── NativeMethods.java │ │ │ │ └── stub/ │ │ │ │ ├── AmsTask.java │ │ │ │ ├── ChooseAccountTypeActivity.java │ │ │ │ ├── ChooseTypeAndAccountActivity.java │ │ │ │ ├── ChooserActivity.java │ │ │ │ ├── DaemonService.java │ │ │ │ ├── ResolverActivity.java │ │ │ │ ├── ShortcutHandleActivity.java │ │ │ │ ├── StubActivity.java │ │ │ │ ├── StubContentProvider.java │ │ │ │ ├── StubDialog.java │ │ │ │ ├── StubJob.java │ │ │ │ ├── StubPendingActivity.java │ │ │ │ ├── StubPendingReceiver.java │ │ │ │ ├── StubPendingService.java │ │ │ │ └── VASettings.java │ │ │ ├── helper/ │ │ │ │ ├── ParcelHelper.java │ │ │ │ ├── PersistenceLayer.java │ │ │ │ ├── collection/ │ │ │ │ │ ├── ArrayMap.java │ │ │ │ │ ├── ArraySet.java │ │ │ │ │ ├── ContainerHelpers.java │ │ │ │ │ ├── IntArray.java │ │ │ │ │ ├── MapCollections.java │ │ │ │ │ ├── SimpleArrayMap.java │ │ │ │ │ └── SparseArray.java │ │ │ │ ├── compat/ │ │ │ │ │ ├── AccountManagerCompat.java │ │ │ │ │ ├── ActivityManagerCompat.java │ │ │ │ │ ├── ApplicationThreadCompat.java │ │ │ │ │ ├── BuildCompat.java │ │ │ │ │ ├── BundleCompat.java │ │ │ │ │ ├── ContentProviderCompat.java │ │ │ │ │ ├── ContentResolverCompat.java │ │ │ │ │ ├── IApplicationThreadCompat.java │ │ │ │ │ ├── NativeLibraryHelperCompat.java │ │ │ │ │ ├── ObjectsCompat.java │ │ │ │ │ ├── PackageParserCompat.java │ │ │ │ │ ├── ParceledListSliceCompat.java │ │ │ │ │ ├── StorageManagerCompat.java │ │ │ │ │ └── SystemPropertiesCompat.java │ │ │ │ └── utils/ │ │ │ │ ├── ArrayUtils.java │ │ │ │ ├── AtomicFile.java │ │ │ │ ├── BitmapUtils.java │ │ │ │ ├── ClassUtils.java │ │ │ │ ├── ComponentUtils.java │ │ │ │ ├── DrawableUtils.java │ │ │ │ ├── FastXmlSerializer.java │ │ │ │ ├── FileUtils.java │ │ │ │ ├── MD5Utils.java │ │ │ │ ├── Mark.java │ │ │ │ ├── OSUtils.java │ │ │ │ ├── Reflect.java │ │ │ │ ├── ReflectException.java │ │ │ │ ├── Singleton.java │ │ │ │ ├── VLog.java │ │ │ │ └── XmlSerializerAndParser.java │ │ │ ├── os/ │ │ │ │ ├── VBinder.java │ │ │ │ ├── VEnvironment.java │ │ │ │ ├── VUserHandle.java │ │ │ │ ├── VUserInfo.java │ │ │ │ └── VUserManager.java │ │ │ ├── remote/ │ │ │ │ ├── AppTaskInfo.java │ │ │ │ ├── InstallResult.java │ │ │ │ ├── InstalledAppInfo.java │ │ │ │ ├── PendingIntentData.java │ │ │ │ ├── PendingResultData.java │ │ │ │ ├── Problem.java │ │ │ │ ├── ReceiverInfo.java │ │ │ │ ├── StubActivityRecord.java │ │ │ │ ├── SyncInfo.java │ │ │ │ ├── VDeviceInfo.java │ │ │ │ └── VParceledListSlice.java │ │ │ └── server/ │ │ │ ├── BinderProvider.java │ │ │ ├── ServiceCache.java │ │ │ ├── accounts/ │ │ │ │ ├── RegisteredServicesParser.java │ │ │ │ ├── VAccount.java │ │ │ │ ├── VAccountManagerService.java │ │ │ │ ├── VContentService.java │ │ │ │ └── VSyncRecord.java │ │ │ ├── am/ │ │ │ │ ├── ActivityRecord.java │ │ │ │ ├── ActivityStack.java │ │ │ │ ├── AppBindRecord.java │ │ │ │ ├── AttributeCache.java │ │ │ │ ├── BroadcastSystem.java │ │ │ │ ├── ConnectionRecord.java │ │ │ │ ├── PendingIntents.java │ │ │ │ ├── ProcessMap.java │ │ │ │ ├── ProcessRecord.java │ │ │ │ ├── ServiceRecord.java │ │ │ │ ├── TaskRecord.java │ │ │ │ ├── UidSystem.java │ │ │ │ └── VActivityManagerService.java │ │ │ ├── device/ │ │ │ │ ├── DeviceInfoPersistenceLayer.java │ │ │ │ └── VDeviceManagerService.java │ │ │ ├── job/ │ │ │ │ └── VJobSchedulerService.java │ │ │ ├── notification/ │ │ │ │ ├── NotificationCompat.java │ │ │ │ ├── NotificationCompatCompatV14.java │ │ │ │ ├── NotificationCompatCompatV21.java │ │ │ │ ├── NotificationFixer.java │ │ │ │ ├── PendIntentCompat.java │ │ │ │ ├── ReflectionActionCompat.java │ │ │ │ ├── RemoteViewsFixer.java │ │ │ │ ├── VNotificationManagerService.java │ │ │ │ └── WidthCompat.java │ │ │ ├── pm/ │ │ │ │ ├── FastImmutableArraySet.java │ │ │ │ ├── IntentResolver.java │ │ │ │ ├── PackageCacheManager.java │ │ │ │ ├── PackagePersistenceLayer.java │ │ │ │ ├── PackageSetting.java │ │ │ │ ├── PackageUserState.java │ │ │ │ ├── PrivilegeAppOptimizer.java │ │ │ │ ├── ProviderIntentResolver.java │ │ │ │ ├── VAppManagerService.java │ │ │ │ ├── VPackageManagerService.java │ │ │ │ ├── VUserManagerService.java │ │ │ │ ├── installer/ │ │ │ │ │ ├── FileBridge.java │ │ │ │ │ ├── PackageHelper.java │ │ │ │ │ ├── PackageInstallInfo.java │ │ │ │ │ ├── PackageInstallObserver.java │ │ │ │ │ ├── PackageInstallerSession.java │ │ │ │ │ ├── SessionInfo.java │ │ │ │ │ ├── SessionParams.java │ │ │ │ │ └── VPackageInstallerService.java │ │ │ │ └── parser/ │ │ │ │ ├── PackageParserEx.java │ │ │ │ └── VPackage.java │ │ │ ├── secondary/ │ │ │ │ ├── BinderDelegateService.java │ │ │ │ └── FakeIdentityBinder.java │ │ │ └── vs/ │ │ │ ├── VSConfig.java │ │ │ ├── VSPersistenceLayer.java │ │ │ └── VirtualStorageService.java │ │ └── mirror/ │ │ ├── MethodParams.java │ │ ├── MethodReflectParams.java │ │ ├── RefBoolean.java │ │ ├── RefClass.java │ │ ├── RefConstructor.java │ │ ├── RefDouble.java │ │ ├── RefFloat.java │ │ ├── RefInt.java │ │ ├── RefLong.java │ │ ├── RefMethod.java │ │ ├── RefObject.java │ │ ├── RefStaticInt.java │ │ ├── RefStaticMethod.java │ │ ├── RefStaticObject.java │ │ ├── android/ │ │ │ ├── accounts/ │ │ │ │ └── IAccountManager.java │ │ │ ├── app/ │ │ │ │ ├── Activity.java │ │ │ │ ├── ActivityManagerNative.java │ │ │ │ ├── ActivityManagerOreo.java │ │ │ │ ├── ActivityThread.java │ │ │ │ ├── ActivityThreadNMR1.java │ │ │ │ ├── ApplicationThreadNative.java │ │ │ │ ├── ContextImpl.java │ │ │ │ ├── ContextImplICS.java │ │ │ │ ├── ContextImplKitkat.java │ │ │ │ ├── IActivityManager.java │ │ │ │ ├── IActivityManagerICS.java │ │ │ │ ├── IActivityManagerL.java │ │ │ │ ├── IActivityManagerN.java │ │ │ │ ├── IAlarmManager.java │ │ │ │ ├── IApplicationThread.java │ │ │ │ ├── IApplicationThreadICSMR1.java │ │ │ │ ├── IApplicationThreadJBMR1.java │ │ │ │ ├── IApplicationThreadKitkat.java │ │ │ │ ├── IApplicationThreadOreo.java │ │ │ │ ├── ISearchManager.java │ │ │ │ ├── LoadedApk.java │ │ │ │ ├── LoadedApkHuaWei.java │ │ │ │ ├── Notification.java │ │ │ │ ├── NotificationL.java │ │ │ │ ├── NotificationM.java │ │ │ │ ├── NotificationManager.java │ │ │ │ ├── PendingIntentJBMR2.java │ │ │ │ ├── backup/ │ │ │ │ │ └── IBackupManager.java │ │ │ │ └── job/ │ │ │ │ ├── IJobScheduler.java │ │ │ │ ├── JobInfo.java │ │ │ │ └── JobParameters.java │ │ │ ├── bluetooth/ │ │ │ │ └── IBluetooth.java │ │ │ ├── content/ │ │ │ │ ├── BroadcastReceiver.java │ │ │ │ ├── ClipboardManager.java │ │ │ │ ├── ClipboardManagerOreo.java │ │ │ │ ├── ContentProviderClient.java │ │ │ │ ├── ContentProviderHolderOreo.java │ │ │ │ ├── ContentProviderNative.java │ │ │ │ ├── ContentResolver.java │ │ │ │ ├── ContentResolverJBMR2.java │ │ │ │ ├── IClipboard.java │ │ │ │ ├── IContentProvider.java │ │ │ │ ├── IContentService.java │ │ │ │ ├── IIntentReceiver.java │ │ │ │ ├── IIntentReceiverJB.java │ │ │ │ ├── IRestrictionsManager.java │ │ │ │ ├── IntentFilter.java │ │ │ │ ├── SyncAdapterType.java │ │ │ │ ├── SyncAdapterTypeN.java │ │ │ │ ├── SyncInfo.java │ │ │ │ ├── SyncRequest.java │ │ │ │ ├── pm/ │ │ │ │ │ ├── ApplicationInfoL.java │ │ │ │ │ ├── ApplicationInfoN.java │ │ │ │ │ ├── ILauncherApps.java │ │ │ │ │ ├── IShortcutService.java │ │ │ │ │ ├── LauncherApps.java │ │ │ │ │ ├── PackageInstaller.java │ │ │ │ │ ├── PackageParser.java │ │ │ │ │ ├── PackageParserJellyBean.java │ │ │ │ │ ├── PackageParserJellyBean17.java │ │ │ │ │ ├── PackageParserLollipop.java │ │ │ │ │ ├── PackageParserLollipop22.java │ │ │ │ │ ├── PackageParserMarshmallow.java │ │ │ │ │ ├── PackageParserNougat.java │ │ │ │ │ ├── PackageUserState.java │ │ │ │ │ ├── ParceledListSlice.java │ │ │ │ │ ├── ParceledListSliceJBMR2.java │ │ │ │ │ └── UserInfo.java │ │ │ │ └── res/ │ │ │ │ ├── AssetManager.java │ │ │ │ └── CompatibilityInfo.java │ │ │ ├── ddm/ │ │ │ │ ├── DdmHandleAppName.java │ │ │ │ └── DdmHandleAppNameJBMR1.java │ │ │ ├── graphics/ │ │ │ │ └── drawable/ │ │ │ │ └── Icon.java │ │ │ ├── hardware/ │ │ │ │ ├── display/ │ │ │ │ │ ├── DisplayManagerGlobal.java │ │ │ │ │ └── IDisplayManager.java │ │ │ │ └── location/ │ │ │ │ └── IContextHubService.java │ │ │ ├── location/ │ │ │ │ ├── ILocationListener.java │ │ │ │ ├── ILocationManager.java │ │ │ │ └── LocationRequestL.java │ │ │ ├── media/ │ │ │ │ ├── AudioManager.java │ │ │ │ ├── IAudioService.java │ │ │ │ ├── IMediaRouterService.java │ │ │ │ ├── MediaRouter.java │ │ │ │ └── session/ │ │ │ │ └── ISessionManager.java │ │ │ ├── net/ │ │ │ │ ├── IConnectivityManager.java │ │ │ │ ├── NetworkInfo.java │ │ │ │ └── wifi/ │ │ │ │ ├── IWifiManager.java │ │ │ │ ├── WifiInfo.java │ │ │ │ ├── WifiScanner.java │ │ │ │ └── WifiSsid.java │ │ │ ├── os/ │ │ │ │ ├── Build.java │ │ │ │ ├── Bundle.java │ │ │ │ ├── Handler.java │ │ │ │ ├── INetworkManagementService.java │ │ │ │ ├── IPowerManager.java │ │ │ │ ├── IUserManager.java │ │ │ │ ├── Message.java │ │ │ │ ├── Process.java │ │ │ │ ├── ServiceManager.java │ │ │ │ ├── StrictMode.java │ │ │ │ └── mount/ │ │ │ │ └── IMountService.java │ │ │ ├── providers/ │ │ │ │ └── Settings.java │ │ │ ├── renderscript/ │ │ │ │ └── RenderScriptCacheDir.java │ │ │ ├── rms/ │ │ │ │ └── resource/ │ │ │ │ ├── ReceiverResourceLP.java │ │ │ │ ├── ReceiverResourceM.java │ │ │ │ └── ReceiverResourceN.java │ │ │ ├── service/ │ │ │ │ └── persistentdata/ │ │ │ │ └── IPersistentDataBlockService.java │ │ │ ├── util/ │ │ │ │ └── Singleton.java │ │ │ ├── view/ │ │ │ │ ├── Display.java │ │ │ │ ├── HardwareRenderer.java │ │ │ │ ├── IGraphicsStats.java │ │ │ │ ├── IWindowManager.java │ │ │ │ ├── RenderScript.java │ │ │ │ ├── SurfaceControl.java │ │ │ │ ├── ThreadedRenderer.java │ │ │ │ └── WindowManagerGlobal.java │ │ │ ├── webkit/ │ │ │ │ ├── IWebViewUpdateService.java │ │ │ │ └── WebViewFactory.java │ │ │ └── widget/ │ │ │ ├── RemoteViews.java │ │ │ └── Toast.java │ │ ├── com/ │ │ │ └── android/ │ │ │ └── internal/ │ │ │ ├── R_Hide.java │ │ │ ├── app/ │ │ │ │ └── IAppOpsService.java │ │ │ ├── appwidget/ │ │ │ │ └── IAppWidgetService.java │ │ │ ├── content/ │ │ │ │ ├── NativeLibraryHelper.java │ │ │ │ └── ReferrerIntent.java │ │ │ ├── os/ │ │ │ │ ├── IDropBoxManagerService.java │ │ │ │ ├── IVibratorService.java │ │ │ │ └── UserManager.java │ │ │ ├── policy/ │ │ │ │ └── PhoneWindow.java │ │ │ ├── telephony/ │ │ │ │ ├── IMms.java │ │ │ │ ├── IPhoneSubInfo.java │ │ │ │ ├── ISms.java │ │ │ │ ├── ISub.java │ │ │ │ ├── ITelephony.java │ │ │ │ ├── ITelephonyRegistry.java │ │ │ │ └── PhoneConstantsMtk.java │ │ │ └── view/ │ │ │ ├── IInputMethodManager.java │ │ │ └── inputmethod/ │ │ │ └── InputMethodManager.java │ │ ├── dalvik/ │ │ │ └── system/ │ │ │ └── VMRuntime.java │ │ ├── java/ │ │ │ └── lang/ │ │ │ ├── ThreadGroup.java │ │ │ └── ThreadGroupN.java │ │ └── libcore/ │ │ └── io/ │ │ ├── ForwardingOs.java │ │ ├── Libcore.java │ │ └── Os.java │ ├── jni/ │ │ ├── Android.mk │ │ ├── Application.mk │ │ ├── Core.cpp │ │ ├── Core.h │ │ ├── Foundation/ │ │ │ ├── IOUniformer.cpp │ │ │ ├── IOUniformer.h │ │ │ ├── VMPatch.cpp │ │ │ └── VMPatch.h │ │ ├── GodinHook/ │ │ │ ├── godin_type.h │ │ │ ├── hookinfo.h │ │ │ ├── instruction/ │ │ │ │ ├── arm_instruction.cpp │ │ │ │ ├── arm_instruction.h │ │ │ │ ├── instruction_helper.cpp │ │ │ │ ├── instruction_helper.h │ │ │ │ ├── thumb_instruction.cpp │ │ │ │ └── thumb_instruction.h │ │ │ ├── mem_helper.cpp │ │ │ ├── mem_helper.h │ │ │ ├── native_hook.cpp │ │ │ ├── native_hook.h │ │ │ ├── thread_helper.cpp │ │ │ └── thread_helper.h │ │ ├── Helper.h │ │ └── MSHook/ │ │ ├── ARM.cpp │ │ ├── ARM.h │ │ ├── CydiaSubstrate.h │ │ ├── Debug.cpp │ │ ├── Debug.h │ │ ├── Hooker.cpp │ │ ├── Hooker.h │ │ ├── Log.h │ │ ├── MSHook.cpp │ │ ├── MSHook.h │ │ ├── PosixMemory.cpp │ │ ├── PosixMemory.h │ │ ├── SubstrateStruct.h │ │ ├── Thumb.cpp │ │ ├── Thumb.h │ │ ├── hde64.h │ │ ├── util.cpp │ │ ├── util.h │ │ ├── x86.cpp │ │ ├── x86.h │ │ ├── x86_64.cpp │ │ └── x86_64.h │ ├── libs/ │ │ ├── android-art-interpret-3.0.0.jar │ │ └── dalvik_hack-3.0.0.5.jar │ └── res/ │ ├── layout/ │ │ ├── app_not_authorized.xml │ │ ├── choose_account_row.xml │ │ ├── choose_account_type.xml │ │ ├── choose_type_and_account.xml │ │ ├── custom_notification.xml │ │ ├── custom_notification_lite.xml │ │ └── resolve_list_item.xml │ └── values/ │ ├── dimens.xml │ ├── integer.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .idea .DS_Store /build /captures ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ import java.security.acl.Group apply plugin: 'com.android.application' apply plugin: 'me.tatarka.retrolambda' android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { applicationId "io.virtualapp" minSdkVersion 15 targetSdkVersion 22 versionCode 24 versionName "1.2.5" multiDexEnabled true android { defaultConfig { ndk { abiFilters "armeabi", "armeabi-v7a", "x86" } } } } buildTypes { release { minifyEnabled false } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } android { lintOptions { checkReleaseBuilds false // Or, if you prefer, you can continue to check for errors in release builds, // but continue the build even when errors are found: abortOnError false } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile project(':lib') //Android Lib compile 'com.android.support:multidex:1.0.1' compile 'com.android.support:appcompat-v7:25.0.1' compile 'com.melnykov:floatingactionbutton:1.3.0' compile 'com.android.support:recyclerview-v7:25.0.1' compile 'com.android.support:percent:25.0.1' compile 'com.android.support:design:25.0.1' compile 'com.android.support:cardview-v7:25.0.1' //Promise Support compile 'org.jdeferred:jdeferred-android-aar:1.2.4' // ThirdParty compile 'com.jonathanfinerty.once:once:1.0.3' compile 'com.flurry.android:analytics:6.9.2' compile 'com.kyleduo.switchbutton:library:1.4.6' compile ('com.google.android.gms:play-services-ads:11.0.0'){ exclude group:'com.android.support' } } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/lody/Desktop/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/io/virtualapp/VApp.java ================================================ package io.virtualapp; import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.support.multidex.MultiDexApplication; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; import com.flurry.android.FlurryAgent; import com.google.android.gms.ads.MobileAds; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.stub.VASettings; import com.lody.virtual.helper.utils.Reflect; import io.virtualapp.delegate.MyAppRequestListener; import io.virtualapp.delegate.MyComponentDelegate; import io.virtualapp.delegate.MyPhoneInfoDelegate; import io.virtualapp.delegate.MyTaskDescriptionDelegate; import jonathanfinerty.once.Once; /** * @author Lody */ // 这里的函数会被多次调用,特性来自于 process: public class VApp extends MultiDexApplication { private static VApp gApp; private SharedPreferences mPreferences; public static VApp getApp() { return gApp; } @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); mPreferences = base.getSharedPreferences("va", Context.MODE_MULTI_PROCESS); VASettings.ENABLE_IO_REDIRECT = true; VASettings.ENABLE_INNER_SHORTCUT = false; try { VirtualCore.get().startup(base); } catch (Throwable e) { e.printStackTrace(); } } @Override public void onCreate() { gApp = this; super.onCreate(); VirtualCore virtualCore = VirtualCore.get(); virtualCore.initialize(new VirtualCore.VirtualInitializer() { @Override public void onMainProcess() { Once.initialise(VApp.this); MobileAds.initialize(VApp.this, "ca-app-pub-1609791120068944~5426483711"); new FlurryAgent.Builder() .withLogEnabled(true) .withListener(() -> { // nothing }) .build(VApp.this, "48RJJP7ZCZZBB6KMMWW5"); } @Override public void onVirtualProcess() { //listener components virtualCore.setComponentDelegate(new MyComponentDelegate()); //fake phone imei,macAddress,BluetoothAddress virtualCore.setPhoneInfoDelegate(new MyPhoneInfoDelegate()); //fake task description's icon and title virtualCore.setTaskDescriptionDelegate(new MyTaskDescriptionDelegate()); } @Override public void onServerProcess() { virtualCore.setAppRequestListener(new MyAppRequestListener(VApp.this)); virtualCore.addVisibleOutsidePackage("com.tencent.mobileqq"); virtualCore.addVisibleOutsidePackage("com.tencent.mobileqqi"); virtualCore.addVisibleOutsidePackage("com.tencent.minihd.qq"); virtualCore.addVisibleOutsidePackage("com.tencent.qqlite"); virtualCore.addVisibleOutsidePackage("com.facebook.katana"); virtualCore.addVisibleOutsidePackage("com.whatsapp"); virtualCore.addVisibleOutsidePackage("com.tencent.mm"); virtualCore.addVisibleOutsidePackage("com.immomo.momo"); } }); } public static SharedPreferences getPreferences() { return getApp().mPreferences; } } ================================================ FILE: app/src/main/java/io/virtualapp/VCommends.java ================================================ package io.virtualapp; /** * @author Lody */ public class VCommends { public static final String TAG_NEW_VERSION = "First launch new Version"; public static final String TAG_SHOW_ADD_APP_GUIDE = "Should show add app guide"; public static final int REQUEST_SELECT_APP = 5; public static final String EXTRA_APP_INFO_LIST = "va.extra.APP_INFO_LIST"; public static final String TAG_ASK_INSTALL_GMS = "va.extra.ASK_INSTALL_GMS"; } ================================================ FILE: app/src/main/java/io/virtualapp/abs/BasePresenter.java ================================================ package io.virtualapp.abs; /** * @author Lody */ public interface BasePresenter { void start(); } ================================================ FILE: app/src/main/java/io/virtualapp/abs/BaseView.java ================================================ package io.virtualapp.abs; import android.app.Activity; import android.content.Context; /** * @author Lody */ public interface BaseView { Activity getActivity(); Context getContext(); void setPresenter(T presenter); } ================================================ FILE: app/src/main/java/io/virtualapp/abs/Callback.java ================================================ package io.virtualapp.abs; /** * @author Lody */ public interface Callback { void callback(T result); } ================================================ FILE: app/src/main/java/io/virtualapp/abs/Value.java ================================================ package io.virtualapp.abs; /** * @author Lody */ public class Value { public T val; } ================================================ FILE: app/src/main/java/io/virtualapp/abs/nestedadapter/RecyclerViewAdapterWrapper.java ================================================ package io.virtualapp.abs.nestedadapter; import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; public class RecyclerViewAdapterWrapper extends RecyclerView.Adapter { protected final RecyclerView.Adapter wrapped; public RecyclerViewAdapterWrapper(RecyclerView.Adapter wrapped) { super(); this.wrapped = wrapped; this.wrapped.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { public void onChanged() { notifyDataSetChanged(); } public void onItemRangeChanged(int positionStart, int itemCount) { notifyItemRangeChanged(positionStart, itemCount); } public void onItemRangeInserted(int positionStart, int itemCount) { notifyItemRangeInserted(positionStart, itemCount); } public void onItemRangeRemoved(int positionStart, int itemCount) { notifyItemRangeRemoved(positionStart, itemCount); } public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { notifyItemMoved(fromPosition, toPosition); } }); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return wrapped.onCreateViewHolder(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { wrapped.onBindViewHolder(holder, position); } @Override public int getItemCount() { return wrapped.getItemCount(); } @Override public int getItemViewType(int position) { return wrapped.getItemViewType(position); } @Override public void setHasStableIds(boolean hasStableIds) { wrapped.setHasStableIds(hasStableIds); } @Override public long getItemId(int position) { return wrapped.getItemId(position); } @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { wrapped.onViewRecycled(holder); } @Override public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) { return wrapped.onFailedToRecycleView(holder); } @Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { wrapped.onViewAttachedToWindow(holder); } @Override public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) { wrapped.onViewDetachedFromWindow(holder); } @Override public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { wrapped.registerAdapterDataObserver(observer); } @Override public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { wrapped.unregisterAdapterDataObserver(observer); } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { wrapped.onAttachedToRecyclerView(recyclerView); } @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) { wrapped.onDetachedFromRecyclerView(recyclerView); } public RecyclerView.Adapter getWrappedAdapter() { return wrapped; } } ================================================ FILE: app/src/main/java/io/virtualapp/abs/nestedadapter/SmartRecyclerAdapter.java ================================================ package io.virtualapp.abs.nestedadapter; import android.support.annotation.NonNull; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.View; import android.view.ViewGroup; public class SmartRecyclerAdapter extends RecyclerViewAdapterWrapper { public static final int TYPE_HEADER = -1; public static final int TYPE_FOOTER = -2; private RecyclerView.LayoutManager layoutManager; private View headerView, footerView; public SmartRecyclerAdapter(@NonNull RecyclerView.Adapter targetAdapter) { super(targetAdapter); } public void setHeaderView(View view) { headerView = view; getWrappedAdapter().notifyDataSetChanged(); } public void removeHeaderView() { headerView = null; getWrappedAdapter().notifyDataSetChanged(); } public void setFooterView(View view) { footerView = view; getWrappedAdapter().notifyDataSetChanged(); } public void removeFooterView() { footerView = null; getWrappedAdapter().notifyDataSetChanged(); } private void setGridHeaderFooter(RecyclerView.LayoutManager layoutManager) { if (layoutManager instanceof GridLayoutManager) { final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { boolean isShowHeader = (position == 0 && hasHeader()); boolean isShowFooter = (position == getItemCount() - 1 && hasFooter()); if (isShowFooter || isShowHeader) { return gridLayoutManager.getSpanCount(); } return 1; } }); } } private boolean hasHeader() { return headerView != null; } private boolean hasFooter() { return footerView != null; } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); layoutManager = recyclerView.getLayoutManager(); setGridHeaderFooter(layoutManager); } @Override public int getItemCount() { return super.getItemCount() + (hasHeader() ? 1 : 0) + (hasFooter() ? 1 : 0); } @Override public int getItemViewType(int position) { if (hasHeader() && position == 0) { return TYPE_HEADER; } if (hasFooter() && position == getItemCount() - 1) { return TYPE_FOOTER; } return super.getItemViewType(hasHeader() ? position - 1 : position); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = null; if (viewType == TYPE_HEADER) { itemView = headerView; } else if (viewType == TYPE_FOOTER) { itemView = footerView; } if (itemView != null) { //set StaggeredGridLayoutManager header & footer view if (layoutManager instanceof StaggeredGridLayoutManager) { ViewGroup.LayoutParams targetParams = itemView.getLayoutParams(); StaggeredGridLayoutManager.LayoutParams StaggerLayoutParams; if (targetParams != null) { StaggerLayoutParams = new StaggeredGridLayoutManager.LayoutParams(targetParams.width, targetParams.height); } else { StaggerLayoutParams = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } StaggerLayoutParams.setFullSpan(true); itemView.setLayoutParams(StaggerLayoutParams); } return new RecyclerView.ViewHolder(itemView) { }; } return super.onCreateViewHolder(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (getItemViewType(position) == TYPE_HEADER || getItemViewType(position) == TYPE_FOOTER) { //if you need get header & footer state , do here return; } super.onBindViewHolder(holder, hasHeader() ? position - 1 : position); } } ================================================ FILE: app/src/main/java/io/virtualapp/abs/percent/PercentLinearLayout.java ================================================ package io.virtualapp.abs.percent; import android.content.Context; import android.content.res.TypedArray; import android.support.percent.PercentLayoutHelper; import android.util.AttributeSet; import android.view.ViewGroup; import android.widget.LinearLayout; /** * @author Lody */ public class PercentLinearLayout extends LinearLayout { private PercentLayoutHelper mPercentLayoutHelper; public PercentLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); mPercentLayoutHelper = new PercentLayoutHelper(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mPercentLayoutHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mPercentLayoutHelper.handleMeasuredStateTooSmall()) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mPercentLayoutHelper.restoreOriginalParams(); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } public static class LayoutParams extends LinearLayout.LayoutParams implements PercentLayoutHelper.PercentLayoutParams { private PercentLayoutHelper.PercentLayoutInfo mPercentLayoutInfo; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); mPercentLayoutInfo = PercentLayoutHelper.getPercentLayoutInfo(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public LayoutParams(MarginLayoutParams source) { super(source); } @Override public PercentLayoutHelper.PercentLayoutInfo getPercentLayoutInfo() { return mPercentLayoutInfo; } @Override protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { PercentLayoutHelper.fetchWidthAndHeight(this, a, widthAttr, heightAttr); } } } ================================================ FILE: app/src/main/java/io/virtualapp/abs/reflect/ReflectException.java ================================================ package io.virtualapp.abs.reflect; /** * @author Lody */ public class ReflectException extends RuntimeException { private static final long serialVersionUID = 663038727503637969L; public ReflectException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/io/virtualapp/abs/ui/VActivity.java ================================================ package io.virtualapp.abs.ui; import android.app.Activity; import android.content.Context; import android.support.annotation.IdRes; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import com.flurry.android.FlurryAgent; import org.jdeferred.android.AndroidDeferredManager; import io.virtualapp.abs.BaseView; /** * @author Lody */ public class VActivity extends AppCompatActivity { /** * Implement of {@link BaseView#getActivity()} */ public Activity getActivity() { return this; } /** * Implement of {@link BaseView#getContext()} ()} */ public Context getContext() { return this; } protected AndroidDeferredManager defer() { return VUiKit.defer(); } public Fragment findFragmentById(@IdRes int id) { return getSupportFragmentManager().findFragmentById(id); } public void replaceFragment(@IdRes int id, Fragment fragment) { getSupportFragmentManager().beginTransaction().replace(id, fragment).commit(); } @Override protected void onStart() { super.onStart(); FlurryAgent.onStartSession(this); } @Override protected void onStop() { super.onStop(); FlurryAgent.onEndSession(this); } } ================================================ FILE: app/src/main/java/io/virtualapp/abs/ui/VFragment.java ================================================ package io.virtualapp.abs.ui; import org.jdeferred.android.AndroidDeferredManager; import android.app.Activity; import android.support.v4.app.Fragment; import io.virtualapp.abs.BasePresenter; /** * @author Lody */ public class VFragment extends Fragment { protected T mPresenter; public T getPresenter() { return mPresenter; } public void setPresenter(T presenter) { this.mPresenter = presenter; } protected AndroidDeferredManager defer() { return VUiKit.defer(); } public void finishActivity() { Activity activity = getActivity(); if (activity != null) { activity.finish(); } } public void destroy() { finishActivity(); } } ================================================ FILE: app/src/main/java/io/virtualapp/abs/ui/VUiKit.java ================================================ package io.virtualapp.abs.ui; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.TypedValue; import org.jdeferred.android.AndroidDeferredManager; /** * @author Lody *

* A set of tools for UI. */ public class VUiKit { private static final AndroidDeferredManager gDM = new AndroidDeferredManager(); private static final Handler gUiHandler = new Handler(Looper.getMainLooper()); public static AndroidDeferredManager defer() { return gDM; } public static int dpToPx(Context context, int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); } public static void post(Runnable r) { gUiHandler.post(r); } public static void postDelayed(long delay, Runnable r) { gUiHandler.postDelayed(r, delay); } public static void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } } ================================================ FILE: app/src/main/java/io/virtualapp/delegate/MyAppRequestListener.java ================================================ package io.virtualapp.delegate; import android.content.Context; import android.widget.Toast; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstallResult; import java.io.IOException; /** * @author Lody */ public class MyAppRequestListener implements VirtualCore.AppRequestListener { private final Context context; public MyAppRequestListener(Context context) { this.context = context; } @Override public void onRequestInstall(String path) { Toast.makeText(context, "Installing: " + path, Toast.LENGTH_SHORT).show(); InstallResult res = VirtualCore.get().installPackage(path, InstallStrategy.UPDATE_IF_EXIST); if (res.isSuccess) { try { VirtualCore.get().preOpt(res.packageName); } catch (IOException e) { e.printStackTrace(); } if (res.isUpdate) { Toast.makeText(context, "Update: " + res.packageName + " success!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(context, "Install: " + res.packageName + " success!", Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(context, "Install failed: " + res.error, Toast.LENGTH_SHORT).show(); } } @Override public void onRequestUninstall(String pkg) { Toast.makeText(context, "Uninstall: " + pkg, Toast.LENGTH_SHORT).show(); } } ================================================ FILE: app/src/main/java/io/virtualapp/delegate/MyComponentDelegate.java ================================================ package io.virtualapp.delegate; import android.app.Activity; import android.content.Intent; import com.lody.virtual.client.hook.delegate.ComponentDelegate; import com.lody.virtual.helper.utils.Reflect; import java.io.File; public class MyComponentDelegate implements ComponentDelegate { @Override public void beforeActivityCreate(Activity activity) { } @Override public void beforeActivityResume(Activity activity) { } @Override public void beforeActivityPause(Activity activity) { } @Override public void beforeActivityDestroy(Activity activity) { } @Override public void afterActivityCreate(Activity activity) { } @Override public void afterActivityResume(Activity activity) { } @Override public void afterActivityPause(Activity activity) { } @Override public void afterActivityDestroy(Activity activity) { } @Override public void onSendBroadcast(Intent intent) { } } ================================================ FILE: app/src/main/java/io/virtualapp/delegate/MyPhoneInfoDelegate.java ================================================ package io.virtualapp.delegate; import com.lody.virtual.client.hook.delegate.PhoneInfoDelegate; /** * Fake the Device ID. */ public class MyPhoneInfoDelegate implements PhoneInfoDelegate { @Override public String getDeviceId(String oldDeviceId, int userId) { return oldDeviceId; } @Override public String getBluetoothAddress(String oldAddress, int userId) { return oldAddress; } @Override public String getMacAddress(String oldAddress, int userId) { return oldAddress; } } ================================================ FILE: app/src/main/java/io/virtualapp/delegate/MyTaskDescriptionDelegate.java ================================================ package io.virtualapp.delegate; import android.annotation.TargetApi; import android.app.ActivityManager; import android.app.Application; import android.os.Build; import com.lody.virtual.client.hook.delegate.TaskDescriptionDelegate; import com.lody.virtual.os.VUserManager; /** * Patch the task description with the (Virtual) user name */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class MyTaskDescriptionDelegate implements TaskDescriptionDelegate { @Override public ActivityManager.TaskDescription getTaskDescription(ActivityManager.TaskDescription oldTaskDescription) { String labelPrefix = "[" + VUserManager.get().getUserName() + "] "; if (!oldTaskDescription.getLabel().startsWith(labelPrefix)) { // Is it really necessary? return new ActivityManager.TaskDescription(labelPrefix + oldTaskDescription.getLabel(), oldTaskDescription.getIcon(), oldTaskDescription.getPrimaryColor()); } else { return oldTaskDescription; } } } ================================================ FILE: app/src/main/java/io/virtualapp/effects/ExplosionAnimator.java ================================================ package io.virtualapp.effects; import java.util.Random; import android.animation.ValueAnimator; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import io.virtualapp.VApp; import io.virtualapp.abs.ui.VUiKit; public class ExplosionAnimator extends ValueAnimator { private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(0.6f); private static final float END_VALUE = 1.4f; private static final float X = VUiKit.dpToPx(VApp.getApp(), 5); private static final float Y = VUiKit.dpToPx(VApp.getApp(), 20); private static final float V = VUiKit.dpToPx(VApp.getApp(), 2); private static final float W = VUiKit.dpToPx(VApp.getApp(), 1); static long DEFAULT_DURATION = 0x450; private Paint mPaint; private Particle[] mParticles; private Rect mBound; private View mContainer; public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) { mPaint = new Paint(); mBound = new Rect(bound); int partLen = 15; mParticles = new Particle[partLen * partLen]; Random random = new Random(System.currentTimeMillis()); int w = bitmap.getWidth() / (partLen + 2); int h = bitmap.getHeight() / (partLen + 2); for (int i = 0; i < partLen; i++) { for (int j = 0; j < partLen; j++) { mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random); } } mContainer = container; setFloatValues(0f, END_VALUE); setInterpolator(DEFAULT_INTERPOLATOR); setDuration(DEFAULT_DURATION); } private Particle generateParticle(int color, Random random) { Particle particle = new Particle(); particle.color = color; particle.radius = V; if (random.nextFloat() < 0.2f) { particle.baseRadius = V + ((X - V) * random.nextFloat()); } else { particle.baseRadius = W + ((V - W) * random.nextFloat()); } float nextFloat = random.nextFloat(); particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f); particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat()); particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f; float f = nextFloat < 0.2f ? particle.bottom : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f; particle.bottom = f; particle.mag = 4.0f * particle.top / particle.bottom; particle.neg = (-particle.mag) / particle.bottom; f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f)); particle.baseCx = f; particle.cx = f; f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f)); particle.baseCy = f; particle.cy = f; particle.life = END_VALUE / 10 * random.nextFloat(); particle.overflow = 0.4f * random.nextFloat(); particle.alpha = 1f; return particle; } public boolean draw(Canvas canvas) { if (!isStarted()) { return false; } for (Particle particle : mParticles) { particle.advance((float) getAnimatedValue()); if (particle.alpha > 0f) { mPaint.setColor(particle.color); mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha)); canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint); } } mContainer.invalidate(); return true; } @Override public void start() { super.start(); mContainer.invalidate(mBound); } private class Particle { float alpha; int color; float cx; float cy; float radius; float baseCx; float baseCy; float baseRadius; float top; float bottom; float mag; float neg; float life; float overflow; public void advance(float factor) { float f = 0f; float normalization = factor / END_VALUE; if (normalization < life || normalization > 1f - overflow) { alpha = 0f; return; } normalization = (normalization - life) / (1f - life - overflow); float f2 = normalization * END_VALUE; if (normalization >= 0.7f) { f = (normalization - 0.7f) / 0.3f; } alpha = 1f - f; f = bottom * f2; cx = baseCx + f; cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag; radius = V + (baseRadius - V) * f2; } } } ================================================ FILE: app/src/main/java/io/virtualapp/effects/ExplosionField.java ================================================ package io.virtualapp.effects; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.ImageView; import io.virtualapp.VApp; import io.virtualapp.abs.ui.VUiKit; public class ExplosionField extends View { private static final Canvas sCanvas = new Canvas(); private List mExplosions = new ArrayList<>(); private int[] mExpandInset = new int[2]; public ExplosionField(Context context) { super(context); init(); } public ExplosionField(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ExplosionField(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public static Bitmap createBitmapFromView(View view) { if (view instanceof ImageView) { Drawable drawable = ((ImageView) view).getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } } view.clearFocus(); Bitmap bitmap = createBitmapSafely(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888, 1); if (bitmap != null) { synchronized (sCanvas) { Canvas canvas = sCanvas; canvas.setBitmap(bitmap); view.draw(canvas); canvas.setBitmap(null); } } return bitmap; } public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) { try { return Bitmap.createBitmap(width, height, config); } catch (OutOfMemoryError e) { e.printStackTrace(); if (retryCount > 0) { System.gc(); return createBitmapSafely(width, height, config, retryCount - 1); } return null; } } public static ExplosionField attachToWindow(Activity activity) { ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT); ExplosionField explosionField = new ExplosionField(activity); rootView.addView(explosionField, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return explosionField; } public static ExplosionField attachToWindow(ViewGroup rootView, Activity activity) { ExplosionField explosionField = new ExplosionField(activity); rootView.addView(explosionField, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return explosionField; } private void init() { Arrays.fill(mExpandInset, VUiKit.dpToPx(VApp.getApp(), 32)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (ExplosionAnimator explosion : mExplosions) { explosion.draw(canvas); } } public void expandExplosionBound(int dx, int dy) { mExpandInset[0] = dx; mExpandInset[1] = dy; } public void explode(Bitmap bitmap, Rect bound, long startDelay, long duration) { final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound); explosion.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mExplosions.remove(animation); } }); explosion.setStartDelay(startDelay); explosion.setDuration(duration); mExplosions.add(explosion); explosion.start(); } public void explode(final View view) { explode(view, null); } public void explode(final View view, OnExplodeFinishListener listener) { Rect r = new Rect(); view.getGlobalVisibleRect(r); int[] location = new int[2]; getLocationOnScreen(location); r.offset(-location[0], -location[1]); r.inset(-mExpandInset[0], -mExpandInset[1]); int startDelay = 100; ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { Random random = new Random(); @Override public void onAnimationUpdate(ValueAnimator animation) { view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f); view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (listener != null) { listener.onExplodeFinish(view); } } }); animator.start(); view.animate().setDuration(150).setStartDelay(startDelay).scaleX(0f).scaleY(0f).alpha(0f).start(); explode(createBitmapFromView(view), r, startDelay, ExplosionAnimator.DEFAULT_DURATION); } public void clear() { mExplosions.clear(); invalidate(); } public interface OnExplodeFinishListener { void onExplodeFinish(View v); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/FlurryROMCollector.java ================================================ package io.virtualapp.home; import android.hardware.Camera; import android.os.Build; import android.util.Log; import com.flurry.android.FlurryAgent; import com.flurry.android.FlurryEventRecordStatus; import com.lody.virtual.client.natives.NativeMethods; import com.lody.virtual.helper.utils.Reflect; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * @author Lody */ public class FlurryROMCollector { private static final String TAG = FlurryROMCollector.class.getSimpleName(); public static void startCollect() { Log.d(TAG, "start collect..."); NativeMethods.init(); if (NativeMethods.gCameraNativeSetup == null) { reportCameraNativeSetup(); } Log.d(TAG, "end collect..."); } private static void reportCameraNativeSetup() { for (Method method : Camera.class.getDeclaredMethods()) { if ("native_setup".equals(method.getName())) { FlurryEventRecordStatus status = FlurryAgent.logEvent("camera::native_setup", createLogContent("method_details", Reflect.getMethodDetails(method))); Log.d(TAG, "report CNS: " + status); break; } } } private static Map createLogContent(String tag, String value) { Map content = new HashMap<>(3); addRomInfo(content); content.put(tag, value); return content; } private static void addRomInfo(Map content) { content.put("device", Build.DEVICE); content.put("brand", Build.BRAND); content.put("manufacturer", Build.MANUFACTURER); content.put("display", Build.DISPLAY); content.put("model", Build.MODEL); content.put("protect", Build.PRODUCT); content.put("sdk_version", "API-" + Build.VERSION.SDK_INT); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/HomeActivity.java ================================================ package io.virtualapp.home; import android.animation.Animator; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.support.v7.widget.helper.ItemTouchHelper; import android.view.ContextThemeWrapper; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import com.lody.virtual.GmsSupport; import com.lody.virtual.client.stub.ChooseTypeAndAccountActivity; import com.lody.virtual.os.VUserInfo; import com.lody.virtual.os.VUserManager; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.nestedadapter.SmartRecyclerAdapter; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.adapters.LaunchpadAdapter; import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration; import io.virtualapp.home.models.AddAppButton; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.home.models.EmptyAppData; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.widgets.TwoGearsView; import static android.support.v7.widget.helper.ItemTouchHelper.ACTION_STATE_DRAG; import static android.support.v7.widget.helper.ItemTouchHelper.DOWN; import static android.support.v7.widget.helper.ItemTouchHelper.END; import static android.support.v7.widget.helper.ItemTouchHelper.LEFT; import static android.support.v7.widget.helper.ItemTouchHelper.RIGHT; import static android.support.v7.widget.helper.ItemTouchHelper.START; import static android.support.v7.widget.helper.ItemTouchHelper.UP; /** * @author Lody */ public class HomeActivity extends VActivity implements HomeContract.HomeView { private static final String TAG = HomeActivity.class.getSimpleName(); private HomeContract.HomePresenter mPresenter; private TwoGearsView mLoadingView; private RecyclerView mLauncherView; private View mMenuView; private PopupMenu mPopupMenu; private View mBottomArea; private View mCreateShortcutBox; private TextView mCreateShortcutTextView; private View mDeleteAppBox; private TextView mDeleteAppTextView; private LaunchpadAdapter mLaunchpadAdapter; private Handler mUiHandler; public static void goHome(Context context) { Intent intent = new Intent(context, HomeActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { overridePendingTransition(0, 0); super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); mUiHandler = new Handler(Looper.getMainLooper()); bindViews(); initLaunchpad(); initMenu(); new HomePresenterImpl(this).start(); } private void initMenu() { mPopupMenu = new PopupMenu(new ContextThemeWrapper(this, R.style.Theme_AppCompat_Light), mMenuView); Menu menu = mPopupMenu.getMenu(); setIconEnable(menu, true); menu.add("Accounts").setIcon(R.drawable.ic_account).setOnMenuItemClickListener(item -> { List users = VUserManager.get().getUsers(); List names = new ArrayList<>(users.size()); for (VUserInfo info : users) { names.add(info.name); } CharSequence[] items = new CharSequence[names.size()]; for (int i = 0; i < names.size(); i++) { items[i] = names.get(i); } new AlertDialog.Builder(this) .setTitle("Please select an user") .setItems(items, (dialog, which) -> { VUserInfo info = users.get(which); Intent intent = new Intent(this, ChooseTypeAndAccountActivity.class); intent.putExtra(ChooseTypeAndAccountActivity.KEY_USER_ID, info.id); startActivity(intent); }).show(); return false; }); menu.add("Virtual Storage").setIcon(R.drawable.ic_vs).setOnMenuItemClickListener(item -> { Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show(); return false; }); menu.add("Notification").setIcon(R.drawable.ic_notification).setOnMenuItemClickListener(item -> { Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show(); return false; }); menu.add("Settings").setIcon(R.drawable.ic_settings).setOnMenuItemClickListener(item -> { Toast.makeText(this, "The coming", Toast.LENGTH_SHORT).show(); return false; }); mMenuView.setOnClickListener(v -> mPopupMenu.show()); } private static void setIconEnable(Menu menu, boolean enable) { try { @SuppressLint("PrivateApi") Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class); m.setAccessible(true); m.invoke(menu, enable); } catch (Exception e) { e.printStackTrace(); } } private void bindViews() { mLoadingView = (TwoGearsView) findViewById(R.id.pb_loading_app); mLauncherView = (RecyclerView) findViewById(R.id.home_launcher); mMenuView = findViewById(R.id.home_menu); mBottomArea = findViewById(R.id.bottom_area); mCreateShortcutBox = findViewById(R.id.create_shortcut_area); mCreateShortcutTextView = (TextView) findViewById(R.id.create_shortcut_text); mDeleteAppBox = findViewById(R.id.delete_app_area); mDeleteAppTextView = (TextView) findViewById(R.id.delete_app_text); } private void initLaunchpad() { mLauncherView.setHasFixedSize(true); StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL); mLauncherView.setLayoutManager(layoutManager); mLaunchpadAdapter = new LaunchpadAdapter(this); SmartRecyclerAdapter wrap = new SmartRecyclerAdapter(mLaunchpadAdapter); View footer = new View(this); footer.setLayoutParams(new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, VUiKit.dpToPx(this, 60))); wrap.setFooterView(footer); mLauncherView.setAdapter(wrap); mLauncherView.addItemDecoration(new ItemOffsetDecoration(this, R.dimen.desktop_divider)); ItemTouchHelper touchHelper = new ItemTouchHelper(new LauncherTouchCallback()); touchHelper.attachToRecyclerView(mLauncherView); mLaunchpadAdapter.setAppClickListener((pos, data) -> { if (!data.isLoading()) { if (data instanceof AddAppButton) { onAddAppButtonClick(); } mLaunchpadAdapter.notifyItemChanged(pos); mPresenter.launchApp(data); } }); } private void onAddAppButtonClick() { ListAppActivity.gotoListApp(this); } private void deleteApp(int position) { AppData data = mLaunchpadAdapter.getList().get(position); new AlertDialog.Builder(this) .setTitle("Delete app") .setMessage("Do you want to delete " + data.getName() + "?") .setPositiveButton(android.R.string.yes, (dialog, which) -> { mPresenter.deleteApp(data); }) .setNegativeButton(android.R.string.no, null) .show(); } private void createShortcut(int position) { AppData model = mLaunchpadAdapter.getList().get(position); if (model instanceof PackageAppData) { mPresenter.createShortcut(model); } } @Override public void setPresenter(HomeContract.HomePresenter presenter) { mPresenter = presenter; } @Override public void showBottomAction() { mBottomArea.setTranslationY(mBottomArea.getHeight()); mBottomArea.setVisibility(View.VISIBLE); mBottomArea.animate().translationY(0).setDuration(500L).start(); } @Override public void hideBottomAction() { mBottomArea.setTranslationY(0); ObjectAnimator transAnim = ObjectAnimator.ofFloat(mBottomArea, "translationY", 0, mBottomArea.getHeight()); transAnim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { mBottomArea.setVisibility(View.GONE); } @Override public void onAnimationCancel(Animator animator) { mBottomArea.setVisibility(View.GONE); } @Override public void onAnimationRepeat(Animator animator) { } }); transAnim.setDuration(500L); transAnim.start(); } @Override public void showLoading() { mLoadingView.setVisibility(View.VISIBLE); mLoadingView.startAnim(); } @Override public void hideLoading() { mLoadingView.setVisibility(View.GONE); mLoadingView.stopAnim(); } @Override public void loadFinish(List list) { list.add(new AddAppButton(this)); mLaunchpadAdapter.setList(list); hideLoading(); } @Override public void loadError(Throwable err) { err.printStackTrace(); hideLoading(); } @Override public void showGuide() { } @Override public void addAppToLauncher(AppData model) { List dataList = mLaunchpadAdapter.getList(); boolean replaced = false; for (int i = 0; i < dataList.size(); i++) { AppData data = dataList.get(i); if (data instanceof EmptyAppData) { mLaunchpadAdapter.replace(i, model); replaced = true; break; } } if (!replaced) { mLaunchpadAdapter.add(model); mLauncherView.smoothScrollToPosition(mLaunchpadAdapter.getItemCount() - 1); } } @Override public void removeAppToLauncher(AppData model) { mLaunchpadAdapter.remove(model); } @Override public void refreshLauncherItem(AppData model) { mLaunchpadAdapter.refresh(model); } @Override public void askInstallGms() { new AlertDialog.Builder(this) .setTitle("Hi") .setMessage("We found that your device has been installed the Google service, whether you need to install them?") .setPositiveButton(android.R.string.ok, (dialog, which) -> { defer().when(() -> { GmsSupport.installGApps(0); }).done((res) -> { mPresenter.dataChanged(); }); }) .setNegativeButton(android.R.string.cancel, (dialog, which) -> Toast.makeText(HomeActivity.this, "You can also find it in the Settings~", Toast.LENGTH_LONG).show()) .setCancelable(false) .show(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && data != null) { List appList = data.getParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST); if (appList != null) { for (AppInfoLite info : appList) { mPresenter.addApp(info); } } } } private class LauncherTouchCallback extends ItemTouchHelper.SimpleCallback { int[] location = new int[2]; boolean upAtDeleteAppArea; boolean upAtCreateShortcutArea; RecyclerView.ViewHolder dragHolder; LauncherTouchCallback() { super(UP | DOWN | LEFT | RIGHT | START | END, 0); } @Override public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll) { return 0; } @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { try { AppData data = mLaunchpadAdapter.getList().get(viewHolder.getAdapterPosition()); if (!data.canReorder()) { return makeMovementFlags(0, 0); } } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } return super.getMovementFlags(recyclerView, viewHolder); } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { int pos = viewHolder.getAdapterPosition(); int targetPos = target.getAdapterPosition(); mLaunchpadAdapter.moveItem(pos, targetPos); return true; } @Override public boolean isLongPressDragEnabled() { return true; } @Override public boolean isItemViewSwipeEnabled() { return false; } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { if (viewHolder instanceof LaunchpadAdapter.ViewHolder) { if (actionState == ACTION_STATE_DRAG) { if (dragHolder != viewHolder) { dragHolder = viewHolder; viewHolder.itemView.setScaleX(1.2f); viewHolder.itemView.setScaleY(1.2f); if (mBottomArea.getVisibility() == View.GONE) { showBottomAction(); } } } } super.onSelectedChanged(viewHolder, actionState); } @Override public boolean canDropOver(RecyclerView recyclerView, RecyclerView.ViewHolder current, RecyclerView.ViewHolder target) { if (upAtCreateShortcutArea || upAtDeleteAppArea) { return false; } try { AppData data = mLaunchpadAdapter.getList().get(target.getAdapterPosition()); return data.canReorder(); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } return false; } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { if (viewHolder instanceof LaunchpadAdapter.ViewHolder) { LaunchpadAdapter.ViewHolder holder = (LaunchpadAdapter.ViewHolder) viewHolder; viewHolder.itemView.setScaleX(1f); viewHolder.itemView.setScaleY(1f); viewHolder.itemView.setBackgroundColor(holder.color); } super.clearView(recyclerView, viewHolder); if (dragHolder == viewHolder) { if (mBottomArea.getVisibility() == View.VISIBLE) { mUiHandler.postDelayed(HomeActivity.this::hideBottomAction, 200L); if (upAtCreateShortcutArea) { createShortcut(viewHolder.getAdapterPosition()); } else if (upAtDeleteAppArea) { deleteApp(viewHolder.getAdapterPosition()); } } dragHolder = null; } } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { } @Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); if (actionState != ACTION_STATE_DRAG || !isCurrentlyActive) { return; } View itemView = viewHolder.itemView; itemView.getLocationInWindow(location); int x = (int) (location[0] + dX); int y = (int) (location[1] + dY); mBottomArea.getLocationInWindow(location); int baseLine = location[1] - mBottomArea.getHeight(); if (y >= baseLine) { mDeleteAppBox.getLocationInWindow(location); int deleteAppAreaStartX = location[0]; if (x < deleteAppAreaStartX) { upAtCreateShortcutArea = true; upAtDeleteAppArea = false; mCreateShortcutTextView.setTextColor(Color.parseColor("#0099cc")); mDeleteAppTextView.setTextColor(Color.WHITE); } else { upAtDeleteAppArea = true; upAtCreateShortcutArea = false; mDeleteAppTextView.setTextColor(Color.parseColor("#0099cc")); mCreateShortcutTextView.setTextColor(Color.WHITE); } } else { upAtCreateShortcutArea = false; upAtDeleteAppArea = false; mDeleteAppTextView.setTextColor(Color.WHITE); mCreateShortcutTextView.setTextColor(Color.WHITE); } } } } ================================================ FILE: app/src/main/java/io/virtualapp/home/HomeContract.java ================================================ package io.virtualapp.home; import java.util.List; import io.virtualapp.abs.BasePresenter; import io.virtualapp.abs.BaseView; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfoLite; /** * @author Lody */ /* package */ class HomeContract { /* package */ interface HomeView extends BaseView { void showBottomAction(); void hideBottomAction(); void showLoading(); void hideLoading(); void loadFinish(List appModels); void loadError(Throwable err); void showGuide(); void addAppToLauncher(AppData model); void removeAppToLauncher(AppData model); void refreshLauncherItem(AppData model); void askInstallGms(); } /* package */ interface HomePresenter extends BasePresenter { void launchApp(AppData data); void dataChanged(); void addApp(AppInfoLite info); void deleteApp(AppData data); void createShortcut(AppData data); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/HomePresenterImpl.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.graphics.Bitmap; import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.InterstitialAd; import com.lody.virtual.GmsSupport; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.os.VUserInfo; import com.lody.virtual.os.VUserManager; import com.lody.virtual.remote.InstallResult; import com.lody.virtual.remote.InstalledAppInfo; import java.io.IOException; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.ads.AdScheduler; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.home.models.MultiplePackageAppData; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.home.repo.AppRepository; import io.virtualapp.home.repo.PackageAppDataStorage; import jonathanfinerty.once.Once; /** * @author Lody */ class HomePresenterImpl implements HomeContract.HomePresenter { private HomeContract.HomeView mView; private Activity mActivity; private AppRepository mRepo; private InterstitialAd mInterstitialAd; private AdScheduler mScheduler = new AdScheduler(10000L); private AppData mTempAppData; HomePresenterImpl(HomeContract.HomeView view) { mView = view; mActivity = view.getActivity(); mRepo = new AppRepository(mActivity); mView.setPresenter(this); mInterstitialAd = new InterstitialAd(mActivity); mInterstitialAd.setAdUnitId("ca-app-pub-1609791120068944/6903216910"); mInterstitialAd.loadAd(new AdRequest.Builder().build()); mInterstitialAd.setAdListener(new AdListener() { @Override public void onAdClosed() { if (mTempAppData != null) { launchApp(mTempAppData); mTempAppData = null; } mInterstitialAd.loadAd(new AdRequest.Builder().build()); } }); } @Override public void start() { dataChanged(); if (!Once.beenDone(VCommends.TAG_SHOW_ADD_APP_GUIDE)) { mView.showGuide(); Once.markDone(VCommends.TAG_SHOW_ADD_APP_GUIDE); } if (!Once.beenDone(VCommends.TAG_ASK_INSTALL_GMS) && GmsSupport.isOutsideGoogleFrameworkExist()) { mView.askInstallGms(); Once.markDone(VCommends.TAG_ASK_INSTALL_GMS); } } @Override public void launchApp(AppData data) { if (mInterstitialAd.isLoaded() && mScheduler.shouldShowAd()) { mTempAppData = data; mInterstitialAd.show(); mScheduler.adShowed(); } else { launchAppNoAd(data); } } public void launchAppNoAd(AppData data) { try { if (data instanceof PackageAppData) { PackageAppData appData = (PackageAppData) data; appData.isFirstOpen = false; LoadingActivity.launch(mActivity, appData.packageName, 0); } else if (data instanceof MultiplePackageAppData) { MultiplePackageAppData multipleData = (MultiplePackageAppData) data; multipleData.isFirstOpen = false; LoadingActivity.launch(mActivity, multipleData.appInfo.packageName, ((MultiplePackageAppData) data).userId); } } catch (Throwable e) { e.printStackTrace(); } } @Override public void dataChanged() { mView.showLoading(); mRepo.getVirtualApps().done(mView::loadFinish).fail(mView::loadError); } @Override public void addApp(AppInfoLite info) { class AddResult { private PackageAppData appData; private int userId; private boolean justEnableHidden; } AddResult addResult = new AddResult(); VUiKit.defer().when(() -> { InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(info.packageName, 0); addResult.justEnableHidden = installedAppInfo != null; if (addResult.justEnableHidden) { int[] userIds = installedAppInfo.getInstalledUsers(); int nextUserId = userIds.length; /* Input : userIds = {0, 1, 3} Output: nextUserId = 2 */ for (int i = 0; i < userIds.length; i++) { if (userIds[i] != i) { nextUserId = i; break; } } addResult.userId = nextUserId; if (VUserManager.get().getUserInfo(nextUserId) == null) { // user not exist, create it automatically. String nextUserName = "Space " + (nextUserId + 1); VUserInfo newUserInfo = VUserManager.get().createUser(nextUserName, VUserInfo.FLAG_ADMIN); if (newUserInfo == null) { throw new IllegalStateException(); } } boolean success = VirtualCore.get().installPackageAsUser(nextUserId, info.packageName); if (!success) { throw new IllegalStateException(); } } else { InstallResult res = mRepo.addVirtualApp(info); if (!res.isSuccess) { throw new IllegalStateException(); } } }).then((res) -> { addResult.appData = PackageAppDataStorage.get().acquire(info.packageName); }).done(res -> { boolean multipleVersion = addResult.justEnableHidden && addResult.userId != 0; if (!multipleVersion) { PackageAppData data = addResult.appData; data.isLoading = true; mView.addAppToLauncher(data); handleOptApp(data, info.packageName, true); } else { MultiplePackageAppData data = new MultiplePackageAppData(addResult.appData, addResult.userId); data.isLoading = true; mView.addAppToLauncher(data); handleOptApp(data, info.packageName, false); } }); } private void handleOptApp(AppData data, String packageName, boolean needOpt) { VUiKit.defer().when(() -> { long time = System.currentTimeMillis(); if (needOpt) { try { VirtualCore.get().preOpt(packageName); } catch (IOException e) { e.printStackTrace(); } } time = System.currentTimeMillis() - time; if (time < 1500L) { try { Thread.sleep(1500L - time); } catch (InterruptedException e) { e.printStackTrace(); } } }).done((res) -> { if (data instanceof PackageAppData) { ((PackageAppData) data).isLoading = false; ((PackageAppData) data).isFirstOpen = true; } else if (data instanceof MultiplePackageAppData) { ((MultiplePackageAppData) data).isLoading = false; ((MultiplePackageAppData) data).isFirstOpen = true; } mView.refreshLauncherItem(data); }); } @Override public void deleteApp(AppData data) { try { mView.removeAppToLauncher(data); if (data instanceof PackageAppData) { mRepo.removeVirtualApp(((PackageAppData) data).packageName, 0); } else { MultiplePackageAppData appData = (MultiplePackageAppData) data; mRepo.removeVirtualApp(appData.appInfo.packageName, appData.userId); } } catch (Throwable e) { e.printStackTrace(); } } @Override public void createShortcut(AppData data) { VirtualCore.OnEmitShortcutListener listener = new VirtualCore.OnEmitShortcutListener() { @Override public Bitmap getIcon(Bitmap originIcon) { return originIcon; } @Override public String getName(String originName) { return originName + "(VA)"; } }; if (data instanceof PackageAppData) { VirtualCore.get().createShortcut(0, ((PackageAppData) data).packageName, listener); } else if (data instanceof MultiplePackageAppData) { MultiplePackageAppData appData = (MultiplePackageAppData) data; VirtualCore.get().createShortcut(appData.userId, appData.appInfo.packageName, listener); } } } ================================================ FILE: app/src/main/java/io/virtualapp/home/ListAppActivity.java ================================================ package io.virtualapp.home; import android.Manifest; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; import android.support.v4.app.ActivityCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.MenuItem; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.home.adapters.AppPagerAdapter; /** * @author Lody */ public class ListAppActivity extends VActivity { private Toolbar mToolBar; private TabLayout mTabLayout; private ViewPager mViewPager; public static void gotoListApp(Activity activity) { Intent intent = new Intent(activity, ListAppActivity.class); activity.startActivityForResult(intent, VCommends.REQUEST_SELECT_APP); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.colorPrimaryDark))); setContentView(R.layout.activity_clone_app); mToolBar = (Toolbar) findViewById(R.id.clone_app_tool_bar); mTabLayout = (TabLayout) mToolBar.findViewById(R.id.clone_app_tab_layout); mViewPager = (ViewPager) findViewById(R.id.clone_app_view_pager); setupToolBar(); mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager())); mTabLayout.setupWithViewPager(mViewPager); // Request permission to access external storage if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0); } } } private void setupToolBar() { setSupportActionBar(mToolBar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } return super.onOptionsItemSelected(item); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { for (int result : grantResults) { if (result == PackageManager.PERMISSION_GRANTED) { mViewPager.setAdapter(new AppPagerAdapter(getSupportFragmentManager())); break; } } } } ================================================ FILE: app/src/main/java/io/virtualapp/home/ListAppContract.java ================================================ package io.virtualapp.home; import java.util.List; import io.virtualapp.abs.BasePresenter; import io.virtualapp.abs.BaseView; import io.virtualapp.home.models.AppInfo; /** * @author Lody * @version 1.0 */ /*package*/ class ListAppContract { interface ListAppView extends BaseView { void startLoading(); void loadFinish(List infoList); } interface ListAppPresenter extends BasePresenter { } } ================================================ FILE: app/src/main/java/io/virtualapp/home/ListAppFragment.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.Toast; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Locale; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VFragment; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.adapters.CloneAppListAdapter; import io.virtualapp.home.adapters.decorations.ItemOffsetDecoration; import io.virtualapp.home.models.AppInfo; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.widgets.DragSelectRecyclerView; /** * @author Lody */ public class ListAppFragment extends VFragment implements ListAppContract.ListAppView { private static final String KEY_SELECT_FROM = "key_select_from"; private DragSelectRecyclerView mRecyclerView; private ProgressBar mProgressBar; private Button mInstallButton; private CloneAppListAdapter mAdapter; public static ListAppFragment newInstance(File selectFrom) { Bundle args = new Bundle(); if (selectFrom != null) args.putString(KEY_SELECT_FROM, selectFrom.getPath()); ListAppFragment fragment = new ListAppFragment(); fragment.setArguments(args); return fragment; } private File getSelectFrom() { Bundle bundle = getArguments(); if (bundle != null) { String selectFrom = bundle.getString(KEY_SELECT_FROM); if (selectFrom != null) { return new File(selectFrom); } } return null; } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_list_app, null); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mAdapter.saveInstanceState(outState); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { mRecyclerView = (DragSelectRecyclerView) view.findViewById(R.id.select_app_recycler_view); mProgressBar = (ProgressBar) view.findViewById(R.id.select_app_progress_bar); mInstallButton = (Button) view.findViewById(R.id.select_app_install_btn); mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, OrientationHelper.VERTICAL)); mRecyclerView.addItemDecoration(new ItemOffsetDecoration(VUiKit.dpToPx(getContext(), 2))); mAdapter = new CloneAppListAdapter(getActivity()); mRecyclerView.setAdapter(mAdapter); mAdapter.setOnItemClickListener(new CloneAppListAdapter.ItemEventListener() { @Override public void onItemClick(AppInfo info, int position) { int count = mAdapter.getSelectedCount(); if (!mAdapter.isIndexSelected(position)) { if (count >= 9) { Toast.makeText(getContext(), R.string.install_too_much_once_time, Toast.LENGTH_SHORT).show(); return; } } mAdapter.toggleSelected(position); } @Override public boolean isSelectable(int position) { return mAdapter.isIndexSelected(position) || mAdapter.getSelectedCount() < 9; } }); mAdapter.setSelectionListener(count -> { mInstallButton.setEnabled(count > 0); mInstallButton.setText(String.format(Locale.ENGLISH, getResources().getString(R.string.install_d), count)); }); mInstallButton.setOnClickListener(v -> { Integer[] selectedIndices = mAdapter.getSelectedIndices(); ArrayList dataList = new ArrayList(selectedIndices.length); for (int index : selectedIndices) { AppInfo info = mAdapter.getItem(index); dataList.add(new AppInfoLite(info.packageName, info.path, info.fastOpen)); } Intent data = new Intent(); data.putParcelableArrayListExtra(VCommends.EXTRA_APP_INFO_LIST, dataList); getActivity().setResult(Activity.RESULT_OK, data); getActivity().finish(); }); new ListAppPresenterImpl(getActivity(), this, getSelectFrom()).start(); } @Override public void startLoading() { mProgressBar.setVisibility(View.VISIBLE); mRecyclerView.setVisibility(View.GONE); } @Override public void loadFinish(List infoList) { mAdapter.setList(infoList); mRecyclerView.setDragSelectActive(true, 0); mAdapter.setSelected(0, false); mProgressBar.setVisibility(View.GONE); mRecyclerView.setVisibility(View.VISIBLE); } @Override public void setPresenter(ListAppContract.ListAppPresenter presenter) { this.mPresenter = presenter; } } ================================================ FILE: app/src/main/java/io/virtualapp/home/ListAppPresenterImpl.java ================================================ package io.virtualapp.home; import android.app.Activity; import android.content.Intent; import java.io.File; import io.virtualapp.VCommends; import io.virtualapp.home.repo.AppDataSource; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.home.repo.AppRepository; /** * @author Lody */ class ListAppPresenterImpl implements ListAppContract.ListAppPresenter { private Activity mActivity; private ListAppContract.ListAppView mView; private AppDataSource mRepository; private File from; ListAppPresenterImpl(Activity activity, ListAppContract.ListAppView view, File fromWhere) { mActivity = activity; mView = view; mRepository = new AppRepository(activity); mView.setPresenter(this); this.from = fromWhere; } @Override public void start() { mView.setPresenter(this); mView.startLoading(); if (from == null) mRepository.getInstalledApps(mActivity).done(mView::loadFinish); else mRepository.getStorageApps(mActivity, from).done(mView::loadFinish); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/LoadingActivity.java ================================================ package io.virtualapp.home; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.RemoteException; import android.widget.ImageView; import android.widget.TextView; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.client.ipc.VActivityManager; import java.util.Locale; import io.virtualapp.R; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.PackageAppData; import io.virtualapp.home.repo.PackageAppDataStorage; import io.virtualapp.widgets.EatBeansView; /** * @author Lody */ public class LoadingActivity extends VActivity { private static final String PKG_NAME_ARGUMENT = "MODEL_ARGUMENT"; private static final String KEY_INTENT = "KEY_INTENT"; private static final String KEY_USER = "KEY_USER"; private PackageAppData appModel; private EatBeansView loadingView; public static void launch(Context context, String packageName, int userId) { Intent intent = VirtualCore.get().getLaunchIntent(packageName, userId); if (intent != null) { Intent loadingPageIntent = new Intent(context, LoadingActivity.class); loadingPageIntent.putExtra(PKG_NAME_ARGUMENT, packageName); loadingPageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); loadingPageIntent.putExtra(KEY_INTENT, intent); loadingPageIntent.putExtra(KEY_USER, userId); context.startActivity(loadingPageIntent); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_loading); loadingView = (EatBeansView) findViewById(R.id.loading_anim); int userId = getIntent().getIntExtra(KEY_USER, -1); String pkg = getIntent().getStringExtra(PKG_NAME_ARGUMENT); appModel = PackageAppDataStorage.get().acquire(pkg); ImageView iconView = (ImageView) findViewById(R.id.app_icon); iconView.setImageDrawable(appModel.icon); TextView nameView = (TextView) findViewById(R.id.app_name); nameView.setText(String.format(Locale.ENGLISH, "Opening %s...", appModel.name)); Intent intent = getIntent().getParcelableExtra(KEY_INTENT); if (intent == null) { return; } VirtualCore.get().setUiCallback(intent, mUiCallback); VUiKit.defer().when(() -> { long startTime = System.currentTimeMillis(); if (!appModel.fastOpen) { try { VirtualCore.get().preOpt(appModel.packageName); } catch (Exception e) { e.printStackTrace(); } } long spend = System.currentTimeMillis() - startTime; if (spend < 500) { try { Thread.sleep(500 - spend); } catch (InterruptedException e) { e.printStackTrace(); } } VActivityManager.get().startActivity(intent, userId); }); } private final VirtualCore.UiCallback mUiCallback = new VirtualCore.UiCallback() { @Override public void onAppOpened(String packageName, int userId) throws RemoteException { finish(); } }; @Override protected void onResume() { super.onResume(); loadingView.startAnim(); } @Override protected void onPause() { super.onPause(); loadingView.stopAnim(); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/adapters/AppPagerAdapter.java ================================================ package io.virtualapp.home.adapters; import android.content.Context; import android.os.Build; import android.os.Environment; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import com.lody.virtual.helper.utils.Reflect; import java.io.File; import java.util.ArrayList; import java.util.List; import io.virtualapp.R; import io.virtualapp.VApp; import io.virtualapp.home.ListAppFragment; /** * @author Lody */ public class AppPagerAdapter extends FragmentPagerAdapter { private List titles = new ArrayList<>(); private List dirs = new ArrayList<>(); public AppPagerAdapter(FragmentManager fm) { super(fm); titles.add(VApp.getApp().getResources().getString(R.string.clone_apps)); dirs.add(null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Context ctx = VApp.getApp(); StorageManager storage = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE); for (StorageVolume volume : storage.getStorageVolumes()) { //Why the fuck are getPathFile and getUserLabel hidden?! //StorageVolume is kinda useless without those... File dir = Reflect.on(volume).call("getPathFile").get(); String label = Reflect.on(volume).call("getUserLabel").get(); if (dir.listFiles() != null) { titles.add(label); dirs.add(dir); } } } else { // Fallback: only support the default storage sources File storageFir = Environment.getExternalStorageDirectory(); if (storageFir != null && storageFir.isDirectory()) { titles.add(VApp.getApp().getResources().getString(R.string.external_storage)); dirs.add(storageFir); } } } @Override public Fragment getItem(int position) { return ListAppFragment.newInstance(dirs.get(position)); } @Override public int getCount() { return titles.size(); } @Override public CharSequence getPageTitle(int position) { return titles.get(position); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/adapters/CloneAppListAdapter.java ================================================ package io.virtualapp.home.adapters; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import java.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.AppInfo; import io.virtualapp.widgets.DragSelectRecyclerViewAdapter; import io.virtualapp.widgets.LabelView; /** * @author Lody */ public class CloneAppListAdapter extends DragSelectRecyclerViewAdapter { private static final int TYPE_FOOTER = -2; private final View mFooterView; private LayoutInflater mInflater; private List mAppList; private ItemEventListener mItemEventListener; public CloneAppListAdapter(Context context) { this.mInflater = LayoutInflater.from(context); mFooterView = new View(context); StaggeredGridLayoutManager.LayoutParams params = new StaggeredGridLayoutManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, VUiKit.dpToPx(context, 60) ); params.setFullSpan(true); mFooterView.setLayoutParams(params); } public void setOnItemClickListener(ItemEventListener mItemEventListener) { this.mItemEventListener = mItemEventListener; } public List getList() { return mAppList; } public void setList(List models) { this.mAppList = models; notifyDataSetChanged(); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == TYPE_FOOTER) { return new ViewHolder(mFooterView); } return new ViewHolder(mInflater.inflate(R.layout.item_clone_app, null)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { if (getItemViewType(position) == TYPE_FOOTER) { return; } super.onBindViewHolder(holder, position); AppInfo info = mAppList.get(position); holder.iconView.setImageDrawable(info.icon); holder.nameView.setText(info.name); if (isIndexSelected(position)) { holder.iconView.setAlpha(1f); holder.appCheckView.setImageResource(R.drawable.ic_check); } else { holder.iconView.setAlpha(0.65f); holder.appCheckView.setImageResource(R.drawable.ic_no_check); } if (info.cloneCount > 0) { holder.labelView.setVisibility(View.VISIBLE); holder.labelView.setText(info.cloneCount + 1 + ""); } else { holder.labelView.setVisibility(View.INVISIBLE); } holder.itemView.setOnClickListener(v -> { mItemEventListener.onItemClick(info, position); }); } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); } @Override protected boolean isIndexSelectable(int index) { return mItemEventListener.isSelectable(index); } @Override public int getItemCount() { return mAppList == null ? 1 : mAppList.size() + 1; } @Override public int getItemViewType(int position) { if (position == getItemCount() - 1) { return TYPE_FOOTER; } return super.getItemViewType(position); } public AppInfo getItem(int index) { return mAppList.get(index); } public interface ItemEventListener { void onItemClick(AppInfo appData, int position); boolean isSelectable(int position); } class ViewHolder extends RecyclerView.ViewHolder { private ImageView iconView; private TextView nameView; private ImageView appCheckView; private LabelView labelView; ViewHolder(View itemView) { super(itemView); if (itemView != mFooterView) { iconView = (ImageView) itemView.findViewById(R.id.item_app_icon); nameView = (TextView) itemView.findViewById(R.id.item_app_name); appCheckView = (ImageView) itemView.findViewById(R.id.item_app_checked); labelView = (LabelView) itemView.findViewById(R.id.item_app_clone_count); } } } } ================================================ FILE: app/src/main/java/io/virtualapp/home/adapters/LaunchpadAdapter.java ================================================ package io.virtualapp.home.adapters; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.List; import io.virtualapp.R; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.MultiplePackageAppData; import io.virtualapp.widgets.LabelView; import io.virtualapp.widgets.LauncherIconView; /** * @author Lody */ public class LaunchpadAdapter extends RecyclerView.Adapter { private LayoutInflater mInflater; private List mList; private SparseIntArray mColorArray = new SparseIntArray(); private OnAppClickListener mAppClickListener; public LaunchpadAdapter(Context context) { mInflater = LayoutInflater.from(context); } public void add(AppData model) { int insertPos = mList.size() - 1; mList.add(insertPos, model); notifyItemInserted(insertPos); } public void replace(int index, AppData data) { mList.set(index, data); notifyItemChanged(index); } public void remove(AppData data) { if (mList.remove(data)) { notifyDataSetChanged(); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new ViewHolder(mInflater.inflate(R.layout.item_launcher_app, null)); } @Override public void onBindViewHolder(ViewHolder holder, int position) { AppData data = mList.get(position); holder.color = getColor(position); holder.iconView.setImageDrawable(data.getIcon()); holder.nameView.setText(data.getName()); if (data.isFirstOpen() && !data.isLoading()) { holder.firstOpenDot.setVisibility(View.VISIBLE); } else { holder.firstOpenDot.setVisibility(View.INVISIBLE); } holder.itemView.setBackgroundColor(holder.color); holder.itemView.setOnClickListener(v -> { if (mAppClickListener != null) { mAppClickListener.onAppClick(position, data); } }); if (data instanceof MultiplePackageAppData) { MultiplePackageAppData multipleData = (MultiplePackageAppData) data; holder.spaceLabelView.setVisibility(View.VISIBLE); holder.spaceLabelView.setText(multipleData.userId + 1 + ""); } else { holder.spaceLabelView.setVisibility(View.INVISIBLE); } if (data.isLoading()) { startLoadingAnimation(holder.iconView); } else { holder.iconView.setProgress(100, false); } } private void startLoadingAnimation(LauncherIconView iconView) { iconView.setProgress(40, true); VUiKit.defer().when(() -> { try { Thread.sleep(900L); } catch (InterruptedException e) { e.printStackTrace(); } }).done((res) -> iconView.setProgress(80, true)); } private int getColor(int position) { int color = mColorArray.get(position); if (color == 0) { int type = position % 3; int row = position / 3; int rowType = row % 3; if (rowType == 0) { if (type == 0) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorA); } else if (type == 1) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorB); } else { color = mInflater.getContext().getResources().getColor(R.color.desktopColorC); } } else if (rowType == 1) { if (type == 0) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorB); } else if (type == 1) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorC); } else { color = mInflater.getContext().getResources().getColor(R.color.desktopColorA); } } else { if (type == 0) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorC); } else if (type == 1) { color = mInflater.getContext().getResources().getColor(R.color.desktopColorA); } else { color = mInflater.getContext().getResources().getColor(R.color.desktopColorB); } } mColorArray.put(position, color); } return color; } @Override public int getItemCount() { return mList == null ? 0 : mList.size(); } public List getList() { return mList; } public void setList(List list) { this.mList = list; notifyDataSetChanged(); } public void setAppClickListener(OnAppClickListener mAppClickListener) { this.mAppClickListener = mAppClickListener; } public void moveItem(int pos, int targetPos) { AppData model = mList.remove(pos); mList.add(targetPos, model); notifyItemMoved(pos, targetPos); } public void refresh(AppData model) { int index = mList.indexOf(model); if (index >= 0) { notifyItemChanged(index); } } public interface OnAppClickListener { void onAppClick(int position, AppData model); } public class ViewHolder extends RecyclerView.ViewHolder { public int color; LauncherIconView iconView; TextView nameView; LabelView spaceLabelView; View firstOpenDot; ViewHolder(View itemView) { super(itemView); iconView = (LauncherIconView) itemView.findViewById(R.id.item_app_icon); nameView = (TextView) itemView.findViewById(R.id.item_app_name); spaceLabelView = (LabelView) itemView.findViewById(R.id.item_app_space_idx); firstOpenDot = itemView.findViewById(R.id.item_first_open_dot); } } } ================================================ FILE: app/src/main/java/io/virtualapp/home/adapters/decorations/ItemOffsetDecoration.java ================================================ package io.virtualapp.home.adapters.decorations; import android.content.Context; import android.graphics.Rect; import android.support.annotation.DimenRes; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.View; public class ItemOffsetDecoration extends RecyclerView.ItemDecoration { private int mItemOffset; public ItemOffsetDecoration(int itemOffset) { mItemOffset = itemOffset; } public ItemOffsetDecoration(@NonNull Context context, @DimenRes int itemOffsetId) { this(context.getResources().getDimensionPixelSize(itemOffsetId)); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/ads/AdScheduler.java ================================================ package io.virtualapp.home.ads; /** * @author Lody */ public class AdScheduler { private long adDeltaTime; private long lastShowAdTime; public AdScheduler(long adDeltaTime) { this.adDeltaTime = adDeltaTime; } public void adShowed() { lastShowAdTime = System.currentTimeMillis(); } public boolean shouldShowAd() { return System.currentTimeMillis() - lastShowAdTime >= adDeltaTime; } } ================================================ FILE: app/src/main/java/io/virtualapp/home/models/AddAppButton.java ================================================ package io.virtualapp.home.models; import android.content.Context; import android.graphics.drawable.Drawable; import io.virtualapp.R; /** * @author Lody */ public class AddAppButton implements AppData { private String name; private Drawable icon; public AddAppButton(Context context) { name = context.getResources().getString(R.string.add_app); icon = context.getResources().getDrawable(R.drawable.ic_add_circle); } @Override public boolean isLoading() { return false; } @Override public boolean isFirstOpen() { return false; } @Override public Drawable getIcon() { return icon; } @Override public String getName() { return name; } @Override public boolean canReorder() { return false; } @Override public boolean canLaunch() { return false; } @Override public boolean canDelete() { return false; } @Override public boolean canCreateShortcut() { return false; } } ================================================ FILE: app/src/main/java/io/virtualapp/home/models/AppData.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; /** * @author Lody */ public interface AppData { boolean isLoading(); boolean isFirstOpen(); Drawable getIcon(); String getName(); boolean canReorder(); boolean canLaunch(); boolean canDelete(); boolean canCreateShortcut(); } ================================================ FILE: app/src/main/java/io/virtualapp/home/models/AppInfo.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; /** * @author Lody */ public class AppInfo { public String packageName; public String path; public boolean fastOpen; public Drawable icon; public CharSequence name; public int cloneCount; } ================================================ FILE: app/src/main/java/io/virtualapp/home/models/AppInfoLite.java ================================================ package io.virtualapp.home.models; import android.os.Parcel; import android.os.Parcelable; /** * @author Lody */ public class AppInfoLite implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public AppInfoLite createFromParcel(Parcel source) { return new AppInfoLite(source); } @Override public AppInfoLite[] newArray(int size) { return new AppInfoLite[size]; } }; public String packageName; public String path; public boolean fastOpen; public AppInfoLite(String packageName, String path, boolean fastOpen) { this.packageName = packageName; this.path = path; this.fastOpen = fastOpen; } protected AppInfoLite(Parcel in) { this.packageName = in.readString(); this.path = in.readString(); this.fastOpen = in.readByte() != 0; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.packageName); dest.writeString(this.path); dest.writeByte(this.fastOpen ? (byte) 1 : (byte) 0); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/models/EmptyAppData.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; /** * @author Lody */ public class EmptyAppData implements AppData { @Override public boolean isLoading() { return false; } @Override public boolean isFirstOpen() { return false; } @Override public Drawable getIcon() { return null; } @Override public String getName() { return null; } @Override public boolean canReorder() { return false; } @Override public boolean canLaunch() { return false; } @Override public boolean canDelete() { return false; } @Override public boolean canCreateShortcut() { return false; } } ================================================ FILE: app/src/main/java/io/virtualapp/home/models/MultiplePackageAppData.java ================================================ package io.virtualapp.home.models; import android.graphics.drawable.Drawable; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstalledAppInfo; /** * @author Lody */ public class MultiplePackageAppData implements AppData { public InstalledAppInfo appInfo; public int userId; public boolean isFirstOpen; public boolean isLoading; public Drawable icon; public String name; public MultiplePackageAppData(PackageAppData target, int userId) { this.userId = userId; this.appInfo = VirtualCore.get().getInstalledAppInfo(target.packageName, 0); this.isFirstOpen = !appInfo.isLaunched(userId); if (target.icon != null) { Drawable.ConstantState state = target.icon.getConstantState(); if (state != null) { icon = state.newDrawable(); } } name = target.name; } @Override public boolean isLoading() { return isLoading; } @Override public boolean isFirstOpen() { return isFirstOpen; } @Override public Drawable getIcon() { return icon; } @Override public String getName() { return name; } @Override public boolean canReorder() { return true; } @Override public boolean canLaunch() { return true; } @Override public boolean canDelete() { return true; } @Override public boolean canCreateShortcut() { return true; } } ================================================ FILE: app/src/main/java/io/virtualapp/home/models/PackageAppData.java ================================================ package io.virtualapp.home.models; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import com.lody.virtual.remote.InstalledAppInfo; /** * @author Lody */ public class PackageAppData implements AppData { public String packageName; public String name; public Drawable icon; public boolean fastOpen; public boolean isFirstOpen; public boolean isLoading; public PackageAppData(Context context, InstalledAppInfo installedAppInfo) { this.packageName = installedAppInfo.packageName; this.isFirstOpen = !installedAppInfo.isLaunched(0); loadData(context, installedAppInfo.getApplicationInfo(installedAppInfo.getInstalledUsers()[0])); } private void loadData(Context context, ApplicationInfo appInfo) { if (appInfo == null) { return; } PackageManager pm = context.getPackageManager(); try { CharSequence sequence = appInfo.loadLabel(pm); if (sequence != null) { name = sequence.toString(); } icon = appInfo.loadIcon(pm); } catch (Throwable e) { e.printStackTrace(); } } @Override public boolean isLoading() { return isLoading; } @Override public boolean isFirstOpen() { return isFirstOpen; } @Override public Drawable getIcon() { return icon; } @Override public String getName() { return name; } @Override public boolean canReorder() { return true; } @Override public boolean canLaunch() { return true; } @Override public boolean canDelete() { return true; } @Override public boolean canCreateShortcut() { return true; } } ================================================ FILE: app/src/main/java/io/virtualapp/home/platform/PlatformInfo.java ================================================ package io.virtualapp.home.platform; import android.content.pm.PackageInfo; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * @author Lody */ public abstract class PlatformInfo { private final Set platformPkgs = new HashSet<>(); public PlatformInfo(String... pkgs) { Collections.addAll(platformPkgs, pkgs); } public abstract boolean relyOnPlatform(PackageInfo info); } ================================================ FILE: app/src/main/java/io/virtualapp/home/platform/WechatPlatformInfo.java ================================================ package io.virtualapp.home.platform; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; /** * @author Lody */ public class WechatPlatformInfo extends PlatformInfo { public WechatPlatformInfo() { super("com.tencent.mm"); } @Override public boolean relyOnPlatform(PackageInfo info) { if (info.activities == null) { return false; } for (ActivityInfo activityInfo : info.activities) { if (activityInfo.name.endsWith("WXEntryActivity")) { return true; } } return false; } } ================================================ FILE: app/src/main/java/io/virtualapp/home/repo/AppDataSource.java ================================================ package io.virtualapp.home.repo; import android.content.Context; import com.lody.virtual.remote.InstallResult; import org.jdeferred.Promise; import java.io.File; import java.util.List; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfo; import io.virtualapp.home.models.AppInfoLite; /** * @author Lody * @version 1.0 */ public interface AppDataSource { /** * @return All the Applications we Virtual. */ Promise, Throwable, Void> getVirtualApps(); /** * @param context Context * @return All the Applications we Installed. */ Promise, Throwable, Void> getInstalledApps(Context context); Promise, Throwable, Void> getStorageApps(Context context, File rootDir); InstallResult addVirtualApp(AppInfoLite info); boolean removeVirtualApp(String packageName, int userId); } ================================================ FILE: app/src/main/java/io/virtualapp/home/repo/AppRepository.java ================================================ package io.virtualapp.home.repo; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import com.lody.virtual.GmsSupport; import com.lody.virtual.client.core.InstallStrategy; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstallResult; import com.lody.virtual.remote.InstalledAppInfo; import org.jdeferred.Promise; import java.io.File; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.AppData; import io.virtualapp.home.models.AppInfo; import io.virtualapp.home.models.AppInfoLite; import io.virtualapp.home.models.MultiplePackageAppData; import io.virtualapp.home.models.PackageAppData; /** * @author Lody */ public class AppRepository implements AppDataSource { private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); private static final List SCAN_PATH_LIST = Arrays.asList( ".", "wandoujia/app", "tencent/tassistant/apk", "BaiduAsa9103056", "360Download", "pp/downloader", "pp/downloader/apk", "pp/downloader/silent/apk"); private Context mContext; public AppRepository(Context context) { mContext = context; } private static boolean isSystemApplication(PackageInfo packageInfo) { return (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && !GmsSupport.isGmsFamilyPackage(packageInfo.packageName); } @Override public Promise, Throwable, Void> getVirtualApps() { return VUiKit.defer().when(() -> { List infos = VirtualCore.get().getInstalledApps(0); List models = new ArrayList<>(); for (InstalledAppInfo info : infos) { if (!VirtualCore.get().isPackageLaunchable(info.packageName)) { continue; } PackageAppData data = new PackageAppData(mContext, info); if (VirtualCore.get().isAppInstalledAsUser(0, info.packageName)) { models.add(data); } int[] userIds = info.getInstalledUsers(); for (int userId : userIds) { if (userId != 0) { models.add(new MultiplePackageAppData(data, userId)); } } } return models; }); } @Override public Promise, Throwable, Void> getInstalledApps(Context context) { return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, context.getPackageManager().getInstalledPackages(0), true)); } @Override public Promise, Throwable, Void> getStorageApps(Context context, File rootDir) { return VUiKit.defer().when(() -> convertPackageInfoToAppData(context, findAndParseAPKs(context, rootDir, SCAN_PATH_LIST), false)); } private List findAndParseAPKs(Context context, File rootDir, List paths) { List packageList = new ArrayList<>(); if (paths == null) return packageList; for (String path : paths) { File[] dirFiles = new File(rootDir, path).listFiles(); if (dirFiles == null) continue; for (File f : dirFiles) { if (!f.getName().toLowerCase().endsWith(".apk")) continue; PackageInfo pkgInfo = null; try { pkgInfo = context.getPackageManager().getPackageArchiveInfo(f.getAbsolutePath(), 0); pkgInfo.applicationInfo.sourceDir = f.getAbsolutePath(); pkgInfo.applicationInfo.publicSourceDir = f.getAbsolutePath(); } catch (Exception e) { // Ignore } if (pkgInfo != null) packageList.add(pkgInfo); } } return packageList; } private List convertPackageInfoToAppData(Context context, List pkgList, boolean fastOpen) { PackageManager pm = context.getPackageManager(); List list = new ArrayList<>(pkgList.size()); String hostPkg = VirtualCore.get().getHostPkg(); for (PackageInfo pkg : pkgList) { // ignore the host package if (hostPkg.equals(pkg.packageName)) { continue; } // ignore the System package if (isSystemApplication(pkg)) { continue; } ApplicationInfo ai = pkg.applicationInfo; String path = ai.publicSourceDir != null ? ai.publicSourceDir : ai.sourceDir; if (path == null) { continue; } AppInfo info = new AppInfo(); info.packageName = pkg.packageName; info.fastOpen = fastOpen; info.path = path; info.icon = ai.loadIcon(pm); info.name = ai.loadLabel(pm); InstalledAppInfo installedAppInfo = VirtualCore.get().getInstalledAppInfo(pkg.packageName, 0); if (installedAppInfo != null) { info.cloneCount = installedAppInfo.getInstalledUsers().length; } list.add(info); } return list; } @Override public InstallResult addVirtualApp(AppInfoLite info) { int flags = InstallStrategy.COMPARE_VERSION | InstallStrategy.SKIP_DEX_OPT; if (info.fastOpen) { flags |= InstallStrategy.DEPEND_SYSTEM_IF_EXIST; } return VirtualCore.get().installPackage(info.path, flags); } @Override public boolean removeVirtualApp(String packageName, int userId) { return VirtualCore.get().uninstallPackageAsUser(packageName, userId); } } ================================================ FILE: app/src/main/java/io/virtualapp/home/repo/PackageAppDataStorage.java ================================================ package io.virtualapp.home.repo; import com.lody.virtual.client.core.VirtualCore; import com.lody.virtual.remote.InstalledAppInfo; import java.util.HashMap; import java.util.Map; import io.virtualapp.VApp; import io.virtualapp.abs.Callback; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.models.PackageAppData; /** * @author Lody *

* Cache the loaded PackageAppData. */ public class PackageAppDataStorage { private static final PackageAppDataStorage STORAGE = new PackageAppDataStorage(); private final Map packageDataMap = new HashMap<>(); public static PackageAppDataStorage get() { return STORAGE; } public PackageAppData acquire(String packageName) { PackageAppData data; synchronized (packageDataMap) { data = packageDataMap.get(packageName); if (data == null) { data = loadAppData(packageName); } } return data; } public void acquire(String packageName, Callback callback) { VUiKit.defer() .when(() -> acquire(packageName)) .done(callback::callback); } private PackageAppData loadAppData(String packageName) { InstalledAppInfo setting = VirtualCore.get().getInstalledAppInfo(packageName, 0); if (setting != null) { PackageAppData data = new PackageAppData(VApp.getApp(), setting); synchronized (packageDataMap) { packageDataMap.put(packageName, data); } return data; } return null; } } ================================================ FILE: app/src/main/java/io/virtualapp/splash/SplashActivity.java ================================================ package io.virtualapp.splash; import android.os.Bundle; import android.view.WindowManager; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.AdView; import com.lody.virtual.client.core.VirtualCore; import io.virtualapp.R; import io.virtualapp.VCommends; import io.virtualapp.abs.ui.VActivity; import io.virtualapp.abs.ui.VUiKit; import io.virtualapp.home.FlurryROMCollector; import io.virtualapp.home.HomeActivity; import jonathanfinerty.once.Once; public class SplashActivity extends VActivity { private AdView mAdView; @Override protected void onCreate(Bundle savedInstanceState) { @SuppressWarnings("unused") boolean enterGuide = !Once.beenDone(Once.THIS_APP_INSTALL, VCommends.TAG_NEW_VERSION); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); showBanner(); VUiKit.defer().when(() -> { if (!Once.beenDone("collect_flurry")) { FlurryROMCollector.startCollect(); Once.markDone("collect_flurry"); } long time = System.currentTimeMillis(); doActionInThread(); time = System.currentTimeMillis() - time; long delta = 3000L - time; if (delta > 0) { VUiKit.sleep(delta); } }).done((res) -> { HomeActivity.goHome(this); finish(); }); } private void showBanner() { mAdView = (AdView) findViewById(R.id.splash_banner); AdRequest adRequest = new AdRequest.Builder().build(); mAdView.loadAd(adRequest); } private void doActionInThread() { if (!VirtualCore.get().isEngineLaunched()) { VirtualCore.get().waitForEngine(); } } } ================================================ FILE: app/src/main/java/io/virtualapp/vs/VSManagerActivity.java ================================================ package io.virtualapp.vs; import io.virtualapp.abs.ui.VActivity; /** * @author Lody * * * */ public class VSManagerActivity extends VActivity { } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/BallGridBeatIndicator.java ================================================ package io.virtualapp.widgets; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Paint; import java.util.ArrayList; public class BallGridBeatIndicator extends Indicator { private static final int ALPHA = 255; private static final int[] ALPHAS = new int[]{ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA}; @Override public void draw(Canvas canvas, Paint paint) { float circleSpacing = 4; float radius = (getWidth() - circleSpacing * 4) / 6; float x = getWidth() / 2 - (radius * 2 + circleSpacing); float y = getWidth() / 2 - (radius * 2 + circleSpacing); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { canvas.save(); float translateX = x + (radius * 2) * j + circleSpacing * j; float translateY = y + (radius * 2) * i + circleSpacing * i; canvas.translate(translateX, translateY); paint.setAlpha(ALPHAS[3 * i + j]); canvas.drawCircle(0, 0, radius, paint); canvas.restore(); } } } @Override public ArrayList onCreateAnimators() { ArrayList animators = new ArrayList<>(); int[] durations = {960, 930, 1190, 1130, 1340, 940, 1200, 820, 1190}; int[] delays = {360, 400, 680, 410, 710, -150, -120, 10, 320}; for (int i = 0; i < 9; i++) { final int index = i; ValueAnimator alphaAnim = ValueAnimator.ofInt(255, 168, 255); alphaAnim.setDuration(durations[i]); alphaAnim.setRepeatCount(-1); alphaAnim.setStartDelay(delays[i]); addUpdateListener(alphaAnim, animation -> { ALPHAS[index] = (int) animation.getAnimatedValue(); postInvalidate(); }); animators.add(alphaAnim); } return animators; } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/BallPulseIndicator.java ================================================ package io.virtualapp.widgets; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Paint; import java.util.ArrayList; public class BallPulseIndicator extends Indicator { public static final float SCALE = 1.0f; //scale x ,y private float[] scaleFloats = new float[]{SCALE, SCALE, SCALE}; @Override public void draw(Canvas canvas, Paint paint) { float circleSpacing = 4; float radius = (Math.min(getWidth(), getHeight()) - circleSpacing * 2) / 6; float x = getWidth() / 2 - (radius * 2 + circleSpacing); float y = getHeight() / 2; for (int i = 0; i < 3; i++) { canvas.save(); float translateX = x + (radius * 2) * i + circleSpacing * i; canvas.translate(translateX, y); canvas.scale(scaleFloats[i], scaleFloats[i]); canvas.drawCircle(0, 0, radius, paint); canvas.restore(); } } @Override public ArrayList onCreateAnimators() { ArrayList animators = new ArrayList<>(); int[] delays = new int[]{120, 240, 360}; for (int i = 0; i < 3; i++) { final int index = i; ValueAnimator scaleAnim = ValueAnimator.ofFloat(1, 0.3f, 1); scaleAnim.setDuration(750); scaleAnim.setRepeatCount(-1); scaleAnim.setStartDelay(delays[i]); addUpdateListener(scaleAnim, animation -> { scaleFloats[index] = (float) animation.getAnimatedValue(); postInvalidate(); }); animators.add(scaleAnim); } return animators; } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/BaseView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.animation.LinearInterpolator; public abstract class BaseView extends View { public ValueAnimator valueAnimator; public BaseView(Context context) { this(context, null); } public BaseView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BaseView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); InitPaint(); } public void startAnim() { stopAnim(); startViewAnim(0f, 1f, 500); } public void startAnim(int time) { stopAnim(); startViewAnim(0f, 1f, time); } public void stopAnim() { if (valueAnimator != null) { clearAnimation(); valueAnimator.setRepeatCount(0); valueAnimator.cancel(); valueAnimator.end(); if (OnStopAnim() == 0) { valueAnimator.setRepeatCount(0); valueAnimator.cancel(); valueAnimator.end(); } } } private ValueAnimator startViewAnim(float startF, final float endF, long time) { valueAnimator = ValueAnimator.ofFloat(startF, endF); valueAnimator.setDuration(time); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.setRepeatCount(SetAnimRepeatCount()); if (ValueAnimator.RESTART == SetAnimRepeatMode()) { valueAnimator.setRepeatMode(ValueAnimator.RESTART); } else if (ValueAnimator.REVERSE == SetAnimRepeatMode()) { valueAnimator.setRepeatMode(ValueAnimator.REVERSE); } valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { OnAnimationUpdate(valueAnimator); } }); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); } @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); } @Override public void onAnimationRepeat(Animator animation) { super.onAnimationRepeat(animation); OnAnimationRepeat(animation); } }); if (!valueAnimator.isRunning()) { AnimIsRunning(); valueAnimator.start(); } return valueAnimator; } protected abstract void InitPaint(); protected abstract void OnAnimationUpdate(ValueAnimator valueAnimator); protected abstract void OnAnimationRepeat(Animator animation); protected abstract int OnStopAnim(); protected abstract int SetAnimRepeatMode(); protected abstract int SetAnimRepeatCount(); protected abstract void AnimIsRunning(); public float getFontlength(Paint paint, String str) { Rect rect = new Rect(); paint.getTextBounds(str, 0, str.length(), rect); return rect.width(); } public float getFontHeight(Paint paint, String str) { Rect rect = new Rect(); paint.getTextBounds(str, 0, str.length(), rect); return rect.height(); } public float getFontHeight(Paint paint) { Paint.FontMetrics fm = paint.getFontMetrics(); return fm.descent - fm.ascent; } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/CardStackAdapter.java ================================================ package io.virtualapp.widgets; import java.util.ArrayList; import java.util.List; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import io.virtualapp.R; /** * This class acts as an adapter for the {@link CardStackLayout} view. This * adapter is intentionally made an abstract class with following abstract * methods - *

*

* {@link #getCount()} - Decides the number of views present in the view *

* {@link #createView(int, ViewGroup)} - Creates the view for all positions in * range [0, {@link #getCount()}) *

* Contains the logic for touch events in {@link #onTouch(View, MotionEvent)} */ public abstract class CardStackAdapter implements View.OnTouchListener, View.OnClickListener { public static final int ANIM_DURATION = 600; public static final int DECELERATION_FACTOR = 2; public static final int INVALID_CARD_POSITION = -1; private final int mScreenHeight; private final int dp30; // Settings for the adapter from layout private float mCardGapBottom; private float mCardGap; private int mParallaxScale; private boolean mParallaxEnabled; private boolean mShowInitAnimation; private int fullCardHeight; private View[] mCardViews; private float dp8; private CardStackLayout mParent; private boolean mScreenTouchable = false; private float mTouchFirstY = -1; private float mTouchPrevY = -1; private float mTouchDistance = 0; private int mSelectedCardPosition = INVALID_CARD_POSITION; private float scaleFactorForElasticEffect; private int mParentPaddingTop = 0; private int mCardPaddingInternal = 0; public CardStackAdapter(Context context) { Resources resources = context.getResources(); DisplayMetrics dm = Resources.getSystem().getDisplayMetrics(); mScreenHeight = dm.heightPixels; dp30 = (int) resources.getDimension(R.dimen.dp30); scaleFactorForElasticEffect = (int) resources.getDimension(R.dimen.dp8); dp8 = (int) resources.getDimension(R.dimen.dp8); } protected float getCardGapBottom() { return mCardGapBottom; } /** * Defines and initializes the view to be shown in the * {@link CardStackLayout} Provides two parameters to the sub-class namely - * * @param position * @param container * @return View corresponding to the position and parent container */ public abstract View createView(int position, ViewGroup container); /** * Defines the number of cards that are present in the * {@link CardStackLayout} * * @return cardCount - Number of views in the related * {@link CardStackLayout} */ public abstract int getCount(); /** * Returns true if no animation is in progress currently. Can be used to * disable any events if they are not allowed during an animation. Returns * false if an animation is in progress. * * @return - true if animation in progress, false otherwise */ public boolean isScreenTouchable() { return mScreenTouchable; } private void setScreenTouchable(boolean screenTouchable) { this.mScreenTouchable = screenTouchable; } void addView(final int position) { View root = createView(position, mParent); root.setOnTouchListener(this); root.setTag(R.id.cardstack_internal_position_tag, position); root.setLayerType(View.LAYER_TYPE_HARDWARE, null); mCardPaddingInternal = root.getPaddingTop(); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, fullCardHeight); root.setLayoutParams(lp); if (mShowInitAnimation) { root.setY(getCardFinalY(position)); setScreenTouchable(false); } else { root.setY(getCardOriginalY(position) - mParentPaddingTop); setScreenTouchable(true); } mCardViews[position] = root; mParent.addView(root); } protected float getCardFinalY(int position) { return mScreenHeight - dp30 - ((getCount() - position) * mCardGapBottom) - mCardPaddingInternal; } protected float getCardOriginalY(int position) { return mParentPaddingTop + mCardGap * position; } /** * Resets all cards in {@link CardStackLayout} to their initial positions * * @param r * Execute r.run() once the reset animation is done */ public void resetCards(Runnable r) { List animations = new ArrayList<>(getCount()); for (int i = 0; i < getCount(); i++) { final View child = mCardViews[i]; animations.add(ObjectAnimator.ofFloat(child, View.Y, (int) child.getY(), getCardOriginalY(i))); } startAnimations(animations, r, true); } /** * Plays together all animations passed in as parameter. Once animation is * completed, r.run() is executed. If parameter isReset is set to true, * {@link #mSelectedCardPosition} is set to {@link #INVALID_CARD_POSITION} * * @param animations * @param r * @param isReset */ private void startAnimations(List animations, final Runnable r, final boolean isReset) { AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether(animations); animatorSet.setDuration(ANIM_DURATION); animatorSet.setInterpolator(new DecelerateInterpolator(DECELERATION_FACTOR)); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (r != null) r.run(); setScreenTouchable(true); if (isReset) mSelectedCardPosition = INVALID_CARD_POSITION; } }); animatorSet.start(); } @Override public boolean onTouch(View v, MotionEvent event) { if (!isScreenTouchable()) { return false; } float y = event.getRawY(); int positionOfCardToMove = (int) v.getTag(R.id.cardstack_internal_position_tag); switch (event.getAction()) { case MotionEvent.ACTION_DOWN : if (mTouchFirstY != -1) { return false; } mTouchPrevY = mTouchFirstY = y; mTouchDistance = 0; break; case MotionEvent.ACTION_MOVE : if (mSelectedCardPosition == INVALID_CARD_POSITION) moveCards(positionOfCardToMove, y - mTouchFirstY); mTouchDistance += Math.abs(y - mTouchPrevY); break; case MotionEvent.ACTION_CANCEL : case MotionEvent.ACTION_UP : if (mTouchDistance < dp8 && Math.abs(y - mTouchFirstY) < dp8 && mSelectedCardPosition == INVALID_CARD_POSITION) { onClick(v); } else { resetCards(); } mTouchPrevY = mTouchFirstY = -1; mTouchDistance = 0; return false; } return true; } @Override public void onClick(final View v) { if (!isScreenTouchable()) { return; } setScreenTouchable(false); if (mSelectedCardPosition == INVALID_CARD_POSITION) { mSelectedCardPosition = (int) v.getTag(R.id.cardstack_internal_position_tag); List animations = new ArrayList<>(getCount()); for (int i = 0; i < getCount(); i++) { View child = mCardViews[i]; animations.add(getAnimatorForView(child, i, mSelectedCardPosition)); } startAnimations(animations, () -> { setScreenTouchable(true); if (mParent.getOnCardSelectedListener() != null) { mParent.getOnCardSelectedListener().onCardSelected(v, mSelectedCardPosition); } }, false); } } /** * This method can be overridden to have different animations for each card * when a click event happens on any card view. This method will be called * for every * * @param view * The view for which this method needs to return an animator * @param selectedCardPosition * Position of the card that was clicked * @param currentCardPosition * Position of the current card * @return animator which has to be applied on the current card */ protected Animator getAnimatorForView(View view, int currentCardPosition, int selectedCardPosition) { if (currentCardPosition != selectedCardPosition) { return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(), getCardFinalY(currentCardPosition)); } else { return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(), getCardOriginalY(0) + (currentCardPosition * mCardGapBottom)); } } private void moveCards(int positionOfCardToMove, float diff) { if (diff < 0 || positionOfCardToMove < 0 || positionOfCardToMove >= getCount()) return; for (int i = positionOfCardToMove; i < getCount(); i++) { final View child = mCardViews[i]; float diffCard = diff / scaleFactorForElasticEffect; if (mParallaxEnabled) { if (mParallaxScale > 0) { diffCard = diffCard * (mParallaxScale / 3) * (getCount() + 1 - i); } else { int scale = mParallaxScale * -1; diffCard = diffCard * (i * (scale / 3) + 1); } } else diffCard = diffCard * (getCount() * 2 + 1); child.setY(getCardOriginalY(i) + diffCard); } } /** * Provides an API to {@link CardStackLayout} to set the parameters provided * to it in its XML * * @param cardStackLayout * Parent of all cards */ void setAdapterParams(CardStackLayout cardStackLayout) { mParent = cardStackLayout; mCardViews = new View[getCount()]; mCardGapBottom = cardStackLayout.getCardGapBottom(); mCardGap = cardStackLayout.getCardGap(); mParallaxScale = cardStackLayout.getParallaxScale(); mParallaxEnabled = cardStackLayout.isParallaxEnabled(); if (mParallaxEnabled && mParallaxScale == 0) mParallaxEnabled = false; mShowInitAnimation = cardStackLayout.isShowInitAnimation(); mParentPaddingTop = cardStackLayout.getPaddingTop(); fullCardHeight = (int) (mScreenHeight - dp30 - dp8 - getCount() * mCardGapBottom); } /** * Resets all cards in {@link CardStackLayout} to their initial positions */ public void resetCards() { resetCards(null); } /** * Returns false if all the cards are in their initial position i.e. no card * is selected *

* Returns true if the {@link CardStackLayout} has a card selected and all * other cards are at the bottom of the screen. * * @return true if any card is selected, false otherwise */ public boolean isCardSelected() { return mSelectedCardPosition != INVALID_CARD_POSITION; } /** * Returns the position of selected card. If no card is selected, returns * {@link #INVALID_CARD_POSITION} */ public int getSelectedCardPosition() { return mSelectedCardPosition; } /** * Since there is no view recycling in {@link CardStackLayout}, we maintain * an instance of every view that is set for every position. This method * returns a view at the requested position. * * @param position * Position of card in {@link CardStackLayout} * @return View at requested position */ public View getCardView(int position) { if (mCardViews == null) return null; return mCardViews[position]; } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/CardStackLayout.java ================================================ package io.virtualapp.widgets; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import io.virtualapp.R; /** * Displays a list of cards as a stack on the screen. *

* XML attributes *

* See {@link R.styleable#CardStackLayout CardStackLayout Attributes} *

* {@link R.styleable#CardStackLayout_showInitAnimation} * {@link R.styleable#CardStackLayout_card_gap} * {@link R.styleable#CardStackLayout_card_gap_bottom} * {@link R.styleable#CardStackLayout_parallax_enabled} * {@link R.styleable#CardStackLayout_parallax_scale} */ public class CardStackLayout extends FrameLayout { public static final boolean PARALLAX_ENABLED_DEFAULT = false; public static final boolean SHOW_INIT_ANIMATION_DEFAULT = true; private float mCardGapBottom; private float mCardGap; private boolean mShowInitAnimation; private boolean mParallaxEnabled; private int mParallaxScale; private OnCardSelected mOnCardSelectedListener = null; private CardStackAdapter mAdapter = null; public CardStackLayout(Context context) { super(context); resetDefaults(); } public CardStackLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CardStackLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); handleArgs(context, attrs, defStyleAttr, 0); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public CardStackLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); handleArgs(context, attrs, defStyleAttr, defStyleRes); } /** * package restricted */ OnCardSelected getOnCardSelectedListener() { return mOnCardSelectedListener; } /** * Listen on card selection events for {@link CardStackLayout}. Sends * clicked view and it's corresponding position in the callback. * * @param onCardSelectedListener * listener */ public void setOnCardSelectedListener(OnCardSelected onCardSelectedListener) { this.mOnCardSelectedListener = onCardSelectedListener; } private void resetDefaults() { mOnCardSelectedListener = null; } private void handleArgs(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { resetDefaults(); final TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CardStackLayout, defStyleAttr, defStyleRes); mParallaxEnabled = a.getBoolean(R.styleable.CardStackLayout_parallax_enabled, PARALLAX_ENABLED_DEFAULT); mShowInitAnimation = a.getBoolean(R.styleable.CardStackLayout_showInitAnimation, SHOW_INIT_ANIMATION_DEFAULT); mParallaxScale = a.getInteger(R.styleable.CardStackLayout_parallax_scale, getResources().getInteger(R.integer.parallax_scale_default)); mCardGap = a.getDimension(R.styleable.CardStackLayout_card_gap, getResources().getDimension(R.dimen.card_gap)); mCardGapBottom = a.getDimension(R.styleable.CardStackLayout_card_gap_bottom, getResources().getDimension(R.dimen.card_gap_bottom)); a.recycle(); } /** * @return adapter of type {@link CardStackAdapter} that is set for this * view. */ public CardStackAdapter getAdapter() { return mAdapter; } /** * Set the adapter for this {@link CardStackLayout} * * @param adapter * Should extend {@link CardStackAdapter} */ public void setAdapter(CardStackAdapter adapter) { this.mAdapter = adapter; mAdapter.setAdapterParams(this); for (int i = 0; i < mAdapter.getCount(); i++) { mAdapter.addView(i); } if (mShowInitAnimation) { postDelayed(this::restoreCards, 500); } } /** * @return currently set parallax scale value. */ public int getParallaxScale() { return mParallaxScale; } /** * Sets the value of parallax scale. Parallax scale is the factor which * decides how much distance a card will scroll when the user drags it down. */ public void setParallaxScale(int mParallaxScale) { this.mParallaxScale = mParallaxScale; } public boolean isParallaxEnabled() { return mParallaxEnabled; } public void setParallaxEnabled(boolean mParallaxEnabled) { this.mParallaxEnabled = mParallaxEnabled; } public boolean isShowInitAnimation() { return mShowInitAnimation; } public void setShowInitAnimation(boolean mShowInitAnimation) { this.mShowInitAnimation = mShowInitAnimation; } /** * @return the gap (in pixels) between two consecutive cards */ public float getCardGap() { return mCardGap; } /** * Set the gap (in pixels) between two consecutive cards */ public void setCardGap(float mCardGap) { this.mCardGap = mCardGap; } /** * @return gap between the two consecutive cards when collapsed to the * bottom of the screen */ public float getCardGapBottom() { return mCardGapBottom; } public void setCardGapBottom(float mCardGapBottom) { this.mCardGapBottom = mCardGapBottom; } /** * @return true if a card is selected, false otherwise */ public boolean isCardSelected() { return mAdapter.isCardSelected(); } /** * Removes the adapter that was previously set using * {@link #setAdapter(CardStackAdapter)} */ public void removeAdapter() { if (getChildCount() > 0) removeAllViews(); mAdapter = null; mOnCardSelectedListener = null; } /** * Animates the cards to their initial position in the layout. */ public void restoreCards() { mAdapter.resetCards(); } /** * Intimates the implementing class about the selection of a card */ public interface OnCardSelected { void onCardSelected(View v, int position); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/CircularAnim.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; import android.app.Activity; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.widget.ImageView; public class CircularAnim { public static final long PERFECT_MILLS = 618; public static final int MINI_RADIUS = 0; private static Long sPerfectMills; private static Long sFullActivityPerfectMills; private static Integer sColorOrImageRes; private static long getPerfectMills() { if (sPerfectMills != null) return sPerfectMills; else return PERFECT_MILLS; } private static long getFullActivityMills() { if (sFullActivityPerfectMills != null) return sFullActivityPerfectMills; else return PERFECT_MILLS; } private static int getColorOrImageRes() { if (sColorOrImageRes != null) return sColorOrImageRes; else return android.R.color.white; } public static VisibleBuilder show(View animView) { return new VisibleBuilder(animView, true); } public static VisibleBuilder hide(View animView) { return new VisibleBuilder(animView, false); } public static FullActivityBuilder fullActivity(Activity activity, View triggerView) { return new FullActivityBuilder(activity, triggerView); } public static void init(long perfectMills, long fullActivityPerfectMills, int colorOrImageRes) { sPerfectMills = perfectMills; sFullActivityPerfectMills = fullActivityPerfectMills; sColorOrImageRes = colorOrImageRes; } public interface OnAnimationEndListener { void onAnimationEnd(); } @SuppressLint("NewApi") public static class VisibleBuilder { private View mAnimView, mTriggerView; private Float mStartRadius, mEndRadius; private long mDurationMills = getPerfectMills(); private boolean isShow; private OnAnimationEndListener mOnAnimationEndListener; public VisibleBuilder(View animView, boolean isShow) { mAnimView = animView; this.isShow = isShow; if (isShow) { mStartRadius = MINI_RADIUS + 0F; } else { mEndRadius = MINI_RADIUS + 0F; } } public VisibleBuilder triggerView(View triggerView) { mTriggerView = triggerView; return this; } public VisibleBuilder startRadius(float startRadius) { mStartRadius = startRadius; return this; } public VisibleBuilder endRadius(float endRadius) { mEndRadius = endRadius; return this; } public VisibleBuilder duration(long durationMills) { mDurationMills = durationMills; return this; } @Deprecated //You can use method - go(OnAnimationEndListener onAnimationEndListener). public VisibleBuilder onAnimationEndListener(OnAnimationEndListener onAnimationEndListener) { mOnAnimationEndListener = onAnimationEndListener; return this; } public void go() { go(null); } public void go(OnAnimationEndListener onAnimationEndListener) { mOnAnimationEndListener = onAnimationEndListener; // 版本判断 if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { doOnEnd(); return; } int rippleCX, rippleCY, maxRadius; if (mTriggerView != null) { int[] tvLocation = new int[2]; mTriggerView.getLocationInWindow(tvLocation); final int tvCX = tvLocation[0] + mTriggerView.getWidth() / 2; final int tvCY = tvLocation[1] + mTriggerView.getHeight() / 2; int[] avLocation = new int[2]; mAnimView.getLocationInWindow(avLocation); final int avLX = avLocation[0]; final int avTY = avLocation[1]; int triggerX = Math.max(avLX, tvCX); triggerX = Math.min(triggerX, avLX + mAnimView.getWidth()); int triggerY = Math.max(avTY, tvCY); triggerY = Math.min(triggerY, avTY + mAnimView.getHeight()); // 以上全为绝对坐标 int avW = mAnimView.getWidth(); int avH = mAnimView.getHeight(); rippleCX = triggerX - avLX; rippleCY = triggerY - avTY; // 计算水波中心点至 @mAnimView 边界的最大距离 int maxW = Math.max(rippleCX, avW - rippleCX); int maxH = Math.max(rippleCY, avH - rippleCY); maxRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1; } else { rippleCX = (mAnimView.getLeft() + mAnimView.getRight()) / 2; rippleCY = (mAnimView.getTop() + mAnimView.getBottom()) / 2; int w = mAnimView.getWidth(); int h = mAnimView.getHeight(); // 勾股定理 & 进一法 maxRadius = (int) Math.sqrt(w * w + h * h) + 1; } if (isShow && mEndRadius == null) mEndRadius = maxRadius + 0F; else if (!isShow && mStartRadius == null) mStartRadius = maxRadius + 0F; try { Animator anim = ViewAnimationUtils.createCircularReveal( mAnimView, rippleCX, rippleCY, mStartRadius, mEndRadius); mAnimView.setVisibility(View.VISIBLE); anim.setDuration(mDurationMills); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); doOnEnd(); } }); anim.start(); } catch (Exception e) { e.printStackTrace(); doOnEnd(); } } private void doOnEnd() { if (isShow) mAnimView.setVisibility(View.VISIBLE); else mAnimView.setVisibility(View.INVISIBLE); if (mOnAnimationEndListener != null) mOnAnimationEndListener.onAnimationEnd(); } } @SuppressLint("NewApi") public static class FullActivityBuilder { private Activity mActivity; private View mTriggerView; private float mStartRadius = MINI_RADIUS; private int mColorOrImageRes = getColorOrImageRes(); private Long mDurationMills; private OnAnimationEndListener mOnAnimationEndListener; private int mEnterAnim = android.R.anim.fade_in, mExitAnim = android.R.anim.fade_out; public FullActivityBuilder(Activity activity, View triggerView) { mActivity = activity; mTriggerView = triggerView; } public FullActivityBuilder startRadius(float startRadius) { mStartRadius = startRadius; return this; } public FullActivityBuilder colorOrImageRes(int colorOrImageRes) { mColorOrImageRes = colorOrImageRes; return this; } public FullActivityBuilder duration(long durationMills) { mDurationMills = durationMills; return this; } public FullActivityBuilder overridePendingTransition(int enterAnim, int exitAnim) { mEnterAnim = enterAnim; mExitAnim = exitAnim; return this; } public void go(OnAnimationEndListener onAnimationEndListener) { mOnAnimationEndListener = onAnimationEndListener; // 版本判断,小于5.0则无动画. if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { doOnEnd(); return; } int[] location = new int[2]; mTriggerView.getLocationInWindow(location); final int cx = location[0] + mTriggerView.getWidth() / 2; final int cy = location[1] + mTriggerView.getHeight() / 2; final ImageView view = new ImageView(mActivity); view.setScaleType(ImageView.ScaleType.CENTER_CROP); view.setImageResource(mColorOrImageRes); final ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView(); int w = decorView.getWidth(); int h = decorView.getHeight(); decorView.addView(view, w, h); int maxW = Math.max(cx, w - cx); int maxH = Math.max(cy, h - cy); final int finalRadius = (int) Math.sqrt(maxW * maxW + maxH * maxH) + 1; try { Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, mStartRadius, finalRadius); int maxRadius = (int) Math.sqrt(w * w + h * h) + 1; if (mDurationMills == null) { double rate = 1d * finalRadius / maxRadius; mDurationMills = (long) (getFullActivityMills() * Math.sqrt(rate)); } final long finalDuration = mDurationMills; anim.setDuration((long) (finalDuration * 0.9)); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); doOnEnd(); mActivity.overridePendingTransition(mEnterAnim, mExitAnim); mTriggerView.postDelayed(new Runnable() { @Override public void run() { if (mActivity.isFinishing()) return; try { Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, finalRadius, mStartRadius); anim.setDuration(finalDuration); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); try { decorView.removeView(view); } catch (Exception e) { e.printStackTrace(); } } }); anim.start(); } catch (Exception e) { e.printStackTrace(); try { decorView.removeView(view); } catch (Exception e1) { e1.printStackTrace(); } } } }, 1000); } }); anim.start(); } catch (Exception e) { e.printStackTrace(); doOnEnd(); } } private void doOnEnd() { mOnAnimationEndListener.onAnimationEnd(); } } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/DragSelectRecyclerView.java ================================================ package io.virtualapp.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.os.Handler; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import io.virtualapp.R; /** * @author Aidan Follestad (afollestad) */ public class DragSelectRecyclerView extends RecyclerView { private static final boolean LOGGING = false; private static final int AUTO_SCROLL_DELAY = 25; private int mLastDraggedIndex = -1; private DragSelectRecyclerViewAdapter mAdapter; private int mInitialSelection; private boolean mDragSelectActive; private int mMinReached; private int mMaxReached; private int mHotspotHeight; private int mHotspotOffsetTop; private int mHotspotOffsetBottom; private int mHotspotTopBoundStart; private int mHotspotTopBoundEnd; private int mHotspotBottomBoundStart; private int mHotspotBottomBoundEnd; private int mAutoScrollVelocity; private FingerListener mFingerListener; private boolean mInTopHotspot; private boolean mInBottomHotspot; private Handler mAutoScrollHandler; private Runnable mAutoScrollRunnable = new Runnable() { @Override public void run() { if (mAutoScrollHandler == null) return; if (mInTopHotspot) { scrollBy(0, -mAutoScrollVelocity); mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY); } else if (mInBottomHotspot) { scrollBy(0, mAutoScrollVelocity); mAutoScrollHandler.postDelayed(this, AUTO_SCROLL_DELAY); } } }; private RectF mTopBoundRect; private RectF mBottomBoundRect; private Paint mDebugPaint; private boolean mDebugEnabled = false; public DragSelectRecyclerView(Context context) { super(context); init(context, null); } public DragSelectRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public DragSelectRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private static void LOG(String message, Object... args) { //noinspection PointlessBooleanExpression if (!LOGGING) return; if (args != null) { Log.d("DragSelectRecyclerView", String.format(message, args)); } else { Log.d("DragSelectRecyclerView", message); } } private void init(Context context, AttributeSet attrs) { mAutoScrollHandler = new Handler(); final int defaultHotspotHeight = context.getResources().getDimensionPixelSize(R.dimen.dsrv_defaultHotspotHeight); if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DragSelectRecyclerView, 0, 0); try { boolean autoScrollEnabled = a.getBoolean(R.styleable.DragSelectRecyclerView_dsrv_autoScrollEnabled, true); if (!autoScrollEnabled) { mHotspotHeight = -1; mHotspotOffsetTop = -1; mHotspotOffsetBottom = -1; LOG("Auto-scroll disabled"); } else { mHotspotHeight = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspotHeight, defaultHotspotHeight); mHotspotOffsetTop = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetTop, 0); mHotspotOffsetBottom = a.getDimensionPixelSize( R.styleable.DragSelectRecyclerView_dsrv_autoScrollHotspot_offsetBottom, 0); LOG("Hotspot height = %d", mHotspotHeight); } } finally { a.recycle(); } } else { mHotspotHeight = defaultHotspotHeight; LOG("Hotspot height = %d", mHotspotHeight); } } public void setFingerListener(@Nullable FingerListener listener) { this.mFingerListener = listener; } @Override protected void onMeasure(int widthSpec, int heightSpec) { super.onMeasure(widthSpec, heightSpec); if (mHotspotHeight > -1) { mHotspotTopBoundStart = mHotspotOffsetTop; mHotspotTopBoundEnd = mHotspotOffsetTop + mHotspotHeight; mHotspotBottomBoundStart = (getMeasuredHeight() - mHotspotHeight) - mHotspotOffsetBottom; mHotspotBottomBoundEnd = getMeasuredHeight() - mHotspotOffsetBottom; LOG("RecyclerView height = %d", getMeasuredHeight()); LOG("Hotspot top bound = %d to %d", mHotspotTopBoundStart, mHotspotTopBoundStart); LOG("Hotspot bottom bound = %d to %d", mHotspotBottomBoundStart, mHotspotBottomBoundEnd); } } public boolean setDragSelectActive(boolean active, int initialSelection) { if (active && mDragSelectActive) { LOG("Drag selection is already active."); return false; } mLastDraggedIndex = -1; mMinReached = -1; mMaxReached = -1; if (!mAdapter.isIndexSelectable(initialSelection)) { mDragSelectActive = false; mInitialSelection = -1; mLastDraggedIndex = -1; LOG("Index %d is not selectable.", initialSelection); return false; } mAdapter.setSelected(initialSelection, true); mDragSelectActive = active; mInitialSelection = initialSelection; mLastDraggedIndex = initialSelection; if (mFingerListener != null) mFingerListener.onDragSelectFingerAction(true); LOG("Drag selection initialized, starting at index %d.", initialSelection); return true; } /** * Use {@link #setAdapter(DragSelectRecyclerViewAdapter)} instead. */ @Override @Deprecated public void setAdapter(Adapter adapter) { if (!(adapter instanceof DragSelectRecyclerViewAdapter)) throw new IllegalArgumentException("Adapter must be a DragSelectRecyclerViewAdapter."); setAdapter((DragSelectRecyclerViewAdapter) adapter); } public void setAdapter(DragSelectRecyclerViewAdapter adapter) { super.setAdapter(adapter); mAdapter = adapter; } private int getItemPosition(MotionEvent e) { final View v = findChildViewUnder(e.getX(), e.getY()); if (v == null) return NO_POSITION; if (v.getTag() == null || !(v.getTag() instanceof ViewHolder)) throw new IllegalStateException("Make sure your adapter makes a call to super.onBindViewHolder(), and doesn't override itemView tags."); final ViewHolder holder = (ViewHolder) v.getTag(); return holder.getAdapterPosition(); } public final void enableDebug() { mDebugEnabled = true; invalidate(); } @Override public void onDraw(Canvas c) { super.onDraw(c); if (mDebugEnabled) { if (mDebugPaint == null) { mDebugPaint = new Paint(); mDebugPaint.setColor(Color.BLACK); mDebugPaint.setAntiAlias(true); mDebugPaint.setStyle(Paint.Style.FILL); mTopBoundRect = new RectF(0, mHotspotTopBoundStart, getMeasuredWidth(), mHotspotTopBoundEnd); mBottomBoundRect = new RectF(0, mHotspotBottomBoundStart, getMeasuredWidth(), mHotspotBottomBoundEnd); } c.drawRect(mTopBoundRect, mDebugPaint); c.drawRect(mBottomBoundRect, mDebugPaint); } } @Override public boolean dispatchTouchEvent(MotionEvent e) { if (mAdapter.getItemCount() == 0) return super.dispatchTouchEvent(e); if (mDragSelectActive) { if (e.getAction() == MotionEvent.ACTION_UP) { mDragSelectActive = false; mInTopHotspot = false; mInBottomHotspot = false; mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); if (mFingerListener != null) mFingerListener.onDragSelectFingerAction(false); return true; } else if (e.getAction() == MotionEvent.ACTION_MOVE) { // Check for auto-scroll hotspot if (mHotspotHeight > -1) { if (e.getY() >= mHotspotTopBoundStart && e.getY() <= mHotspotTopBoundEnd) { mInBottomHotspot = false; if (!mInTopHotspot) { mInTopHotspot = true; LOG("Now in TOP hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY); } final float simulatedFactor = mHotspotTopBoundEnd - mHotspotTopBoundStart; final float simulatedY = e.getY() - mHotspotTopBoundStart; mAutoScrollVelocity = (int) (simulatedFactor - simulatedY) / 2; LOG("Auto scroll velocity = %d", mAutoScrollVelocity); } else if (e.getY() >= mHotspotBottomBoundStart && e.getY() <= mHotspotBottomBoundEnd) { mInTopHotspot = false; if (!mInBottomHotspot) { mInBottomHotspot = true; LOG("Now in BOTTOM hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mAutoScrollHandler.postDelayed(mAutoScrollRunnable, AUTO_SCROLL_DELAY); } final float simulatedY = e.getY() + mHotspotBottomBoundEnd; final float simulatedFactor = mHotspotBottomBoundStart + mHotspotBottomBoundEnd; mAutoScrollVelocity = (int) (simulatedY - simulatedFactor) / 2; LOG("Auto scroll velocity = %d", mAutoScrollVelocity); } else if (mInTopHotspot || mInBottomHotspot) { LOG("Left the hotspot"); mAutoScrollHandler.removeCallbacks(mAutoScrollRunnable); mInTopHotspot = false; mInBottomHotspot = false; } } // Drag selection logic // NOTE: DISABLE IT // if (itemPosition != NO_POSITION && mLastDraggedIndex != itemPosition) { // mLastDraggedIndex = itemPosition; // if (mMinReached == -1) mMinReached = mLastDraggedIndex; // if (mMaxReached == -1) mMaxReached = mLastDraggedIndex; // if (mLastDraggedIndex > mMaxReached) // mMaxReached = mLastDraggedIndex; // if (mLastDraggedIndex < mMinReached) // mMinReached = mLastDraggedIndex; // if (mAdapter != null) // mAdapter.selectRange(mInitialSelection, mLastDraggedIndex, mMinReached, mMaxReached); // if (mInitialSelection == mLastDraggedIndex) { // mMinReached = mLastDraggedIndex; // mMaxReached = mLastDraggedIndex; // } // } return true; } } return super.dispatchTouchEvent(e); } public interface FingerListener { void onDragSelectFingerAction(boolean fingerDown); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/DragSelectRecyclerViewAdapter.java ================================================ package io.virtualapp.widgets; import android.os.Bundle; import android.support.annotation.CallSuper; import android.support.v7.widget.RecyclerView; import java.util.ArrayList; /** * @author Aidan Follestad (afollestad) */ public abstract class DragSelectRecyclerViewAdapter extends RecyclerView.Adapter { private ArrayList mSelectedIndices; private SelectionListener mSelectionListener; private int mLastCount = -1; private int mMaxSelectionCount = -1; protected DragSelectRecyclerViewAdapter() { mSelectedIndices = new ArrayList<>(); } private void fireSelectionListener() { if (mLastCount == mSelectedIndices.size()) return; mLastCount = mSelectedIndices.size(); if (mSelectionListener != null) mSelectionListener.onDragSelectionChanged(mLastCount); } public void setMaxSelectionCount(int maxSelectionCount) { this.mMaxSelectionCount = maxSelectionCount; } public void setSelectionListener(SelectionListener selectionListener) { this.mSelectionListener = selectionListener; } public void saveInstanceState(Bundle out) { saveInstanceState("selected_indices", out); } public void saveInstanceState(String key, Bundle out) { out.putSerializable(key, mSelectedIndices); } public void restoreInstanceState(Bundle in) { restoreInstanceState("selected_indices", in); } public void restoreInstanceState(String key, Bundle in) { if (in != null && in.containsKey(key)) { //noinspection unchecked mSelectedIndices = (ArrayList) in.getSerializable(key); if (mSelectedIndices == null) mSelectedIndices = new ArrayList<>(); else fireSelectionListener(); } } public final void setSelected(int index, boolean selected) { if (!isIndexSelectable(index)) selected = false; if (selected) { if (!mSelectedIndices.contains(index) && (mMaxSelectionCount == -1 || mSelectedIndices.size() < mMaxSelectionCount)) { mSelectedIndices.add(index); notifyItemChanged(index); } } else if (mSelectedIndices.contains(index)) { mSelectedIndices.remove((Integer) index); notifyItemChanged(index); } fireSelectionListener(); } public final boolean toggleSelected(int index) { boolean selectedNow = false; if (isIndexSelectable(index)) { if (mSelectedIndices.contains(index)) { mSelectedIndices.remove((Integer) index); } else if (mMaxSelectionCount == -1 || mSelectedIndices.size() < mMaxSelectionCount) { mSelectedIndices.add(index); selectedNow = true; } notifyItemChanged(index); } fireSelectionListener(); return selectedNow; } protected boolean isIndexSelectable(int index) { return true; } @CallSuper @Override public void onBindViewHolder(VH holder, int position) { holder.itemView.setTag(holder); } public final void selectRange(int from, int to, int min, int max) { if (from == to) { // Finger is back on the initial item, unselect everything else for (int i = min; i <= max; i++) { if (i == from) continue; setSelected(i, false); } fireSelectionListener(); return; } if (to < from) { // When selecting from one to previous items for (int i = to; i <= from; i++) setSelected(i, true); if (min > -1 && min < to) { // Unselect items that were selected during this drag but no longer are for (int i = min; i < to; i++) { if (i == from) continue; setSelected(i, false); } } if (max > -1) { for (int i = from + 1; i <= max; i++) setSelected(i, false); } } else { // When selecting from one to next items for (int i = from; i <= to; i++) setSelected(i, true); if (max > -1 && max > to) { // Unselect items that were selected during this drag but no longer are for (int i = to + 1; i <= max; i++) { if (i == from) continue; setSelected(i, false); } } if (min > -1) { for (int i = min; i < from; i++) setSelected(i, false); } } fireSelectionListener(); } public final void selectAll() { int max = getItemCount(); mSelectedIndices.clear(); for (int i = 0; i < max; i++) { if (isIndexSelectable(i)) { mSelectedIndices.add(i); } } notifyDataSetChanged(); fireSelectionListener(); } public final void clearSelected() { mSelectedIndices.clear(); notifyDataSetChanged(); fireSelectionListener(); } public final int getSelectedCount() { return mSelectedIndices.size(); } public final Integer[] getSelectedIndices() { return mSelectedIndices.toArray(new Integer[mSelectedIndices.size()]); } public final boolean isIndexSelected(int index) { return mSelectedIndices.contains(index); } public interface SelectionListener { void onDragSelectionChanged(int count); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/EatBeansView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; public class EatBeansView extends BaseView { int eatSpeed = 5; private Paint mPaint, mPaintEye; private float mWidth = 0f; private float mHigh = 0f; private float mPadding = 5f; private float eatErWidth = 60f; private float eatErPositionX = 0f; private float beansWidth = 10f; private float mAngle = 34; private float eatErStartAngle = mAngle; private float eatErEndAngle = 360 - 2 * eatErStartAngle; public EatBeansView(Context context) { super(context); } public EatBeansView(Context context, AttributeSet attrs) { super(context, attrs); } public EatBeansView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth(); mHigh = getMeasuredHeight(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float eatRightX = mPadding + eatErWidth + eatErPositionX; RectF rectF = new RectF(mPadding + eatErPositionX, mHigh / 2 - eatErWidth / 2, eatRightX, mHigh / 2 + eatErWidth / 2); canvas.drawArc(rectF, eatErStartAngle, eatErEndAngle , true, mPaint); canvas.drawCircle(mPadding + eatErPositionX + eatErWidth / 2, mHigh / 2 - eatErWidth / 4, beansWidth / 2, mPaintEye); int beansCount = (int) ((mWidth - mPadding * 2 - eatErWidth) / beansWidth / 2); for (int i = 0; i < beansCount; i++) { float x = beansCount * i + beansWidth / 2 + mPadding + eatErWidth; if (x > eatRightX) { canvas.drawCircle(x, mHigh / 2, beansWidth / 2, mPaint); } } } private void initPaint() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(Color.WHITE); mPaintEye = new Paint(); mPaintEye.setAntiAlias(true); mPaintEye.setStyle(Paint.Style.FILL); mPaintEye.setColor(Color.BLACK); } public void setViewColor(int color) { mPaint.setColor(color); postInvalidate(); } public void setEyeColor(int color) { mPaintEye.setColor(color); postInvalidate(); } @Override protected void InitPaint() { initPaint(); } @Override protected void OnAnimationUpdate(ValueAnimator valueAnimator) { float mAnimatedValue = (float) valueAnimator.getAnimatedValue(); eatErPositionX = (mWidth - 2 * mPadding - eatErWidth) * mAnimatedValue; eatErStartAngle = mAngle * (1 - (mAnimatedValue * eatSpeed - (int) (mAnimatedValue * eatSpeed))); eatErEndAngle = 360 - eatErStartAngle * 2; invalidate(); } @Override protected void OnAnimationRepeat(Animator animation) { } @Override protected int OnStopAnim() { eatErPositionX = 0; postInvalidate(); return 1; } @Override protected int SetAnimRepeatMode() { return ValueAnimator.RESTART; } @Override protected void AnimIsRunning() { } @Override protected int SetAnimRepeatCount() { return ValueAnimator.INFINITE; } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/Indicator.java ================================================ package io.virtualapp.widgets; import android.animation.ValueAnimator; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import java.util.ArrayList; import java.util.HashMap; public abstract class Indicator extends Drawable implements Animatable { private static final Rect ZERO_BOUNDS_RECT = new Rect(); protected Rect drawBounds = ZERO_BOUNDS_RECT; private HashMap mUpdateListeners = new HashMap<>(); private ArrayList mAnimators; private int alpha = 255; private boolean mHasAnimators; private Paint mPaint = new Paint(); public Indicator() { mPaint.setColor(Color.WHITE); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); } public int getColor() { return mPaint.getColor(); } public void setColor(int color) { mPaint.setColor(color); } @Override public int getAlpha() { return alpha; } @Override public void setAlpha(int alpha) { this.alpha = alpha; } @Override public int getOpacity() { return PixelFormat.OPAQUE; } @Override public void setColorFilter(ColorFilter colorFilter) { } @Override public void draw(Canvas canvas) { draw(canvas, mPaint); } public abstract void draw(Canvas canvas, Paint paint); public abstract ArrayList onCreateAnimators(); @Override public void start() { ensureAnimators(); if (mAnimators == null) { return; } // If the animators has not ended, do nothing. if (isStarted()) { return; } startAnimators(); invalidateSelf(); } private void startAnimators() { for (int i = 0; i < mAnimators.size(); i++) { ValueAnimator animator = mAnimators.get(i); //when the animator restart , add the updateListener again because they // was removed by animator stop . ValueAnimator.AnimatorUpdateListener updateListener = mUpdateListeners.get(animator); if (updateListener != null) { animator.addUpdateListener(updateListener); } animator.start(); } } private void stopAnimators() { if (mAnimators != null) { for (ValueAnimator animator : mAnimators) { if (animator != null && animator.isStarted()) { animator.removeAllUpdateListeners(); animator.end(); } } } } private void ensureAnimators() { if (!mHasAnimators) { mAnimators = onCreateAnimators(); mHasAnimators = true; } } @Override public void stop() { stopAnimators(); } private boolean isStarted() { for (ValueAnimator animator : mAnimators) { return animator.isStarted(); } return false; } @Override public boolean isRunning() { for (ValueAnimator animator : mAnimators) { return animator.isRunning(); } return false; } /** * Your should use this to add AnimatorUpdateListener when * create animator , otherwise , animator doesn't work when * the animation restart . * * @param updateListener */ public void addUpdateListener(ValueAnimator animator, ValueAnimator.AnimatorUpdateListener updateListener) { mUpdateListeners.put(animator, updateListener); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); setDrawBounds(bounds); } public void setDrawBounds(int left, int top, int right, int bottom) { this.drawBounds = new Rect(left, top, right, bottom); } public void postInvalidate() { invalidateSelf(); } public Rect getDrawBounds() { return drawBounds; } public void setDrawBounds(Rect drawBounds) { setDrawBounds(drawBounds.left, drawBounds.top, drawBounds.right, drawBounds.bottom); } public int getWidth() { return drawBounds.width(); } public int getHeight() { return drawBounds.height(); } public int centerX() { return drawBounds.centerX(); } public int centerY() { return drawBounds.centerY(); } public float exactCenterX() { return drawBounds.exactCenterX(); } public float exactCenterY() { return drawBounds.exactCenterY(); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/LabelView.java ================================================ package io.virtualapp.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import io.virtualapp.R; public class LabelView extends View { private static final int DEFAULT_DEGREES = 45; private String mTextContent; private int mTextColor; private float mTextSize; private boolean mTextBold; private boolean mFillTriangle; private boolean mTextAllCaps; private int mBackgroundColor; private float mMinSize; private float mPadding; private int mGravity; private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Path mPath = new Path(); public LabelView(Context context) { this(context, null); } public LabelView(Context context, AttributeSet attrs) { super(context, attrs); obtainAttributes(context, attrs); mTextPaint.setTextAlign(Paint.Align.CENTER); } private void obtainAttributes(Context context, AttributeSet attrs) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LabelView); mTextContent = ta.getString(R.styleable.LabelView_lv_text); mTextColor = ta.getColor(R.styleable.LabelView_lv_text_color, Color.parseColor("#ffffff")); mTextSize = ta.getDimension(R.styleable.LabelView_lv_text_size, sp2px(11)); mTextBold = ta.getBoolean(R.styleable.LabelView_lv_text_bold, true); mTextAllCaps = ta.getBoolean(R.styleable.LabelView_lv_text_all_caps, true); mFillTriangle = ta.getBoolean(R.styleable.LabelView_lv_fill_triangle, false); mBackgroundColor = ta.getColor(R.styleable.LabelView_lv_background_color, Color.parseColor("#FF4081")); mMinSize = ta.getDimension(R.styleable.LabelView_lv_min_size, mFillTriangle ? dp2px(35) : dp2px(50)); mPadding = ta.getDimension(R.styleable.LabelView_lv_padding, dp2px(3.5f)); mGravity = ta.getInt(R.styleable.LabelView_lv_gravity, Gravity.TOP | Gravity.LEFT); ta.recycle(); } public String getText() { return mTextContent; } public void setText(String text) { mTextContent = text; invalidate(); } public int getTextColor() { return mTextColor; } public void setTextColor(int textColor) { mTextColor = textColor; invalidate(); } public float getTextSize() { return mTextSize; } public void setTextSize(float textSize) { mTextSize = sp2px(textSize); invalidate(); } public boolean isTextBold() { return mTextBold; } public void setTextBold(boolean textBold) { mTextBold = textBold; invalidate(); } public boolean isFillTriangle() { return mFillTriangle; } public void setFillTriangle(boolean fillTriangle) { mFillTriangle = fillTriangle; invalidate(); } public boolean isTextAllCaps() { return mTextAllCaps; } public void setTextAllCaps(boolean textAllCaps) { mTextAllCaps = textAllCaps; invalidate(); } public int getBgColor() { return mBackgroundColor; } public void setBgColor(int backgroundColor) { mBackgroundColor = backgroundColor; invalidate(); } public float getMinSize() { return mMinSize; } public void setMinSize(float minSize) { mMinSize = dp2px(minSize); invalidate(); } public float getPadding() { return mPadding; } public void setPadding(float padding) { mPadding = dp2px(padding); invalidate(); } public int getGravity() { return mGravity; } /** * Gravity.TOP | Gravity.LEFT * Gravity.TOP | Gravity.RIGHT * Gravity.BOTTOM | Gravity.LEFT * Gravity.BOTTOM | Gravity.RIGHT */ public void setGravity(int gravity) { mGravity = gravity; } @Override protected void onDraw(Canvas canvas) { int size = getHeight(); mTextPaint.setColor(mTextColor); mTextPaint.setTextSize(mTextSize); mTextPaint.setFakeBoldText(mTextBold); mBackgroundPaint.setColor(mBackgroundColor); float textHeight = mTextPaint.descent() - mTextPaint.ascent(); if (mFillTriangle) { if (mGravity == (Gravity.TOP | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, 0); mPath.lineTo(0, size); mPath.lineTo(size, 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, -DEFAULT_DEGREES, canvas, true); } else if (mGravity == (Gravity.TOP | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(size, 0); mPath.lineTo(0, 0); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, DEFAULT_DEGREES, canvas, true); } else if (mGravity == (Gravity.BOTTOM | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, size); mPath.lineTo(0, 0); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, DEFAULT_DEGREES, canvas, false); } else if (mGravity == (Gravity.BOTTOM | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(size, size); mPath.lineTo(0, size); mPath.lineTo(size, 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawTextWhenFill(size, -DEFAULT_DEGREES, canvas, false); } } else { double delta = (textHeight + mPadding * 2) * Math.sqrt(2); if (mGravity == (Gravity.TOP | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, (float) (size - delta)); mPath.lineTo(0, size); mPath.lineTo(size, 0); mPath.lineTo((float) (size - delta), 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, -DEFAULT_DEGREES, canvas, textHeight, true); } else if (mGravity == (Gravity.TOP | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(0, 0); mPath.lineTo((float) delta, 0); mPath.lineTo(size, (float) (size - delta)); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, DEFAULT_DEGREES, canvas, textHeight, true); } else if (mGravity == (Gravity.BOTTOM | Gravity.LEFT)) { mPath.reset(); mPath.moveTo(0, 0); mPath.lineTo(0, (float) delta); mPath.lineTo((float) (size - delta), size); mPath.lineTo(size, size); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, DEFAULT_DEGREES, canvas, textHeight, false); } else if (mGravity == (Gravity.BOTTOM | Gravity.RIGHT)) { mPath.reset(); mPath.moveTo(0, size); mPath.lineTo((float) delta, size); mPath.lineTo(size, (float) delta); mPath.lineTo(size, 0); mPath.close(); canvas.drawPath(mPath, mBackgroundPaint); drawText(size, -DEFAULT_DEGREES, canvas, textHeight, false); } } } private void drawText(int size, float degrees, Canvas canvas, float textHeight, boolean isTop) { canvas.save(); canvas.rotate(degrees, size / 2f, size / 2f); float delta = isTop ? -(textHeight + mPadding * 2) / 2 : (textHeight + mPadding * 2) / 2; float textBaseY = size / 2 - (mTextPaint.descent() + mTextPaint.ascent()) / 2 + delta; canvas.drawText(mTextAllCaps ? mTextContent.toUpperCase() : mTextContent, getPaddingLeft() + (size - getPaddingLeft() - getPaddingRight()) / 2, textBaseY, mTextPaint); canvas.restore(); } private void drawTextWhenFill(int size, float degrees, Canvas canvas, boolean isTop) { canvas.save(); canvas.rotate(degrees, size / 2f, size / 2f); float delta = isTop ? -size / 4 : size / 4; float textBaseY = size / 2 - (mTextPaint.descent() + mTextPaint.ascent()) / 2 + delta; canvas.drawText(mTextAllCaps ? mTextContent.toUpperCase() : mTextContent, getPaddingLeft() + (size - getPaddingLeft() - getPaddingRight()) / 2, textBaseY, mTextPaint); canvas.restore(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int measuredWidth = measureWidth(widthMeasureSpec); setMeasuredDimension(measuredWidth, measuredWidth); } /** * 确定View宽度大小 */ private int measureWidth(int widthMeasureSpec) { int result; int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec); if (specMode == MeasureSpec.EXACTLY) {//大小确定直接使用 result = specSize; } else { int padding = getPaddingLeft() + getPaddingRight(); mTextPaint.setColor(mTextColor); mTextPaint.setTextSize(mTextSize); float textWidth = mTextPaint.measureText(mTextContent + ""); result = (int) ((padding + (int) textWidth) * Math.sqrt(2)); //如果父视图的测量要求为AT_MOST,即限定了一个最大值,则再从系统建议值和自己计算值中去一个较小值 if (specMode == MeasureSpec.AT_MOST) { result = Math.min(result, specSize); } result = Math.max((int) mMinSize, result); } return result; } protected int dp2px(float dp) { final float scale = getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); } protected int sp2px(float sp) { final float scale = getResources().getDisplayMetrics().scaledDensity; return (int) (sp * scale + 0.5f); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/LauncherIconView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.util.Log; import android.view.animation.DecelerateInterpolator; import io.virtualapp.R; import static android.graphics.Canvas.ALL_SAVE_FLAG; public class LauncherIconView extends AppCompatImageView implements ShimmerViewBase { private static final int SMOOTH_ANIM_THRESHOLD = 5; private static final String TAG = "LauncherIconView"; private ShimmerViewHelper mShimmerViewHelper; private Shimmer mShimmer; private float mProgress; private int mHeight; private int mWidth; private int mStrokeWidth; private float mRadius; private float mInterDelta; private int mMaskColor; private float mMaxMaskRadius; private float mMaskAnimDelta; private boolean mIsSquare; private boolean mMaskAnimRunning; private long mMediumAnimTime; private Paint mShimmerPaint; private Paint mPaint; private RectF mProgressOval; private ValueAnimator mInterAnim; private ValueAnimator mProgressAnimator; public LauncherIconView(Context context) { super(context); init(context, null); } public LauncherIconView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public LauncherIconView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { mMediumAnimTime = getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressImageView); try { this.mProgress = a.getInteger(R.styleable.ProgressImageView_pi_progress, 0); this.mStrokeWidth = a.getDimensionPixelOffset(R.styleable.ProgressImageView_pi_stroke, 8); this.mRadius = a.getDimensionPixelOffset(R.styleable.ProgressImageView_pi_radius, 0); this.mIsSquare = a.getBoolean(R.styleable.ProgressImageView_pi_force_square, false); this.mMaskColor = a.getColor(R.styleable.ProgressImageView_pi_mask_color, Color.argb(180, 0, 0, 0)); this.mPaint = new Paint(); mPaint.setColor(mMaskColor); mPaint.setAntiAlias(true); this.mShimmerPaint = new Paint(); mShimmerPaint.setColor(Color.WHITE); } finally { a.recycle(); } mShimmerViewHelper = new ShimmerViewHelper(this, mShimmerPaint, attrs); } private void initParams() { if (mWidth == 0) mWidth = getWidth(); if (mHeight == 0) mHeight = getHeight(); if (mWidth != 0 && mHeight != 0) { if (mRadius == 0) mRadius = Math.min(mWidth, mHeight) / 4f; if (mMaxMaskRadius == 0) mMaxMaskRadius = (float) (0.5f * Math.sqrt(mWidth * mWidth + mHeight * mHeight)); if (mProgressOval == null) mProgressOval = new RectF( mWidth / 2f - mRadius + mStrokeWidth, mHeight / 2f - mRadius + mStrokeWidth, mWidth / 2f + mRadius - mStrokeWidth, mHeight / 2f + mRadius - mStrokeWidth); } } @Override protected void onDraw(Canvas canvas) { if (mShimmerViewHelper != null) { mShimmerViewHelper.onDraw(); } super.onDraw(canvas); int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, ALL_SAVE_FLAG); initParams(); if (mProgress < 100) { drawMask(canvas); if (mProgress == 0) updateInterAnim(canvas); else drawProgress(canvas); } if (mMaskAnimRunning) updateMaskAnim(canvas); canvas.restoreToCount(sc); } private void drawMask(Canvas canvas) { canvas.drawRect(0, 0, mWidth, mHeight, mPaint); } private void drawProgress(Canvas canvas) { mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius, mPaint); mPaint.setXfermode(null); //start angle : -90 ~ 270;sweep Angle : 360 ~ 0; canvas.drawArc(mProgressOval, -90 + mProgress * 3.6f, 360 - mProgress * 3.6f, true, mPaint); } private void updateInterAnim(Canvas canvas) { // if (!mInterAnimRunning) mInterDelta = 0.f; //outer circle mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); canvas.drawCircle(mWidth / 2.f, mHeight / 2.f, mRadius, mPaint); mPaint.setXfermode(null); //inner circle canvas.drawCircle(mWidth / 2.f, mHeight / 2.f, mRadius - mInterDelta, mPaint); } private void updateMaskAnim(Canvas canvas) { canvas.drawRect(0, 0, mWidth, mHeight, mPaint); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); canvas.drawCircle(mWidth / 2f, mHeight / 2f, mRadius + mMaskAnimDelta, mPaint);//mRatio : 0 ~ mRatio * 1.5 mPaint.setXfermode(null); } private void startInterAnim(final int progress) { if (mInterAnim != null) mInterAnim.cancel(); mInterAnim = ValueAnimator.ofFloat(0.f, mStrokeWidth); mInterAnim.setInterpolator(new DecelerateInterpolator()); mInterAnim.setDuration(getContext().getResources().getInteger(android.R.integer.config_shortAnimTime)); mInterAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mInterDelta = (float) animation.getAnimatedValue(); invalidate(); } }); mInterAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); // mInterAnimRunning = true; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); // mInterAnimRunning = false; } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); // mInterAnimRunning = false; if (progress > 0) startProgressAnim(0, progress); } }); mInterAnim.start(); } private void startProgressAnim(float from, float to) { if (mProgressAnimator != null) mProgressAnimator.cancel(); final boolean isReverse = from > to; mProgressAnimator = ValueAnimator.ofFloat(from, to); mProgressAnimator.setInterpolator(new DecelerateInterpolator()); mProgressAnimator.setDuration(mMediumAnimTime); mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mProgress = (float) animation.getAnimatedValue(); if (0 < mProgress && mProgress < 100) invalidate(); else if (mProgress == 100 && !isReverse) startMaskAnim(); } }); mProgressAnimator.start(); } private void startMaskAnim() { if (mProgressAnimator != null) mProgressAnimator.cancel(); ValueAnimator animator = ValueAnimator.ofFloat(0.f, mMaxMaskRadius); animator.setInterpolator(new DecelerateInterpolator()); animator.setDuration(mMediumAnimTime); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMaskAnimRunning = true; mMaskAnimDelta = (float) animation.getAnimatedValue(); invalidate(); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); mMaskAnimRunning = true; } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); mMaskAnimRunning = false; } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mMaskAnimRunning = false; } }); animator.start(); } /** * get the stroke width. * * @return the stroke width in pixel. */ public int getStrokeWidth() { return mStrokeWidth; } /** * set the stroke width.default is 8dp. * * @param strokeWidth stroke width in pixel */ public void setStrokeWidth(int strokeWidth) { this.mStrokeWidth = strokeWidth; this.mProgressOval = null; invalidate(); } /** * get the radius of inner progress circle. * * @return the inner circle radius in pixel. */ public float getRadius() { return mRadius; } /** * set the radius of the inner progress circle. * * @param radius radius in pixel */ public void setRadius(float radius) { this.mRadius = radius; this.mProgressOval = null; invalidate(); } /** * get the color for mask . * * @return the mask color */ public int getMaskColor() { return mMaskColor; } /** * set the color for mask. Argb will looks better. Default is Color.argb(180,0,0,0) * * @param maskColor the color value. */ public void setMaskColor(int maskColor) { mMaskColor = maskColor; mPaint.setColor(mMaskColor); invalidate(); } /** * get current progress. * * @return current progress value. */ public int getProgress() { return (int) mProgress; } /** * @param progress the progress ,range [0,100] */ public void setProgress(int progress) { setProgress(progress, true); } /** * @param progress the progress in [0,100] * @param animate true to enable smooth animation when progress changed more than 5. */ public void setProgress(int progress, boolean animate) { progress = Math.min(Math.max(progress, 0), 100); Log.d(TAG, "setProgress: p:" + progress + ",mp:" + mProgress); if (Math.abs(progress - mProgress) > SMOOTH_ANIM_THRESHOLD && animate) { if (mProgress == 0) { startInterAnim(progress); } else { startProgressAnim(mProgress, progress); } } else if (progress == 100 && animate) { mProgress = 100; startMaskAnim(); } else { mProgress = progress; if (mProgress == 0.f) mInterDelta = 0.f; invalidate(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mIsSquare) { int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); int size = measuredWidth == 0 ? MeasureSpec.getSize(heightMeasureSpec) : measuredWidth; setMeasuredDimension(size, size); } } @Override public float getGradientX() { return mShimmerViewHelper.getGradientX(); } @Override public void setGradientX(float gradientX) { mShimmerViewHelper.setGradientX(gradientX); } @Override public boolean isShimmering() { return mShimmerViewHelper.isShimmering(); } @Override public void setShimmering(boolean isShimmering) { mShimmerViewHelper.setShimmering(isShimmering); } @Override public boolean isSetUp() { return mShimmerViewHelper.isSetUp(); } @Override public void setAnimationSetupCallback(ShimmerViewHelper.AnimationSetupCallback callback) { mShimmerViewHelper.setAnimationSetupCallback(callback); } @Override public int getPrimaryColor() { return mShimmerViewHelper.getPrimaryColor(); } @Override public void setPrimaryColor(int primaryColor) { mShimmerViewHelper.setPrimaryColor(primaryColor); } @Override public int getReflectionColor() { return mShimmerViewHelper.getReflectionColor(); } @Override public void setReflectionColor(int reflectionColor) { mShimmerViewHelper.setReflectionColor(reflectionColor); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mShimmerViewHelper != null) { mShimmerViewHelper.onSizeChanged(); } } public void stopShimmer() { if (mShimmer != null && mShimmer.isAnimating()) { mShimmer.cancel(); mShimmer = null; } } public void startShimmer() { stopShimmer(); mShimmer = new Shimmer(); mShimmer.setRepeatCount(1) .setStartDelay(800L) .setDirection(Shimmer.ANIMATION_DIRECTION_LTR) .start(this); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/LoadingIndicatorView.java ================================================ package io.virtualapp.widgets; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.AnimationUtils; import io.virtualapp.R; public class LoadingIndicatorView extends View { private static final String TAG = "LoadingIndicatorView"; private static final Indicator DEFAULT_INDICATOR = new BallGridBeatIndicator(); private static final int MIN_SHOW_TIME = 500; // ms private static final int MIN_DELAY = 500; // ms int mMinWidth; int mMaxWidth; int mMinHeight; int mMaxHeight; private long mStartTime = -1; private boolean mPostedHide = false; private boolean mPostedShow = false; private boolean mDismissed = false; private Indicator mIndicator; private int mIndicatorColor; private boolean mShouldStartAnimationDrawable; private final Runnable mDelayedHide = new Runnable() { @Override public void run() { mPostedHide = false; mStartTime = -1; setVisibility(View.GONE); } }; private final Runnable mDelayedShow = new Runnable() { @Override public void run() { mPostedShow = false; if (!mDismissed) { mStartTime = System.currentTimeMillis(); setVisibility(View.VISIBLE); } } }; public LoadingIndicatorView(Context context) { super(context); init(context, null, 0, 0); } public LoadingIndicatorView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs, 0, R.style.AVLoadingIndicatorView); } public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, R.style.AVLoadingIndicatorView); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs, defStyleAttr, R.style.AVLoadingIndicatorView); } private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mMinWidth = 24; mMaxWidth = 48; mMinHeight = 24; mMaxHeight = 48; final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.LoadingIndicatorView, defStyleAttr, defStyleRes); mMinWidth = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_minWidth, mMinWidth); mMaxWidth = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_maxWidth, mMaxWidth); mMinHeight = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_minHeight, mMinHeight); mMaxHeight = a.getDimensionPixelSize(R.styleable.LoadingIndicatorView_maxHeight, mMaxHeight); String indicatorName = a.getString(R.styleable.LoadingIndicatorView_indicatorName); mIndicatorColor = a.getColor(R.styleable.LoadingIndicatorView_indicatorColor, Color.WHITE); setIndicator(indicatorName); if (mIndicator == null) { setIndicator(DEFAULT_INDICATOR); } a.recycle(); } public Indicator getIndicator() { return mIndicator; } /** * You should pay attention to pass this parameter with two way: * for example: * 1. Only class Name,like "SimpleIndicator".(This way would use default package name with * "com.wang.avi.indicators") * 2. Class name with full package,like "com.my.android.indicators.SimpleIndicator". * * @param indicatorName the class must be extend Indicator . */ public void setIndicator(String indicatorName) { if (TextUtils.isEmpty(indicatorName)) { return; } StringBuilder drawableClassName = new StringBuilder(); if (!indicatorName.contains(".")) { String defaultPackageName = getClass().getPackage().getName(); drawableClassName.append(defaultPackageName) .append("."); } drawableClassName.append(indicatorName); try { Class drawableClass = Class.forName(drawableClassName.toString()); Indicator indicator = (Indicator) drawableClass.newInstance(); setIndicator(indicator); } catch (ClassNotFoundException e) { Log.e(TAG, "Didn't find your class , check the name again !"); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } public void setIndicator(Indicator d) { if (mIndicator != d) { if (mIndicator != null) { mIndicator.setCallback(null); unscheduleDrawable(mIndicator); } mIndicator = d; //need to set indicator color again if you didn't specified when you update the indicator . setIndicatorColor(mIndicatorColor); if (d != null) { d.setCallback(this); } postInvalidate(); } } /** * setIndicatorColor(0xFF00FF00) * or * setIndicatorColor(Color.BLUE) * or * setIndicatorColor(Color.parseColor("#FF4081")) * or * setIndicatorColor(0xFF00FF00) * or * setIndicatorColor(getResources().getColor(android.R.color.black)) * * @param color */ public void setIndicatorColor(int color) { this.mIndicatorColor = color; mIndicator.setColor(color); } public void smoothToShow() { startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_in)); setVisibility(VISIBLE); } public void smoothToHide() { startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_out)); setVisibility(GONE); } public void hide() { mDismissed = true; removeCallbacks(mDelayedShow); long diff = System.currentTimeMillis() - mStartTime; if (diff >= MIN_SHOW_TIME || mStartTime == -1) { // The progress spinner has been shown long enough // OR was not shown yet. If it wasn't shown yet, // it will just never be shown. setVisibility(View.GONE); } else { // The progress spinner is shown, but not long enough, // so put a delayed message in to hide it when its been // shown long enough. if (!mPostedHide) { postDelayed(mDelayedHide, MIN_SHOW_TIME - diff); mPostedHide = true; } } } public void show() { // Reset the start time. mStartTime = -1; mDismissed = false; removeCallbacks(mDelayedHide); if (!mPostedShow) { postDelayed(mDelayedShow, MIN_DELAY); mPostedShow = true; } } @Override protected boolean verifyDrawable(Drawable who) { return who == mIndicator || super.verifyDrawable(who); } void startAnimation() { if (getVisibility() != VISIBLE) { return; } if (mIndicator instanceof Animatable) { mShouldStartAnimationDrawable = true; } postInvalidate(); } void stopAnimation() { if (mIndicator instanceof Animatable) { mIndicator.stop(); mShouldStartAnimationDrawable = false; } postInvalidate(); } @Override public void setVisibility(int v) { if (getVisibility() != v) { super.setVisibility(v); if (v == GONE || v == INVISIBLE) { stopAnimation(); } else { startAnimation(); } } } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (visibility == GONE || visibility == INVISIBLE) { stopAnimation(); } else { startAnimation(); } } @Override public void invalidateDrawable(Drawable dr) { if (verifyDrawable(dr)) { final Rect dirty = dr.getBounds(); final int scrollX = getScrollX() + getPaddingLeft(); final int scrollY = getScrollY() + getPaddingTop(); invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); } else { super.invalidateDrawable(dr); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateDrawableBounds(w, h); } private void updateDrawableBounds(int w, int h) { // onDraw will translate the canvas so we draw starting at 0,0. // Subtract out padding for the purposes of the calculations below. w -= getPaddingRight() + getPaddingLeft(); h -= getPaddingTop() + getPaddingBottom(); int right = w; int bottom = h; int top = 0; int left = 0; if (mIndicator != null) { // Maintain aspect ratio. Certain kinds of animated drawables // get very confused otherwise. final int intrinsicWidth = mIndicator.getIntrinsicWidth(); final int intrinsicHeight = mIndicator.getIntrinsicHeight(); final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight; final float boundAspect = (float) w / h; if (intrinsicAspect != boundAspect) { if (boundAspect > intrinsicAspect) { // New width is larger. Make it smaller to match height. final int width = (int) (h * intrinsicAspect); left = (w - width) / 2; right = left + width; } else { // New height is larger. Make it smaller to match width. final int height = (int) (w * (1 / intrinsicAspect)); top = (h - height) / 2; bottom = top + height; } } mIndicator.setBounds(left, top, right, bottom); } } @Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); drawTrack(canvas); } void drawTrack(Canvas canvas) { final Drawable d = mIndicator; if (d != null) { // Translate canvas so a indeterminate circular progress bar with padding // rotates properly in its animation final int saveCount = canvas.save(); canvas.translate(getPaddingLeft(), getPaddingTop()); d.draw(canvas); canvas.restoreToCount(saveCount); if (mShouldStartAnimationDrawable && d instanceof Animatable) { ((Animatable) d).start(); mShouldStartAnimationDrawable = false; } } } @Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int dw = 0; int dh = 0; final Drawable d = mIndicator; if (d != null) { dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); } updateDrawableState(); dw += getPaddingLeft() + getPaddingRight(); dh += getPaddingTop() + getPaddingBottom(); final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0); final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0); setMeasuredDimension(measuredWidth, measuredHeight); } @Override protected void drawableStateChanged() { super.drawableStateChanged(); updateDrawableState(); } private void updateDrawableState() { final int[] state = getDrawableState(); if (mIndicator != null && mIndicator.isStateful()) { mIndicator.setState(state); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void drawableHotspotChanged(float x, float y) { super.drawableHotspotChanged(x, y); if (mIndicator != null) { mIndicator.setHotspot(x, y); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); startAnimation(); removeCallbacks(); } @Override protected void onDetachedFromWindow() { stopAnimation(); // This should come after stopAnimation(), otherwise an invalidate message remains in the // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation super.onDetachedFromWindow(); removeCallbacks(); } private void removeCallbacks() { removeCallbacks(mDelayedHide); removeCallbacks(mDelayedShow); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/MarqueeTextView.java ================================================ package io.virtualapp.widgets; import android.content.Context; import android.graphics.Canvas; import android.support.v7.widget.AppCompatTextView; import android.util.AttributeSet; public class MarqueeTextView extends AppCompatTextView { private boolean isStop = false; public MarqueeTextView(Context context) { super(context); } public MarqueeTextView(Context context, AttributeSet attrs) { super(context, attrs); } public MarqueeTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public boolean isFocused() { if (this.isStop) { return super.isFocused(); } return true; } public void stopScroll() { this.isStop = true; } public void start() { this.isStop = false; } protected void onDetachedFromWindow() { stopScroll(); super.onDetachedFromWindow(); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/MaterialRippleLayout.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.util.Property; import android.util.TypedValue; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import android.widget.AdapterView; import android.widget.FrameLayout; import io.virtualapp.R; import static android.view.GestureDetector.SimpleOnGestureListener; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; public class MaterialRippleLayout extends FrameLayout { private static final int DEFAULT_DURATION = 350; private static final int DEFAULT_FADE_DURATION = 75; private static final float DEFAULT_DIAMETER_DP = 35; private static final float DEFAULT_ALPHA = 0.2f; private static final int DEFAULT_COLOR = Color.BLACK; private static final int DEFAULT_BACKGROUND = Color.TRANSPARENT; private static final boolean DEFAULT_HOVER = true; private static final boolean DEFAULT_DELAY_CLICK = true; private static final boolean DEFAULT_PERSISTENT = false; private static final boolean DEFAULT_SEARCH_ADAPTER = false; private static final boolean DEFAULT_RIPPLE_OVERLAY = false; private static final int DEFAULT_ROUNDED_CORNERS = 0; private static final int FADE_EXTRA_DELAY = 50; private static final long HOVER_DURATION = 2500; private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Rect bounds = new Rect(); private int rippleColor; private boolean rippleOverlay; private boolean rippleHover; private int rippleDiameter; private int rippleDuration; private int rippleAlpha; private boolean rippleDelayClick; private int rippleFadeDuration; private boolean ripplePersistent; private Drawable rippleBackground; private boolean rippleInAdapter; private float rippleRoundedCorners; private float radius; private AdapterView parentAdapter; private View childView; private AnimatorSet rippleAnimator; private ObjectAnimator hoverAnimator; private Point currentCoords = new Point(); private Point previousCoords = new Point(); private int layerType; private boolean eventCancelled; private boolean prepressed; private int positionInAdapter; private GestureDetector gestureDetector; private PerformClickEvent pendingClickEvent; private PressedEvent pendingPressEvent; private boolean hasPerformedLongPress; /* * Animations */ private Property radiusProperty = new Property(Float.class, "radius") { @Override public Float get(MaterialRippleLayout object) { return object.getRadius(); } @Override public void set(MaterialRippleLayout object, Float value) { object.setRadius(value); } }; private Property circleAlphaProperty = new Property(Integer.class, "rippleAlpha") { @Override public Integer get(MaterialRippleLayout object) { return object.getRippleAlpha(); } @Override public void set(MaterialRippleLayout object, Integer value) { object.setRippleAlpha(value); } }; private SimpleOnGestureListener longClickListener = new GestureDetector.SimpleOnGestureListener() { public void onLongPress(MotionEvent e) { hasPerformedLongPress = childView.performLongClick(); if (hasPerformedLongPress) { if (rippleHover) { startRipple(null); } cancelPressedEvent(); } } @Override public boolean onDown(MotionEvent e) { hasPerformedLongPress = false; return super.onDown(e); } }; public MaterialRippleLayout(Context context) { this(context, null, 0); } public MaterialRippleLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MaterialRippleLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setWillNotDraw(false); gestureDetector = new GestureDetector(context, longClickListener); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaterialRippleLayout); rippleColor = a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleColor, DEFAULT_COLOR); rippleDiameter = a.getDimensionPixelSize( R.styleable.MaterialRippleLayout_mrl_rippleDimension, (int) dpToPx(getResources(), DEFAULT_DIAMETER_DP) ); rippleOverlay = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleOverlay, DEFAULT_RIPPLE_OVERLAY); rippleHover = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleHover, DEFAULT_HOVER); rippleDuration = a.getInt(R.styleable.MaterialRippleLayout_mrl_rippleDuration, DEFAULT_DURATION); rippleAlpha = (int) (255 * a.getFloat(R.styleable.MaterialRippleLayout_mrl_rippleAlpha, DEFAULT_ALPHA)); rippleDelayClick = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleDelayClick, DEFAULT_DELAY_CLICK); rippleFadeDuration = a.getInteger(R.styleable.MaterialRippleLayout_mrl_rippleFadeDuration, DEFAULT_FADE_DURATION); rippleBackground = new ColorDrawable(a.getColor(R.styleable.MaterialRippleLayout_mrl_rippleBackground, DEFAULT_BACKGROUND)); ripplePersistent = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_ripplePersistent, DEFAULT_PERSISTENT); rippleInAdapter = a.getBoolean(R.styleable.MaterialRippleLayout_mrl_rippleInAdapter, DEFAULT_SEARCH_ADAPTER); rippleRoundedCorners = a.getDimensionPixelSize(R.styleable.MaterialRippleLayout_mrl_rippleRoundedCorners, DEFAULT_ROUNDED_CORNERS); a.recycle(); paint.setColor(rippleColor); paint.setAlpha(rippleAlpha); enableClipPathSupportIfNecessary(); } public static RippleBuilder on(View view) { return new RippleBuilder(view); } static float dpToPx(Resources resources, float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics()); } @SuppressWarnings("unchecked") public T getChildView() { return (T) childView; } @Override public final void addView(View child, int index, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("MaterialRippleLayout can host only one child"); } //noinspection unchecked childView = child; super.addView(child, index, params); } @Override public void setOnClickListener(OnClickListener onClickListener) { if (childView == null) { throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks"); } childView.setOnClickListener(onClickListener); } @Override public void setOnLongClickListener(OnLongClickListener onClickListener) { if (childView == null) { throw new IllegalStateException("MaterialRippleLayout must have a child view to handle clicks"); } childView.setOnLongClickListener(onClickListener); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return !findClickableViewInChild(childView, (int) event.getX(), (int) event.getY()); } @Override public boolean onTouchEvent(MotionEvent event) { boolean superOnTouchEvent = super.onTouchEvent(event); if (!isEnabled() || !childView.isEnabled()) return superOnTouchEvent; boolean isEventInBounds = bounds.contains((int) event.getX(), (int) event.getY()); if (isEventInBounds) { previousCoords.set(currentCoords.x, currentCoords.y); currentCoords.set((int) event.getX(), (int) event.getY()); } boolean gestureResult = gestureDetector.onTouchEvent(event); if (gestureResult || hasPerformedLongPress) { return true; } else { int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_UP: pendingClickEvent = new PerformClickEvent(); if (prepressed) { childView.setPressed(true); postDelayed( new Runnable() { @Override public void run() { childView.setPressed(false); } }, ViewConfiguration.getPressedStateDuration()); } if (isEventInBounds) { startRipple(pendingClickEvent); } else if (!rippleHover) { setRadius(0); } if (!rippleDelayClick && isEventInBounds) { pendingClickEvent.run(); } cancelPressedEvent(); break; case MotionEvent.ACTION_DOWN: setPositionInAdapter(); eventCancelled = false; pendingPressEvent = new PressedEvent(event); if (isInScrollingContainer()) { cancelPressedEvent(); prepressed = true; postDelayed(pendingPressEvent, ViewConfiguration.getTapTimeout()); } else { pendingPressEvent.run(); } break; case MotionEvent.ACTION_CANCEL: if (rippleInAdapter) { // dont use current coords in adapter since they tend to jump drastically on scroll currentCoords.set(previousCoords.x, previousCoords.y); previousCoords = new Point(); } childView.onTouchEvent(event); if (rippleHover) { if (!prepressed) { startRipple(null); } } else { childView.setPressed(false); } cancelPressedEvent(); break; case MotionEvent.ACTION_MOVE: if (rippleHover) { if (isEventInBounds && !eventCancelled) { invalidate(); } else if (!isEventInBounds) { startRipple(null); } } if (!isEventInBounds) { cancelPressedEvent(); if (hoverAnimator != null) { hoverAnimator.cancel(); } childView.onTouchEvent(event); eventCancelled = true; } break; } return true; } } private void cancelPressedEvent() { if (pendingPressEvent != null) { removeCallbacks(pendingPressEvent); prepressed = false; } } private void startHover() { if (eventCancelled) return; if (hoverAnimator != null) { hoverAnimator.cancel(); } final float radius = (float) (Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2)) * 1.2f); hoverAnimator = ObjectAnimator.ofFloat(this, radiusProperty, rippleDiameter, radius) .setDuration(HOVER_DURATION); hoverAnimator.setInterpolator(new LinearInterpolator()); hoverAnimator.start(); } private void startRipple(final Runnable animationEndRunnable) { if (eventCancelled) return; float endRadius = getEndRadius(); cancelAnimations(); rippleAnimator = new AnimatorSet(); rippleAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (!ripplePersistent) { setRadius(0); setRippleAlpha(rippleAlpha); } if (animationEndRunnable != null && rippleDelayClick) { animationEndRunnable.run(); } childView.setPressed(false); } }); ObjectAnimator ripple = ObjectAnimator.ofFloat(this, radiusProperty, radius, endRadius); ripple.setDuration(rippleDuration); ripple.setInterpolator(new DecelerateInterpolator()); ObjectAnimator fade = ObjectAnimator.ofInt(this, circleAlphaProperty, rippleAlpha, 0); fade.setDuration(rippleFadeDuration); fade.setInterpolator(new AccelerateInterpolator()); fade.setStartDelay(rippleDuration - rippleFadeDuration - FADE_EXTRA_DELAY); if (ripplePersistent) { rippleAnimator.play(ripple); } else if (getRadius() > endRadius) { fade.setStartDelay(0); rippleAnimator.play(fade); } else { rippleAnimator.playTogether(ripple, fade); } rippleAnimator.start(); } private void cancelAnimations() { if (rippleAnimator != null) { rippleAnimator.cancel(); rippleAnimator.removeAllListeners(); } if (hoverAnimator != null) { hoverAnimator.cancel(); } } private float getEndRadius() { final int width = getWidth(); final int height = getHeight(); final int halfWidth = width / 2; final int halfHeight = height / 2; final float radiusX = halfWidth > currentCoords.x ? width - currentCoords.x : currentCoords.x; final float radiusY = halfHeight > currentCoords.y ? height - currentCoords.y : currentCoords.y; return (float) Math.sqrt(Math.pow(radiusX, 2) + Math.pow(radiusY, 2)) * 1.2f; } private boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; } private AdapterView findParentAdapterView() { if (parentAdapter != null) { return parentAdapter; } ViewParent current = getParent(); while (true) { if (current instanceof AdapterView) { parentAdapter = (AdapterView) current; return parentAdapter; } else { try { current = current.getParent(); } catch (NullPointerException npe) { throw new RuntimeException("Could not find a parent AdapterView"); } } } } private void setPositionInAdapter() { if (rippleInAdapter) { positionInAdapter = findParentAdapterView().getPositionForView(MaterialRippleLayout.this); } } private boolean adapterPositionChanged() { if (rippleInAdapter) { int newPosition = findParentAdapterView().getPositionForView(MaterialRippleLayout.this); final boolean changed = newPosition != positionInAdapter; positionInAdapter = newPosition; if (changed) { cancelPressedEvent(); cancelAnimations(); childView.setPressed(false); setRadius(0); } return changed; } return false; } private boolean findClickableViewInChild(View view, int x, int y) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { View child = viewGroup.getChildAt(i); final Rect rect = new Rect(); child.getHitRect(rect); final boolean contains = rect.contains(x, y); if (contains) { return findClickableViewInChild(child, x - rect.left, y - rect.top); } } } else if (view != childView) { return (view.isEnabled() && (view.isClickable() || view.isLongClickable() || view.isFocusableInTouchMode())); } return view.isFocusableInTouchMode(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); bounds.set(0, 0, w, h); rippleBackground.setBounds(bounds); } @Override public boolean isInEditMode() { return true; } /* * Drawing */ @Override public void draw(Canvas canvas) { final boolean positionChanged = adapterPositionChanged(); if (rippleOverlay) { if (!positionChanged) { rippleBackground.draw(canvas); } super.draw(canvas); if (!positionChanged) { if (rippleRoundedCorners != 0) { Path clipPath = new Path(); RectF rect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight()); clipPath.addRoundRect(rect, rippleRoundedCorners, rippleRoundedCorners, Path.Direction.CW); canvas.clipPath(clipPath); } canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint); } } else { if (!positionChanged) { rippleBackground.draw(canvas); canvas.drawCircle(currentCoords.x, currentCoords.y, radius, paint); } super.draw(canvas); } } private float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; invalidate(); } public int getRippleAlpha() { return paint.getAlpha(); } public void setRippleAlpha(Integer rippleAlpha) { paint.setAlpha(rippleAlpha); invalidate(); } /* * Accessor */ public void setRippleColor(int rippleColor) { this.rippleColor = rippleColor; paint.setColor(rippleColor); paint.setAlpha(rippleAlpha); invalidate(); } public void setRippleOverlay(boolean rippleOverlay) { this.rippleOverlay = rippleOverlay; } public void setRippleDiameter(int rippleDiameter) { this.rippleDiameter = rippleDiameter; } public void setRippleDuration(int rippleDuration) { this.rippleDuration = rippleDuration; } public void setRippleBackground(int color) { rippleBackground = new ColorDrawable(color); rippleBackground.setBounds(bounds); invalidate(); } public void setRippleHover(boolean rippleHover) { this.rippleHover = rippleHover; } public void setRippleDelayClick(boolean rippleDelayClick) { this.rippleDelayClick = rippleDelayClick; } public void setRippleFadeDuration(int rippleFadeDuration) { this.rippleFadeDuration = rippleFadeDuration; } public void setRipplePersistent(boolean ripplePersistent) { this.ripplePersistent = ripplePersistent; } public void setRippleInAdapter(boolean rippleInAdapter) { this.rippleInAdapter = rippleInAdapter; } public void setRippleRoundedCorners(int rippleRoundedCorner) { this.rippleRoundedCorners = rippleRoundedCorner; enableClipPathSupportIfNecessary(); } public void setDefaultRippleAlpha(float alpha) { this.rippleAlpha = (int) (255 * alpha); paint.setAlpha(rippleAlpha); invalidate(); } public void performRipple() { currentCoords = new Point(getWidth() / 2, getHeight() / 2); startRipple(null); } public void performRipple(Point anchor) { currentCoords = new Point(anchor.x, anchor.y); startRipple(null); } /** * {@link Canvas#clipPath(Path)} is not supported in hardware accelerated layers * before API 18. Use software layer instead *

* https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported */ private void enableClipPathSupportIfNecessary() { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (rippleRoundedCorners != 0) { layerType = getLayerType(); setLayerType(LAYER_TYPE_SOFTWARE, null); } else { setLayerType(layerType, null); } } } public static class RippleBuilder { private final Context context; private final View child; private int rippleColor = DEFAULT_COLOR; private boolean rippleOverlay = DEFAULT_RIPPLE_OVERLAY; private boolean rippleHover = DEFAULT_HOVER; private float rippleDiameter = DEFAULT_DIAMETER_DP; private int rippleDuration = DEFAULT_DURATION; private float rippleAlpha = DEFAULT_ALPHA; private boolean rippleDelayClick = DEFAULT_DELAY_CLICK; private int rippleFadeDuration = DEFAULT_FADE_DURATION; private boolean ripplePersistent = DEFAULT_PERSISTENT; private int rippleBackground = DEFAULT_BACKGROUND; private boolean rippleSearchAdapter = DEFAULT_SEARCH_ADAPTER; private float rippleRoundedCorner = DEFAULT_ROUNDED_CORNERS; public RippleBuilder(View child) { this.child = child; this.context = child.getContext(); } public RippleBuilder rippleColor(int color) { this.rippleColor = color; return this; } public RippleBuilder rippleOverlay(boolean overlay) { this.rippleOverlay = overlay; return this; } public RippleBuilder rippleHover(boolean hover) { this.rippleHover = hover; return this; } public RippleBuilder rippleDiameterDp(int diameterDp) { this.rippleDiameter = diameterDp; return this; } public RippleBuilder rippleDuration(int duration) { this.rippleDuration = duration; return this; } public RippleBuilder rippleAlpha(float alpha) { this.rippleAlpha = alpha; return this; } public RippleBuilder rippleDelayClick(boolean delayClick) { this.rippleDelayClick = delayClick; return this; } public RippleBuilder rippleFadeDuration(int fadeDuration) { this.rippleFadeDuration = fadeDuration; return this; } public RippleBuilder ripplePersistent(boolean persistent) { this.ripplePersistent = persistent; return this; } public RippleBuilder rippleBackground(int color) { this.rippleBackground = color; return this; } public RippleBuilder rippleInAdapter(boolean inAdapter) { this.rippleSearchAdapter = inAdapter; return this; } public RippleBuilder rippleRoundedCorners(int radiusDp) { this.rippleRoundedCorner = radiusDp; return this; } public MaterialRippleLayout create() { MaterialRippleLayout layout = new MaterialRippleLayout(context); layout.setRippleColor(rippleColor); layout.setDefaultRippleAlpha(rippleAlpha); layout.setRippleDelayClick(rippleDelayClick); layout.setRippleDiameter((int) dpToPx(context.getResources(), rippleDiameter)); layout.setRippleDuration(rippleDuration); layout.setRippleFadeDuration(rippleFadeDuration); layout.setRippleHover(rippleHover); layout.setRipplePersistent(ripplePersistent); layout.setRippleOverlay(rippleOverlay); layout.setRippleBackground(rippleBackground); layout.setRippleInAdapter(rippleSearchAdapter); layout.setRippleRoundedCorners((int) dpToPx(context.getResources(), rippleRoundedCorner)); ViewGroup.LayoutParams params = child.getLayoutParams(); ViewGroup parent = (ViewGroup) child.getParent(); int index = 0; if (parent != null && parent instanceof MaterialRippleLayout) { throw new IllegalStateException("MaterialRippleLayout could not be created: parent of the view already is a MaterialRippleLayout"); } if (parent != null) { index = parent.indexOfChild(child); parent.removeView(child); } layout.addView(child, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); if (parent != null) { parent.addView(layout, index, params); } return layout; } } /* * Helper */ private class PerformClickEvent implements Runnable { @Override public void run() { if (hasPerformedLongPress) return; // if parent is an AdapterView, try to call its ItemClickListener if (getParent() instanceof AdapterView) { // try clicking direct child first if (!childView.performClick()) // if it did not handle it dispatch to adapterView clickAdapterView((AdapterView) getParent()); } else if (rippleInAdapter) { // find adapter view clickAdapterView(findParentAdapterView()); } else { // otherwise, just perform click on child childView.performClick(); } } private void clickAdapterView(AdapterView parent) { final int position = parent.getPositionForView(MaterialRippleLayout.this); final long itemId = parent.getAdapter() != null ? parent.getAdapter().getItemId(position) : 0; if (position != AdapterView.INVALID_POSITION) { parent.performItemClick(MaterialRippleLayout.this, position, itemId); } } } /* * Builder */ private final class PressedEvent implements Runnable { private final MotionEvent event; public PressedEvent(MotionEvent event) { this.event = event; } @Override public void run() { prepressed = false; childView.setLongClickable(false);//prevent the child's long click,let's the ripple layout call it's performLongClick childView.onTouchEvent(event); childView.setPressed(true); if (rippleHover) { startHover(); } } } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/RippleButton.java ================================================ package io.virtualapp.widgets; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.Shader; import android.os.Build; import android.support.v7.widget.AppCompatButton; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.animation.AccelerateDecelerateInterpolator; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.view.ViewHelper; import io.virtualapp.R; @SuppressLint("ClickableViewAccessibility") public class RippleButton extends AppCompatButton { private float mDownX; private float mDownY; private float mAlphaFactor; private float mDensity; private float mRadius; private float mMaxRadius; private int mRippleColor; private boolean mIsAnimating = false; private boolean mHover = true; private RadialGradient mRadialGradient; private Paint mPaint; private ObjectAnimator mRadiusAnimator; private boolean mAnimationIsCancel; private Rect mRect; private Path mPath = new Path(); public RippleButton(Context context) { this(context, null); } public RippleButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RippleButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RippleButton); mRippleColor = a.getColor(R.styleable.RippleButton_rippleColor, mRippleColor); mAlphaFactor = a.getFloat(R.styleable.RippleButton_alphaFactor, mAlphaFactor); mHover = a.getBoolean(R.styleable.RippleButton_hover, mHover); a.recycle(); } private int dp(int dp) { return (int) (dp * mDensity + 0.5f); } public void init() { mDensity = getContext().getResources().getDisplayMetrics().density; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setAlpha(100); setRippleColor(Color.BLACK, 0.2f); } public void setRippleColor(int rippleColor, float alphaFactor) { mRippleColor = rippleColor; mAlphaFactor = alphaFactor; } public void setHover(boolean enabled) { mHover = enabled; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mMaxRadius = (float) Math.sqrt(w * w + h * h); } @Override public boolean onTouchEvent(final MotionEvent event) { Log.d("TouchEvent", String.valueOf(event.getActionMasked())); Log.d("mIsAnimating", String.valueOf(mIsAnimating)); Log.d("mAnimationIsCancel", String.valueOf(mAnimationIsCancel)); boolean superResult = super.onTouchEvent(event); if (event.getActionMasked() == MotionEvent.ACTION_DOWN && this.isEnabled() && mHover) { mRect = new Rect(getLeft(), getTop(), getRight(), getBottom()); mAnimationIsCancel = false; mDownX = event.getX(); mDownY = event.getY(); mRadiusAnimator = ObjectAnimator.ofFloat(this, "radius", 0, dp(50)) .setDuration(400); mRadiusAnimator .setInterpolator(new AccelerateDecelerateInterpolator()); mRadiusAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { mIsAnimating = true; } @Override public void onAnimationEnd(Animator animator) { setRadius(0); ViewHelper.setAlpha(RippleButton.this, 1); mIsAnimating = false; } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); mRadiusAnimator.start(); if (!superResult) { return true; } } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE && this.isEnabled() && mHover) { mDownX = event.getX(); mDownY = event.getY(); // Cancel the ripple animation when moved outside if (mAnimationIsCancel = !mRect.contains( getLeft() + (int) event.getX(), getTop() + (int) event.getY())) { setRadius(0); } else { setRadius(dp(50)); } if (!superResult) { return true; } } else if (event.getActionMasked() == MotionEvent.ACTION_UP && !mAnimationIsCancel && this.isEnabled()) { mDownX = event.getX(); mDownY = event.getY(); final float tempRadius = (float) Math.sqrt(mDownX * mDownX + mDownY * mDownY); float targetRadius = Math.max(tempRadius, mMaxRadius); if (mIsAnimating) { mRadiusAnimator.cancel(); } mRadiusAnimator = ObjectAnimator.ofFloat(this, "radius", dp(50), targetRadius); mRadiusAnimator.setDuration(500); mRadiusAnimator .setInterpolator(new AccelerateDecelerateInterpolator()); mRadiusAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { mIsAnimating = true; } @Override public void onAnimationEnd(Animator animator) { setRadius(0); ViewHelper.setAlpha(RippleButton.this, 1); mIsAnimating = false; } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); mRadiusAnimator.start(); if (!superResult) { return true; } } return superResult; } public int adjustAlpha(int color, float factor) { int alpha = Math.round(Color.alpha(color) * factor); int red = Color.red(color); int green = Color.green(color); int blue = Color.blue(color); return Color.argb(alpha, red, green, blue); } public void setRadius(final float radius) { mRadius = radius; if (mRadius > 0) { mRadialGradient = new RadialGradient(mDownX, mDownY, mRadius, adjustAlpha(mRippleColor, mAlphaFactor), mRippleColor, Shader.TileMode.MIRROR); mPaint.setShader(mRadialGradient); } invalidate(); } @Override protected void onDraw(final Canvas canvas) { super.onDraw(canvas); if (isInEditMode()) { return; } canvas.save(Canvas.CLIP_SAVE_FLAG); mPath.reset(); mPath.addCircle(mDownX, mDownY, mRadius, Path.Direction.CW); canvas.clipPath(mPath); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) canvas.restore(); canvas.drawCircle(mDownX, mDownY, mRadius, mPaint); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/ShadowProperty.java ================================================ package io.virtualapp.widgets; public class ShadowProperty { public static final int ALL = 0x1111; public static final int LEFT = 0x0001; public static final int TOP = 0x0010; public static final int RIGHT = 0x0100; public static final int BOTTOM = 0x1000; /** * 阴影颜色 */ private int shadowColor; /** * 阴影半径 */ private int shadowRadius; /** * 阴影x偏移 */ private int shadowDx; /** * 阴影y偏移 */ private int shadowDy; /** * 阴影边 */ private int shadowSide = ALL; public int getShadowSide() { return shadowSide; } public ShadowProperty setShadowSide(int shadowSide) { this.shadowSide = shadowSide; return this; } public int getShadowOffset() { return getShadowOffsetHalf() * 2; } public int getShadowOffsetHalf() { return 0 >= shadowRadius ? 0 : Math.max(shadowDx, shadowDy) + shadowRadius; } public int getShadowColor() { return shadowColor; } public ShadowProperty setShadowColor(int shadowColor) { this.shadowColor = shadowColor; return this; } public int getShadowRadius() { return shadowRadius; } public ShadowProperty setShadowRadius(int shadowRadius) { this.shadowRadius = shadowRadius; return this; } public int getShadowDx() { return shadowDx; } public ShadowProperty setShadowDx(int shadowDx) { this.shadowDx = shadowDx; return this; } public int getShadowDy() { return shadowDy; } public ShadowProperty setShadowDy(int shadowDy) { this.shadowDy = shadowDy; return this; } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/ShadowViewDrawable.java ================================================ package io.virtualapp.widgets; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; public class ShadowViewDrawable extends Drawable { private Paint paint; private RectF bounds = new RectF(); private int width; private int height; private ShadowProperty shadowProperty; private int shadowOffset; private RectF drawRect; private float rx; private float ry; private PorterDuffXfermode srcOut = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT); public ShadowViewDrawable(ShadowProperty shadowProperty, int color, float rx, float ry) { this.shadowProperty = shadowProperty; shadowOffset = this.shadowProperty.getShadowOffset(); this.rx = rx; this.ry = ry; paint = new Paint(); paint.setAntiAlias(true); /** * 解决旋转时的锯齿问题 */ paint.setFilterBitmap(true); paint.setDither(true); paint.setStyle(Paint.Style.FILL); paint.setColor(color); /** * 设置阴影 */ paint.setShadowLayer(shadowProperty.getShadowRadius(), shadowProperty.getShadowDx(), shadowProperty.getShadowDy(), shadowProperty.getShadowColor()); drawRect = new RectF(); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); if (bounds.right - bounds.left > 0 && bounds.bottom - bounds.top > 0) { this.bounds.left = bounds.left; this.bounds.right = bounds.right; this.bounds.top = bounds.top; this.bounds.bottom = bounds.bottom; width = (int) (this.bounds.right - this.bounds.left); height = (int) (this.bounds.bottom - this.bounds.top); int shadowSide = shadowProperty.getShadowSide(); int left = (shadowSide & ShadowProperty.LEFT) == ShadowProperty.LEFT ? shadowOffset : 0; int top = (shadowSide & ShadowProperty.TOP) == ShadowProperty.TOP ? shadowOffset : 0; int right = width - ((shadowSide & ShadowProperty.RIGHT) == ShadowProperty.RIGHT ? shadowOffset : 0); int bottom = height - ((shadowSide & ShadowProperty.BOTTOM) == ShadowProperty.BOTTOM ? shadowOffset : 0); drawRect = new RectF(left, top, right, bottom); invalidateSelf(); } } @Override public void draw(@NonNull Canvas canvas) { paint.setXfermode(null); canvas.drawRoundRect( drawRect, rx, ry, paint ); paint.setXfermode(srcOut); canvas.drawRoundRect(drawRect, rx, ry, paint); } public ShadowViewDrawable setColor(int color) { paint.setColor(color); return this; } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter cf) { } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/Shimmer.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.os.Build; import android.view.View; public class Shimmer { public static final int ANIMATION_DIRECTION_LTR = 0; public static final int ANIMATION_DIRECTION_RTL = 1; private static final int DEFAULT_REPEAT_COUNT = ValueAnimator.INFINITE; private static final long DEFAULT_DURATION = 1000; private static final long DEFAULT_START_DELAY = 0; private static final int DEFAULT_DIRECTION = ANIMATION_DIRECTION_LTR; private int repeatCount; private long duration; private long startDelay; private int direction; private Animator.AnimatorListener animatorListener; private ObjectAnimator animator; public Shimmer() { repeatCount = DEFAULT_REPEAT_COUNT; duration = DEFAULT_DURATION; startDelay = DEFAULT_START_DELAY; direction = DEFAULT_DIRECTION; } public int getRepeatCount() { return repeatCount; } public Shimmer setRepeatCount(int repeatCount) { this.repeatCount = repeatCount; return this; } public long getDuration() { return duration; } public Shimmer setDuration(long duration) { this.duration = duration; return this; } public long getStartDelay() { return startDelay; } public Shimmer setStartDelay(long startDelay) { this.startDelay = startDelay; return this; } public int getDirection() { return direction; } public Shimmer setDirection(int direction) { if (direction != ANIMATION_DIRECTION_LTR && direction != ANIMATION_DIRECTION_RTL) { throw new IllegalArgumentException("The animation direction must be either ANIMATION_DIRECTION_LTR or ANIMATION_DIRECTION_RTL"); } this.direction = direction; return this; } public Animator.AnimatorListener getAnimatorListener() { return animatorListener; } public Shimmer setAnimatorListener(Animator.AnimatorListener animatorListener) { this.animatorListener = animatorListener; return this; } public void start(final V shimmerView) { if (isAnimating()) { return; } final Runnable animate = new Runnable() { @Override public void run() { shimmerView.setShimmering(true); float fromX = 0; float toX = shimmerView.getWidth(); if (direction == ANIMATION_DIRECTION_RTL) { fromX = shimmerView.getWidth(); toX = 0; } animator = ObjectAnimator.ofFloat(shimmerView, "gradientX", fromX, toX); animator.setRepeatCount(repeatCount); animator.setDuration(duration); animator.setStartDelay(startDelay); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { shimmerView.setShimmering(false); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { shimmerView.postInvalidate(); } else { shimmerView.postInvalidateOnAnimation(); } animator = null; } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); if (animatorListener != null) { animator.addListener(animatorListener); } animator.start(); } }; if (!shimmerView.isSetUp()) { shimmerView.setAnimationSetupCallback(new ShimmerViewHelper.AnimationSetupCallback() { @Override public void onSetupAnimation(final View target) { animate.run(); } }); } else { animate.run(); } } public void cancel() { if (animator != null) { animator.cancel(); } } public boolean isAnimating() { return animator != null && animator.isRunning(); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/ShimmerViewBase.java ================================================ package io.virtualapp.widgets; public interface ShimmerViewBase { float getGradientX(); void setGradientX(float gradientX); boolean isShimmering(); void setShimmering(boolean isShimmering); boolean isSetUp(); void setAnimationSetupCallback(ShimmerViewHelper.AnimationSetupCallback callback); int getPrimaryColor(); void setPrimaryColor(int primaryColor); int getReflectionColor(); void setReflectionColor(int reflectionColor); } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/ShimmerViewHelper.java ================================================ package io.virtualapp.widgets; import android.content.res.TypedArray; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import io.virtualapp.R; public class ShimmerViewHelper { private static final int DEFAULT_REFLECTION_COLOR = 0xFFFFFFFF; private View view; private Paint paint; // center position of the gradient private float gradientX; // shader applied on the text view // only null until the first global layout private LinearGradient linearGradient; // shader's local matrix // never null private Matrix linearGradientMatrix; private int primaryColor; // shimmer reflection color private int reflectionColor; // true when animating private boolean isShimmering; // true after first global layout private boolean isSetUp; // callback called after first global layout private AnimationSetupCallback callback; public ShimmerViewHelper(View view, Paint paint, AttributeSet attributeSet) { this.view = view; this.paint = paint; init(attributeSet); } public float getGradientX() { return gradientX; } public void setGradientX(float gradientX) { this.gradientX = gradientX; view.invalidate(); } public boolean isShimmering() { return isShimmering; } public void setShimmering(boolean isShimmering) { this.isShimmering = isShimmering; } public boolean isSetUp() { return isSetUp; } public void setAnimationSetupCallback(AnimationSetupCallback callback) { this.callback = callback; } public int getPrimaryColor() { return primaryColor; } public void setPrimaryColor(int primaryColor) { this.primaryColor = primaryColor; if (isSetUp) { resetLinearGradient(); } } public int getReflectionColor() { return reflectionColor; } public void setReflectionColor(int reflectionColor) { this.reflectionColor = reflectionColor; if (isSetUp) { resetLinearGradient(); } } private void init(AttributeSet attributeSet) { reflectionColor = DEFAULT_REFLECTION_COLOR; if (attributeSet != null) { TypedArray a = view.getContext().obtainStyledAttributes(attributeSet, R.styleable.ShimmerView, 0, 0); if (a != null) { try { reflectionColor = a.getColor(R.styleable.ShimmerView_reflectionColor, DEFAULT_REFLECTION_COLOR); } catch (Exception e) { android.util.Log.e("ShimmerTextView", "Error while creating the view:", e); } finally { a.recycle(); } } } linearGradientMatrix = new Matrix(); } private void resetLinearGradient() { // our gradient is a simple linear gradient from textColor to reflectionColor. its axis is at the center // when it's outside of the view, the outer color (textColor) will be repeated (Shader.TileMode.CLAMP) // initially, the linear gradient is positioned on the left side of the view linearGradient = new LinearGradient(-view.getWidth(), 0, 0, 0, new int[]{ primaryColor, reflectionColor, primaryColor, }, new float[]{ 0, 0.5f, 1 }, Shader.TileMode.CLAMP ); paint.setShader(linearGradient); } protected void onSizeChanged() { resetLinearGradient(); if (!isSetUp) { isSetUp = true; if (callback != null) { callback.onSetupAnimation(view); } } } /** * content of the wrapping view's onDraw(Canvas) * MUST BE CALLED BEFORE SUPER STATEMENT */ public void onDraw() { // only draw the shader gradient over the text while animating if (isShimmering) { // first onDraw() when shimmering if (paint.getShader() == null) { paint.setShader(linearGradient); } // translate the shader local matrix linearGradientMatrix.setTranslate(2 * gradientX, 0); // this is required in order to invalidate the shader's position linearGradient.setLocalMatrix(linearGradientMatrix); } else { // we're not animating, remove the shader from the paint paint.setShader(null); } } public interface AnimationSetupCallback { void onSetupAnimation(View target); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/TwoGearsView.java ================================================ package io.virtualapp.widgets; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; public class TwoGearsView extends BaseView { ValueAnimator valueAnimator = null; float mAnimatedValue = 0f; float hypotenuse = 0f; float smallRingCenterX = 0f; float smallRingCenterY = 0f; float bigRingCenterX = 0f; float bigRingCenterY = 0f; private float mWidth = 0f; private Paint mPaint, mPaintAxle; private Paint mPaintRing; private float mPadding = 0f; private float mWheelLength; private int mWheelSmallSpace = 10; private int mWheelBigSpace = 8; public TwoGearsView(Context context) { super(context); } public TwoGearsView(Context context, AttributeSet attrs) { super(context, attrs); } public TwoGearsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getMeasuredWidth() > getHeight()) mWidth = getMeasuredHeight(); else mWidth = getMeasuredWidth(); } private void drawSmallRing(Canvas canvas) { hypotenuse = (float) (mWidth * Math.sqrt(2)); smallRingCenterX = (float) ((hypotenuse / 6.f) * Math.cos(45 * Math.PI / 180f)); smallRingCenterY = (float) ((hypotenuse / 6.f) * Math.sin(45 * Math.PI / 180f)); mPaintRing.setStrokeWidth(dip2px(1.0f)); canvas.drawCircle(mPadding + smallRingCenterX, smallRingCenterY + mPadding, smallRingCenterX, mPaintRing); mPaintRing.setStrokeWidth(dip2px(1.5f)); canvas.drawCircle(mPadding + smallRingCenterX, smallRingCenterY + mPadding, smallRingCenterX / 2, mPaintRing); } private void drawSmallGear(Canvas canvas) { mPaint.setStrokeWidth(dip2px(1)); for (int i = 0; i < 360; i = i + mWheelSmallSpace) { int angle = (int) (mAnimatedValue * mWheelSmallSpace + i); float x3 = (float) ((smallRingCenterX) * Math.cos(angle * Math.PI / 180f)); float y3 = (float) ((smallRingCenterY) * Math.sin(angle * Math.PI / 180f)); float x4 = (float) ((smallRingCenterX + mWheelLength) * Math.cos(angle * Math.PI / 180f)); float y4 = (float) ((smallRingCenterY + mWheelLength) * Math.sin(angle * Math.PI / 180f)); canvas.drawLine(mPadding + smallRingCenterX - x4, smallRingCenterY + mPadding - y4, smallRingCenterX + mPadding - x3, smallRingCenterY + mPadding - y3, mPaint); } } private void drawBigGear(Canvas canvas) { bigRingCenterX = (float) ((hypotenuse / 2.f) * Math.cos(45 * Math.PI / 180f)); bigRingCenterY = (float) ((hypotenuse / 2.f) * Math.sin(45 * Math.PI / 180f)); float strokeWidth = dip2px(1.5f) / 4; mPaint.setStrokeWidth(dip2px(1.5f)); for (int i = 0; i < 360; i = i + mWheelBigSpace) { int angle = (int) (360 - (mAnimatedValue * mWheelBigSpace + i)); float x3 = (float) ((bigRingCenterX - smallRingCenterX) * Math.cos(angle * Math.PI / 180f)); float y3 = (float) ((bigRingCenterY - smallRingCenterY) * Math.sin(angle * Math.PI / 180f)); float x4 = (float) ((bigRingCenterX - smallRingCenterX + mWheelLength) * Math.cos(angle * Math.PI / 180f)); float y4 = (float) ((bigRingCenterY - smallRingCenterY + mWheelLength) * Math.sin(angle * Math.PI / 180f)); canvas.drawLine(bigRingCenterX + mPadding - x4 + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding - y4 + mWheelLength * 2 + strokeWidth, bigRingCenterX + mPadding - x3 + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding - y3 + mWheelLength * 2 + strokeWidth, mPaint); } } private void drawBigRing(Canvas canvas) { float strokeWidth = dip2px(1.5f) / 4; mPaintRing.setStrokeWidth(dip2px(1.5f)); canvas.drawCircle(bigRingCenterX + mPadding + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding + mWheelLength * 2 + strokeWidth, bigRingCenterX - smallRingCenterX - strokeWidth, mPaintRing); mPaintRing.setStrokeWidth(dip2px(1.5f)); canvas.drawCircle(bigRingCenterX + mPadding + mWheelLength * 2 + strokeWidth, bigRingCenterY + mPadding + mWheelLength * 2 + strokeWidth, (bigRingCenterX - smallRingCenterX) / 2 - strokeWidth, mPaintRing); } private void drawAxle(Canvas canvas) { for (int i = 0; i < 3; i++) { float x3 = (float) ((smallRingCenterX) * Math.cos(i * (360 / 3) * Math.PI / 180f)); float y3 = (float) ((smallRingCenterY) * Math.sin(i * (360 / 3) * Math.PI / 180f)); canvas.drawLine(mPadding + smallRingCenterX, mPadding + smallRingCenterY, mPadding + smallRingCenterX - x3, mPadding + smallRingCenterY - y3, mPaintAxle); } for (int i = 0; i < 3; i++) { float x3 = (float) ((bigRingCenterX - smallRingCenterX) * Math.cos(i * (360 / 3) * Math.PI / 180f)); float y3 = (float) ((bigRingCenterY - smallRingCenterY) * Math.sin(i * (360 / 3) * Math.PI / 180f)); canvas.drawLine(bigRingCenterX + mPadding + mWheelLength * 2, bigRingCenterY + mPadding + mWheelLength * 2, bigRingCenterX + mPadding + mWheelLength * 2 - x3, bigRingCenterY + mPadding + mWheelLength * 2 - y3, mPaintAxle); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPadding = dip2px(5); canvas.save(); canvas.rotate(180, mWidth / 2, mWidth / 2); drawSmallRing(canvas); drawSmallGear(canvas); drawBigGear(canvas); drawBigRing(canvas); drawAxle(canvas); canvas.restore(); } private void initPaint() { mPaintRing = new Paint(); mPaintRing.setAntiAlias(true); mPaintRing.setStyle(Paint.Style.STROKE); mPaintRing.setColor(Color.WHITE); mPaintRing.setStrokeWidth(dip2px(1.5f)); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setColor(Color.WHITE); mPaint.setStrokeWidth(dip2px(1)); mPaintAxle = new Paint(); mPaintAxle.setAntiAlias(true); mPaintAxle.setStyle(Paint.Style.FILL); mPaintAxle.setColor(Color.WHITE); mPaintAxle.setStrokeWidth(dip2px(1.5f)); mWheelLength = dip2px(2f); } public void setViewColor(int color) { mPaint.setColor(color); mPaintAxle.setColor(color); mPaintRing.setColor(color); postInvalidate(); } @Override protected void InitPaint() { initPaint(); } @Override protected void OnAnimationUpdate(ValueAnimator valueAnimator) { mAnimatedValue = (float) valueAnimator.getAnimatedValue(); postInvalidate(); } @Override protected void OnAnimationRepeat(Animator animation) { } @Override protected int OnStopAnim() { postInvalidate(); return 1; } @Override protected int SetAnimRepeatMode() { return ValueAnimator.RESTART; } @Override protected void AnimIsRunning() { } @Override protected int SetAnimRepeatCount() { return ValueAnimator.INFINITE; } private int dip2px(float dpValue) { final float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } } ================================================ FILE: app/src/main/java/io/virtualapp/widgets/ViewHelper.java ================================================ package io.virtualapp.widgets; import io.virtualapp.VApp; /** * @author Lody */ public class ViewHelper { public static int dip2px(float dpValue) { final float scale = VApp.getApp().getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } } ================================================ FILE: app/src/main/res/drawable/blue_circle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/fab_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/home_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/icon_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/sel_clone_app_btn.xml ================================================ ================================================ FILE: app/src/main/res/drawable/sel_guide_btn.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_clone_app_btn.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_clone_app_btn_pressed.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_clone_app.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_home.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_install.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_loading.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_splash.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_users.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_list_app.xml ================================================