Repository: AndyJennifer/SimpleEyes Branch: master Commit: 6334fcbc2496 Files: 313 Total size: 655.3 KB Directory structure: gitextract_epk5pk4i/ ├── .circleci/ │ └── config.yml ├── .gitignore ├── .idea/ │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── dictionaries/ │ │ └── andy.xml │ ├── encodings.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── jennifer/ │ │ └── andy/ │ │ └── simpleeyes/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ └── fonts/ │ │ │ └── Lobster-1.4.otf │ │ ├── java/ │ │ │ └── com/ │ │ │ └── jennifer/ │ │ │ └── andy/ │ │ │ └── simpleeyes/ │ │ │ ├── AndyApplication.kt │ │ │ ├── UserPreferences.kt │ │ │ ├── entity/ │ │ │ │ └── AndyInfo.kt │ │ │ ├── manager/ │ │ │ │ └── ActivityManager.kt │ │ │ ├── net/ │ │ │ │ ├── Api.kt │ │ │ │ ├── ApiService.kt │ │ │ │ ├── Extras.kt │ │ │ │ └── RetrofitConfig.kt │ │ │ ├── player/ │ │ │ │ ├── FileMediaDataSource.kt │ │ │ │ ├── IjkMediaController.kt │ │ │ │ ├── IjkVideoView.kt │ │ │ │ ├── IjkVideoViewWrapper.kt │ │ │ │ ├── MeasureHelper.kt │ │ │ │ ├── PolicyCompat.kt │ │ │ │ ├── event/ │ │ │ │ │ └── VideoProgressEvent.kt │ │ │ │ ├── render/ │ │ │ │ │ ├── IRenderView.kt │ │ │ │ │ ├── SurfaceRenderView.kt │ │ │ │ │ └── TextureRenderView.kt │ │ │ │ └── view/ │ │ │ │ ├── ControllerView.kt │ │ │ │ ├── ControllerViewFactory.kt │ │ │ │ ├── ErrorView.kt │ │ │ │ ├── FullScreenControllerView.kt │ │ │ │ └── TinyControllerView.kt │ │ │ ├── router/ │ │ │ │ ├── EyesPathReplaceService.kt │ │ │ │ ├── RouteIntercept.kt │ │ │ │ └── SchemeFilterActivity.kt │ │ │ ├── rx/ │ │ │ │ ├── RxBus.kt │ │ │ │ ├── RxThreadHelper.kt │ │ │ │ └── error/ │ │ │ │ ├── FlowableRetryDelay.kt │ │ │ │ ├── GlobalErrorProcessor.kt │ │ │ │ ├── GlobalErrorTransformer.kt │ │ │ │ ├── ObservabeRetryDelay.kt │ │ │ │ └── RetryConfig.kt │ │ │ ├── ui/ │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── author/ │ │ │ │ │ ├── AuthorTagDetailActivity.kt │ │ │ │ │ ├── model/ │ │ │ │ │ │ └── AuthorModel.kt │ │ │ │ │ ├── presenter/ │ │ │ │ │ │ └── AuthorTagDetailPresenter.kt │ │ │ │ │ └── ui/ │ │ │ │ │ └── AuthorTagDetailView.kt │ │ │ │ ├── base/ │ │ │ │ │ ├── BaseActivity.kt │ │ │ │ │ ├── BaseAppCompatActivity.kt │ │ │ │ │ ├── BaseAppCompatFragment.kt │ │ │ │ │ ├── BaseFragment.kt │ │ │ │ │ ├── BaseFragmentItemAdapter.kt │ │ │ │ │ ├── BaseView.kt │ │ │ │ │ ├── LoadMoreView.kt │ │ │ │ │ ├── adapter/ │ │ │ │ │ │ └── BaseDataAdapter.kt │ │ │ │ │ ├── model/ │ │ │ │ │ │ └── BaseModel.kt │ │ │ │ │ └── presenter/ │ │ │ │ │ ├── BasePresenter.kt │ │ │ │ │ └── LoadMorePresenter.kt │ │ │ │ ├── common/ │ │ │ │ │ ├── CommonModel.kt │ │ │ │ │ ├── CommonPresenter.kt │ │ │ │ │ ├── CommonRecyclerActivity.kt │ │ │ │ │ └── CommonView.kt │ │ │ │ ├── feed/ │ │ │ │ │ ├── AllCategoryActivity.kt │ │ │ │ │ ├── CategoryTabActivity.kt │ │ │ │ │ ├── FeedFragment.kt │ │ │ │ │ ├── RankListActivity.kt │ │ │ │ │ ├── TagActivity.kt │ │ │ │ │ ├── TagDetailInfoFragment.kt │ │ │ │ │ ├── TopicActivity.kt │ │ │ │ │ ├── WebViewActivity.kt │ │ │ │ │ ├── model/ │ │ │ │ │ │ └── FeedModel.kt │ │ │ │ │ ├── presenter/ │ │ │ │ │ │ ├── AllCategoryPresenter.kt │ │ │ │ │ │ ├── CategoryTabPresenter.kt │ │ │ │ │ │ ├── FeedPresenter.kt │ │ │ │ │ │ ├── RankListPresenter.kt │ │ │ │ │ │ ├── TagDetailInfoPresenter.kt │ │ │ │ │ │ └── TopicPresenter.kt │ │ │ │ │ └── view/ │ │ │ │ │ ├── AllCategoryView.kt │ │ │ │ │ ├── CategoryTabView.kt │ │ │ │ │ ├── FeedView.kt │ │ │ │ │ ├── RankListView.kt │ │ │ │ │ ├── TagDetailInfoView.kt │ │ │ │ │ └── TopicView.kt │ │ │ │ ├── follow/ │ │ │ │ │ ├── AllAuthorActivity.kt │ │ │ │ │ ├── FollowFragment.kt │ │ │ │ │ ├── model/ │ │ │ │ │ │ └── FollowModel.kt │ │ │ │ │ ├── presenter/ │ │ │ │ │ │ ├── AllAuthorPresenter.kt │ │ │ │ │ │ └── FollowPresenter.kt │ │ │ │ │ └── view/ │ │ │ │ │ ├── AllAuthorView.kt │ │ │ │ │ └── FollowView.kt │ │ │ │ ├── home/ │ │ │ │ │ ├── DailyEliteActivity.kt │ │ │ │ │ ├── HomeFragment.kt │ │ │ │ │ ├── adapter/ │ │ │ │ │ │ ├── CollectionCardCoverAdapter.kt │ │ │ │ │ │ ├── DailyEliteAdapter.kt │ │ │ │ │ │ └── SquareCollectionAdapter.kt │ │ │ │ │ ├── model/ │ │ │ │ │ │ └── HomeModel.kt │ │ │ │ │ ├── presenter/ │ │ │ │ │ │ ├── DailyElitePresenter.kt │ │ │ │ │ │ └── HomePresenter.kt │ │ │ │ │ └── view/ │ │ │ │ │ ├── DailyEliteView.kt │ │ │ │ │ └── HomeView.kt │ │ │ │ ├── login/ │ │ │ │ │ └── LoginActivity.kt │ │ │ │ ├── profile/ │ │ │ │ │ ├── CacheFragment.kt │ │ │ │ │ ├── ProfileFragment.kt │ │ │ │ │ ├── adapter/ │ │ │ │ │ │ └── ProfileSettingAdapter.kt │ │ │ │ │ ├── model/ │ │ │ │ │ │ └── ProfileModel.kt │ │ │ │ │ ├── presenter/ │ │ │ │ │ │ ├── CachePresenter.kt │ │ │ │ │ │ └── ProfilePresenter.kt │ │ │ │ │ └── view/ │ │ │ │ │ ├── CacheView.kt │ │ │ │ │ └── ProfileView.kt │ │ │ │ ├── search/ │ │ │ │ │ ├── SearchHotActivity.kt │ │ │ │ │ ├── adapter/ │ │ │ │ │ │ ├── CollectionBriefAdapter.kt │ │ │ │ │ │ ├── SearchHotAdapter.kt │ │ │ │ │ │ └── SearchVideoAdapter.kt │ │ │ │ │ ├── presenter/ │ │ │ │ │ │ └── SearchPresenter.kt │ │ │ │ │ └── view/ │ │ │ │ │ └── SearchView.kt │ │ │ │ ├── splash/ │ │ │ │ │ ├── LandingActivity.kt │ │ │ │ │ ├── LocalCommonLandingFragment.kt │ │ │ │ │ ├── SloganFragment.kt │ │ │ │ │ ├── VideoLandingFragment.kt │ │ │ │ │ └── adapter/ │ │ │ │ │ └── SplashVideoFragmentAdapter.kt │ │ │ │ └── video/ │ │ │ │ ├── VideoDetailActivity.kt │ │ │ │ ├── VideoInfoByIdActivity.kt │ │ │ │ ├── adapter/ │ │ │ │ │ └── VideoDetailAdapter.kt │ │ │ │ ├── model/ │ │ │ │ │ └── VideoDetailModel.kt │ │ │ │ ├── presenter/ │ │ │ │ │ ├── VideoDetailPresenter.kt │ │ │ │ │ └── VideoInfoByIdPresenter.kt │ │ │ │ └── view/ │ │ │ │ ├── VideoDetailView.kt │ │ │ │ └── VideoInfoByIdView.kt │ │ │ ├── utils/ │ │ │ │ ├── AppUtils.kt │ │ │ │ ├── ButterKnife.kt │ │ │ │ ├── DensityUtils.kt │ │ │ │ ├── IntentRouterUtils.kt │ │ │ │ ├── KeyboardUtils.kt │ │ │ │ ├── NetWorkUtils.kt │ │ │ │ ├── ProcessUtils.kt │ │ │ │ ├── ScreenUtils.kt │ │ │ │ ├── SystemUtils.kt │ │ │ │ ├── TimeUtils.kt │ │ │ │ ├── ToastUtils.kt │ │ │ │ ├── UDIDUtils.kt │ │ │ │ ├── VideoPlayerUtils.kt │ │ │ │ └── ext/ │ │ │ │ └── IntentEx.kt │ │ │ └── widget/ │ │ │ ├── BottomBar.kt │ │ │ ├── BottomItem.kt │ │ │ ├── BottomItemLayout.kt │ │ │ ├── CollectionOfHorizontalScrollCardView.kt │ │ │ ├── CustomLoadMoreView.kt │ │ │ ├── EliteImageView.kt │ │ │ ├── FullScreenVideoView.kt │ │ │ ├── GridItemDecoration.kt │ │ │ ├── ItemHeaderView.kt │ │ │ ├── SearchHotRemindView.kt │ │ │ ├── StickyNavLayout.kt │ │ │ ├── VerticalProgressBar.kt │ │ │ ├── VideoDetailAuthorView.kt │ │ │ ├── font/ │ │ │ │ ├── CustomFontTextView.kt │ │ │ │ ├── CustomFontTypeWriterTextView.kt │ │ │ │ ├── FontType.kt │ │ │ │ ├── PrintSpan.kt │ │ │ │ ├── PrintSpanGroup.kt │ │ │ │ └── TypefaceManager.kt │ │ │ ├── image/ │ │ │ │ ├── CenterAlignImageSpan.kt │ │ │ │ └── imageloader/ │ │ │ │ └── FrescoImageLoader.kt │ │ │ ├── pull/ │ │ │ │ ├── head/ │ │ │ │ │ ├── EliteHeaderView.kt │ │ │ │ │ ├── HeaderRefreshView.kt │ │ │ │ │ ├── HomePageHeaderView.kt │ │ │ │ │ └── VideoDetailHeadView.kt │ │ │ │ ├── refresh/ │ │ │ │ │ ├── LinearLayoutManagerWithSmoothScroller.kt │ │ │ │ │ ├── PullRefreshView.kt │ │ │ │ │ ├── PullToRefresh.kt │ │ │ │ │ ├── PullToRefreshBase.kt │ │ │ │ │ └── PullToRefreshRecyclerView.kt │ │ │ │ └── zoom/ │ │ │ │ ├── PullToZoom.kt │ │ │ │ ├── PullToZoomBase.kt │ │ │ │ └── PullToZoomRecyclerView.kt │ │ │ ├── state/ │ │ │ │ ├── MultipleStateView.kt │ │ │ │ └── NetLoadingView.kt │ │ │ ├── tab/ │ │ │ │ ├── AnimationUtils.java │ │ │ │ ├── ShortTabLayout.java │ │ │ │ └── TabItem.java │ │ │ └── viewpager/ │ │ │ ├── InterceptVerticalViewPager.kt │ │ │ └── MarginWithIndicatorViewPager.kt │ │ └── res/ │ │ ├── anim/ │ │ │ ├── bottom_in.xml │ │ │ ├── bottom_out.xml │ │ │ ├── fade_in.xml │ │ │ ├── fade_out.xml │ │ │ ├── left_in.xml │ │ │ ├── left_out.xml │ │ │ ├── no_anim.xml │ │ │ ├── right_in.xml │ │ │ ├── right_out.xml │ │ │ ├── scale_in.xml │ │ │ ├── scale_out.xml │ │ │ ├── top_in.xml │ │ │ └── top_out.xml │ │ ├── color/ │ │ │ └── selector_item_square_text.xml │ │ ├── drawable/ │ │ │ ├── ic_launcher_background.xml │ │ │ ├── seek_bar_layer.xml │ │ │ ├── selector_checkbox_bg.xml │ │ │ ├── selector_item_square_foreground.xml │ │ │ ├── shape_black_border.xml │ │ │ ├── shape_border_bottom.xml │ │ │ ├── shape_border_bottom_top.xml │ │ │ ├── shape_hot_search_bg.xml │ │ │ ├── shape_indicator_selected.xml │ │ │ ├── shape_indicator_unselected.xml │ │ │ ├── shape_share_bg.xml │ │ │ ├── shape_show_all_border.xml │ │ │ ├── shape_translate_border.xml │ │ │ ├── shape_video_detail_placeholder.xml │ │ │ ├── shape_webview_scrollbar.xml │ │ │ └── shape_white_border.xml │ │ ├── layout/ │ │ │ ├── activity_all_author.xml │ │ │ ├── activity_all_category.xml │ │ │ ├── activity_author_tag_detail.xml │ │ │ ├── activity_category_tab.xml │ │ │ ├── activity_common_recyclerview.xml │ │ │ ├── activity_daily_elite.xml │ │ │ ├── activity_landing.xml │ │ │ ├── activity_login.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_rank.xml │ │ │ ├── activity_tag.xml │ │ │ ├── activity_topic.xml │ │ │ ├── activity_video_detail.xml │ │ │ ├── activity_videoinfo_by_id.xml │ │ │ ├── activity_webview.xml │ │ │ ├── dialog_light_controller.xml │ │ │ ├── dialog_volume_controller.xml │ │ │ ├── empty_search_word.xml │ │ │ ├── fragment_cache.xml │ │ │ ├── fragment_feed.xml │ │ │ ├── fragment_follow.xml │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_local_coomon_landing.xml │ │ │ ├── fragment_profile.xml │ │ │ ├── fragment_search_hot.xml │ │ │ ├── fragment_slogan.xml │ │ │ ├── fragment_tag_detail_info.xml │ │ │ ├── fragment_video_landing.xml │ │ │ ├── item_collection_brief.xml │ │ │ ├── item_collection_card_cover.xml │ │ │ ├── item_collection_of_horizontal_scroll_card.xml │ │ │ ├── item_hot_search.xml │ │ │ ├── item_profile_setting.xml │ │ │ ├── item_square_card.xml │ │ │ ├── item_square_collection.xml │ │ │ ├── item_the_end.xml │ │ │ ├── item_video_samll_card.xml │ │ │ ├── item_video_text_card.xml │ │ │ ├── layout_author_tag_detail_header.xml │ │ │ ├── layout_blank_card.xml │ │ │ ├── layout_bottom_item.xml │ │ │ ├── layout_brife_card.xml │ │ │ ├── layout_card_banner.xml │ │ │ ├── layout_category_head_view.xml │ │ │ ├── layout_category_tab_toolbar.xml │ │ │ ├── layout_center_title_share_toolbar.xml │ │ │ ├── layout_choiceness.xml │ │ │ ├── layout_collection_of_horizontal_scroll_card.xml │ │ │ ├── layout_collection_with_brief.xml │ │ │ ├── layout_collection_with_cover.xml │ │ │ ├── layout_common_text.xml │ │ │ ├── layout_common_toolbar.xml │ │ │ ├── layout_division_line.xml │ │ │ ├── layout_follow_card.xml │ │ │ ├── layout_horizontal_scroll_card.xml │ │ │ ├── layout_ijk_wrapper.xml │ │ │ ├── layout_left_title_share_toolbar.xml │ │ │ ├── layout_load_more_view.xml │ │ │ ├── layout_loading_message.xml │ │ │ ├── layout_loading_view.xml │ │ │ ├── layout_margin_with_indicator_pager.xml │ │ │ ├── layout_media_controller_full_screen.xml │ │ │ ├── layout_media_controller_tiny.xml │ │ │ ├── layout_search_hot_remind_view.xml │ │ │ ├── layout_single_text.xml │ │ │ ├── layout_single_video.xml │ │ │ ├── layout_square_collection.xml │ │ │ ├── layout_video_author_head.xml │ │ │ ├── layout_video_detail_head.xml │ │ │ ├── layout_video_error.xml │ │ │ ├── layout_video_small_card.xml │ │ │ ├── refresh_category_header.xml │ │ │ └── refresh_daily_elite_header.xml │ │ ├── layout-v21/ │ │ │ └── layout_square_collection.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── ids_sticky_nav_layout.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── xml/ │ │ └── network_security_config.xml │ └── test/ │ └── java/ │ └── com/ │ └── jennifer/ │ └── andy/ │ └── simpleeyes/ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ references: cache_key: &cache_key key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} jobs: build: docker: - image: circleci/android:api-28-alpha environment: JAVA_TOOL_OPTIONS: "-Xmx1024m" GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2" TERM: dumb steps: - checkout - restore_cache: <<: *cache_key - run: name: Download Dependencies command: ./gradlew dependencies - save_cache: <<: *cache_key paths: - ~/.gradle/caches - ~/.gradle/wrapper - run: name: Run JVM Tests & Lint command: ./gradlew check - store_artifacts: path: app/build/reports destination: reports - store_test_results: path: app/build/test-results ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild ================================================ FILE: .idea/codeStyles/Project.xml ================================================
xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
.*:name http://schemas.android.com/apk/res/android
name ^$
style ^$
.* ^$ BY_NAME
.* http://schemas.android.com/apk/res/android ANDROID_ATTRIBUTE_ORDER
.* .* BY_NAME
================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/dictionaries/andy.xml ================================================ ================================================ FILE: .idea/encodings.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: LICENSE ================================================ Copyright [2019] [AndyJennifer] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # SimpleEyes [![CircleCI](https://circleci.com/gh/AndyJennifer/SimpleEyes.svg?style=shield)](https://circleci.com/gh/AndyJennifer/SimpleEyes) [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) SimpleEyes 一款基于 Kotlin 开发的短视频项目。该项目为如下两个分支: - [master 分支](https://github.com/AndyJennifer/SimpleEyes/tree/master) :MVP + Retrofit + Rxjava - [jetpack 分支](https://github.com/AndyJennifer/SimpleEyes/tree/simpleeyes-jetpack):MVVM + Android Jetpack + Retrofit + RxJava 你可以根据自己的需要,选择不同的分支。 ## 项目展示 ☘ ![picture_1.png](https://upload-images.jianshu.io/upload_images/2824145-9c4c8943bc9eebc7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 闪光点 ✨✨ - 100% 纯 Kotlin 开发。巧妙了运用了 Kotlin 的诸多特性,如扩展函数,数据类,委托等。 - 丰富的动画与自定义View。如下拉刷新、视差动画、TextView打字动画、嵌套滑动,状态布局。 - RxJava 常用操作符的使用,并新增了全局错误处理。 - 支持视频的横竖屏切换、音量控制、亮度控制等。 - 良好的代码规范与注释。 - 良好的设计模式与架构。 - ..... ## 完成的功能 💼 - 闪屏页实现 - [x] 开场短视频 - [x] 开场动画效果1、2 - 首页 - [x] 底部 Tab 切换 - [x] 顶部轮播 - [x] 下拉视差刷新,加载更多 - [x] 视频搜索 - [x] 每日精选 - [x] 登录页 - 发现 - [x] 热门、分类、作者、 - [x] 全部分类 - [x] 视频搜索 - 关注 - [x] 全部作者 - [x] 作者简介及视频 - 我的 - [ ] 我的消息 - [ ] 我的缓存 ## 使用的库 💪 站在巨人的肩膀上。可以看得更远。该项目中运用了以下开源库: - [Fresco](https://github.com/facebook/fresco) - [ARouter](https://github.com/alibaba/ARouter) - [Fragmentation](https://github.com/YoKeyword/Fragmentation) - [RxJava](https://github.com/ReactiveX/RxJava) - [Retrofit](https://github.com/square/retrofit) - [BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper) - [IjkPlayer](https://github.com/Bilibili/ijkplayer) - [FlexBox-Layout](https://github.com/google/flexbox-layout) - [Banner](https://github.com/youth5201314/banner) - [Android KTX](https://developer.android.google.cn/kotlin/ktx) - [Koin](https://github.com/InsertKoinIO/koin) ## 声明 📢 感谢 [开眼App](http://www.kaiyanapp.com) 提供参考,本人是豆瓣粉丝,使用了其中的 Api ,并非攻击。如构成侵权,请及时通知我删除或者修改。数据来源来自[开眼](https://www.kaiyanapp.com/) ,一切解释权归开眼所有。 ## 最后 注意:此开源项目仅做学习交流使用。如用到实际项目,还需多考虑其他因素,请多多斟酌。如果你觉得该项目不错,欢迎点击 star ❤️,follow,也可以帮忙分享给你更多的朋友。你的支持与鼓励是给我继续做好该项目的最大动力。 ## 联系我 - QQ:443696320 - 简书:[AndyandJennifer](https://www.jianshu.com/users/921c778fb5e1/timeline) - 掘金:[AndyandJennifer](https://juejin.im/user/5acc1ea06fb9a028bc2e0fc1) - Email: [andyjennifer@126.com](andyjennifer@126.com) ## License ```text Copyright [2019] [AndyJennifer] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' kapt { arguments { arg("AROUTER_MODULE_NAME", project.getName()) } } android { compileSdkVersion 28 buildToolsVersion '28.0.3' defaultConfig { applicationId "com.jennifer.andy.simpleeyes" minSdkVersion 21 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } kotlinOptions{ jvmTarget = "1.8" } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'com.google.android.material:material:1.0.0' //RxJava implementation 'io.reactivex.rxjava2:rxjava:2.2.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' //retrofit implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' //fragmentation implementation 'me.yokeyword:fragmentationx:1.0.1' //recyclerView implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.44' //fresco implementation 'com.facebook.fresco:fresco:1.11.0' //ijk implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8' //loadingView implementation 'com.wang.avi:library:2.1.3' implementation 'com.youth.banner:banner:1.4.10' //flex implementation 'com.google.android:flexbox:0.3.2' //pageIndicator implementation 'com.romandanylyk:pageindicatorview:1.0.1@aar' //ARouter api 'com.alibaba:arouter-api:1.4.1' kapt 'com.alibaba:arouter-compiler:1.2.2' //LeakCanary debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3' debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3' //ktx implementation 'androidx.core:core-ktx:1.1.0' //autodispose implementation 'com.uber.autodispose:autodispose-android:1.4.0' implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.4.0' } ================================================ 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/andy/Library/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 *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/com/jennifer/andy/simpleeyes/ExampleInstrumentedTest.kt ================================================ package com.jennifer.andy.simpleeyes import androidx.test.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getTargetContext() assertEquals("com.jennifer.andy.simplemusic", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/AndyApplication.kt ================================================ package com.jennifer.andy.simpleeyes import android.app.Application import com.alibaba.android.arouter.launcher.ARouter import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.imagepipeline.core.ImagePipelineConfig import com.squareup.leakcanary.LeakCanary /** * Author: andy.xwt * Date: 2017/10/15 09:27 * Description: */ class AndyApplication : Application() { companion object { lateinit var INSTANCE: Application } override fun onCreate() { super.onCreate() INSTANCE = this initARoute() initFresco() initLeakCanary() } /** * 初始化路由操作 */ private fun initARoute() { if (BuildConfig.DEBUG) { ARouter.openLog() ARouter.openDebug() } ARouter.init(this) } /** * 初始化Fresco,打开压缩 */ private fun initFresco() { val config = ImagePipelineConfig.newBuilder(this) .setDownsampleEnabled(true) .build() Fresco.initialize(this, config) } /** * 初始化LeakCanary,检测内存泄露 */ private fun initLeakCanary() { if (LeakCanary.isInAnalyzerProcess(this)) { return } LeakCanary.install(this) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/UserPreferences.kt ================================================ package com.jennifer.andy.simpleeyes import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit /** * Author: andy.xwt * Date: 2018/4/9 09:34 * Description:用户配置信息 */ object UserPreferences { private const val NAME = "andy." private const val KEY_IS_USER_LOGIN = "is_user_login" private const val KEY_IS_FIRST_LOGIN = "is_first_login" private const val KEY_IS_SHOW_USER_ANIM = "key_is_show_user_anim" /** * 保存用户是否登录配置 */ fun saveUserIsLogin(isUserLogin: Boolean) { getSharedPreferences().edit { putBoolean(KEY_IS_USER_LOGIN, isUserLogin) } } /** * 是否像用户展示过动画 */ fun saveShowUserAnim(isUserLogin: Boolean) { getSharedPreferences().edit { putBoolean(KEY_IS_SHOW_USER_ANIM, isUserLogin) } } /** * 保存用户是否是第一次登录 */ fun saveUserIsFirstLogin(isFirstLogin: Boolean) { getSharedPreferences().edit { putBoolean(KEY_IS_FIRST_LOGIN, isFirstLogin) } } /** * 获取用户是否登录 */ fun getUserIsLogin() = getSharedPreferences().getBoolean(KEY_IS_USER_LOGIN, false) /** * 是否像用户展示过动画 */ fun getShowUserAnim() = getSharedPreferences().getBoolean(KEY_IS_SHOW_USER_ANIM, false); /** * 获取用户是否是第一次登录 */ fun getUserIsFirstLogin() = getSharedPreferences().getBoolean(KEY_IS_FIRST_LOGIN, true) private fun getSharedPreferences(): SharedPreferences { return AndyApplication.INSTANCE.getSharedPreferences(NAME, Context.MODE_PRIVATE) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/entity/AndyInfo.kt ================================================ package com.jennifer.andy.simpleeyes.entity import java.io.Serializable /** * Author: andy.xwt * Date: 2017/10/19 19:00 * Description: 页面数据信息,主要是视频信息 */ data class AndyInfo(var count: Int, var total: Int, var nextPageUrl: String?, var date: Long, var nextPublishTime: Long, var topIssue: TopIssue, var refreshCount: Int, var lastStartId: Int, var itemList: MutableList) : Serializable data class JenniferInfo(var nextPageUrl: String?, var date: Long, var nextPublishTime: Long, var topIssue: TopIssue, var refreshCount: Int, var lastStartId: Int, var issueList: MutableList) : Serializable data class Content(var type: String, var data: ContentBean, var id: String, var date: Long, var tag: String) : Serializable data class ContentBean(var dataType: String, var header: Header?, var follow: FollowInfo?, var content: Content?, var itemList: MutableList, var id: String, var title: String, var description: String, var library: String, var tags: MutableList, var consumption: ConsumptionBean, var image: String, var resourceType: String, var slogan: String, var provider: ProviderBean, var category: String, var author: AuthorBean, var cover: CoverBean, var playUrl: String, var thumbPlayUrl: String, var duration: Int, var webUrl: WebUrlBean, var releaseTime: Long, var playInfo: MutableList, var campaign: String, var waterMarks: String, var type: String?, var titlePgc: String, var descriptionPgc: String, var remark: String, var idx: Int, var shareAdTrack: String, var favoriteAdTrack: String, var webAdTrack: String, var date: Long, val label: Label?, var labelList: MutableList<*>, var descriptionEditor: String, var isCollected: Boolean, var isPlayed: Boolean, var subtitles: MutableList<*>, var lastViewTime: String, var playlists: String, var text: String, var icon: String, var iconType: String, var height: Int, var src: Int, var actionUrl: String?) : Serializable data class Header(var id: String, var title: String?, var subTitle: String?, var subTitleFont: String, var textAlign: String?, var font: String, var cover: String, var label: Any, var actionUrl: String?, var labelList: MutableList<*>, var icon: String?, var iconType: String, var description: String?, var follow: FollowInfo?, var time: Long?) : Serializable data class Label(var text: String, var card: String, var detail: String) : Serializable data class TagsBean(var id: Int, var name: String, var actionUrl: String, var adTrack: String) : Serializable data class ConsumptionBean(var collectionCount: String, var shareCount: String, var replyCount: String) : Serializable data class TopIssue(var type: String, var data: TopIssueBean, var tag: String) : Serializable data class TopIssueBean(var dataType: String, var count: Int, var itemList: MutableList) : Serializable data class ProviderBean(var name: String, var alias: String, var icon: String) : Serializable data class AuthorBean(var id: String, var icon: String, var name: String, var description: String, var link: String, var latestReleaseTime: Long, var videoNum: Int, var adTrack: String, var follow: FollowInfo, var shield: ShieldBean, var approvedNotReadyVideoCount: Int, var isIfPgc: Boolean = false) : Serializable data class ShieldBean( var itemType: String, var itemId: Int, var isShielded: Boolean) : Serializable data class CoverBean(var feed: String, var detail: String, var blurred: String, var sharing: String, var homepage: String) : Serializable data class WebUrlBean(var raw: String, var forWeibo: String) : Serializable data class PlayInfoBean(var height: Int, var width: Int, var name: String, var type: String, var url: String, var urlList: MutableList) : Serializable data class UrlListBean(var name: String, var url: String, var size: Double) : Serializable data class Category(var categoryInfo: CategoryInfo, var tabInfo: TabInfo) data class CategoryInfo(var dataType: String, var id: String, var name: String, var description: String, var headerImage: String, var actionUrl: String, var followInfo: FollowInfo) data class Tab(var tabInfo: TabInfo, var pgcInfo: PgcInfo) : Serializable data class TabInfo(var tabList: MutableList, var defaultIdx: Int) : Serializable data class TabDetailInfo(var id: Int, var name: String, var apiUrl: String) : Serializable data class FollowInfo(var itemType: String, var itemId: String, var followed: Boolean) : Serializable data class PgcInfo(var dataType: String, var id: Int, var icon: String, var name: String, var brief: String, var description: String, var actionUrl: String, var followCount: Int, var shareCount: Int, var videoCount: Int, var collectCount: Int, var followInfo: FollowInfo) : Serializable ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/manager/ActivityManager.kt ================================================ package com.jennifer.andy.simpleeyes.manager import android.app.Activity /** * Author: andy.xwt * Date: 2017/8/14 00:15 * Description:Activity管理类 */ class ActivityManager private constructor() { private val sActivityList = ArrayList() companion object { fun getInstance(): ActivityManager { return BaseAppManagerHolder.instance } } private object BaseAppManagerHolder { val instance = ActivityManager() } /** * 当前activity的个数 */ val mActivitySize: Int get() = sActivityList.size val mForwardActivity: Activity? @Synchronized get() = if (mActivitySize > 0) sActivityList[mActivitySize - 1] else null /** * 添加相应activity */ @Synchronized fun removeActivity(activity: Activity) { if (activity in sActivityList) sActivityList.remove(activity) } /** * 添加相应的activity */ @Synchronized fun addActivity(activity: Activity) { sActivityList.add(activity) } /** * 清除栈内activity */ @Synchronized fun clear() { var i = sActivityList.size - 1 while (i > -1) { val activity = sActivityList[i] removeActivity(activity) activity.finish() i = sActivityList.size i-- } } /** * 清除栈顶activity */ @Synchronized fun clearToTop() { var i = sActivityList.size - 2 while (i > -1) { val activity = sActivityList[i] removeActivity(activity) activity.finish() i = sActivityList.size - 1 i-- } } /** * 获取最上层的Activity */ @Synchronized fun getTopActivity(): Activity? { return if (sActivityList.isNotEmpty()) { sActivityList[sActivityList.size - 1] } else { null } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/net/Api.kt ================================================ package com.jennifer.andy.simpleeyes.net /** * Author: andy.xwt * Date: 2017/10/15 09:54 * Description: */ object Api { /** * 主域名 */ val BASE_URL: String get() = "http://baobab.kaiyanapp.com/" /** * 获取默认Service */ fun getDefault() = RetrofitConfig.getDefaultService() } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/net/ApiService.kt ================================================ package com.jennifer.andy.simpleeyes.net import com.jennifer.andy.simpleeyes.entity.* import io.reactivex.Observable import okhttp3.ResponseBody import retrofit2.Call import retrofit2.http.* /** * Author: andy.xwt * Date: 2017/10/10 22:46 * Description: */ interface ApiService { /////////////////////////////////////////////////////////////////////////// // 首页相关 /////////////////////////////////////////////////////////////////////////// /** * 首页 */ @GET("api/v4/tabs/selected") fun getHomeInfo(): Observable /** * 获取热门关键词 */ @GET("api/v3/queries/hot") fun getHotWord(): Observable> /** * 关键词搜索 */ @GET("api/v1/search") fun searchVideoByWord(@Query("query") word: String): Observable /** * 每日精选旁的日历显示 */ @GET("api/v3/issueNavigationList") fun getIssueNaviGationList(): Observable /////////////////////////////////////////////////////////////////////////// // 发现相关 /////////////////////////////////////////////////////////////////////////// /** * 发现 */ @GET("api/v4/discovery") fun getDiscoveryTab(): Observable /** * 获取全部分类信息 */ @GET("api/v4/categories/all") fun getAllCategoriesInfo(): Observable /** * 获取排行榜tab信息 */ @GET("api/v4/rankList") fun getRankListTab(): Observable /** * 获取专题信息 */ @GET("api/v3/specialTopics") fun getTopicInfo(): Observable /** * 获取tag信息 * @param tagId tagId * @param strategy tag模式 */ @GET("api/v3/tag/videos") fun getTagInfo(@Query("tagId") tagId: String, @Query("strategy") strategy: String): Observable /** * 获取种类下tab详细信息 */ @GET("api/v4/categories/detail/tab") fun getCategoryTabInfo(@Query("id") id: String): Observable /** * 获取种类下tab下列表集合 */ @GET("api/v4/categories/detail/index") fun getCategroyTabListItemInfo(@Query("id") id: String): Observable /////////////////////////////////////////////////////////////////////////// // 关注相关 /////////////////////////////////////////////////////////////////////////// /** * 关注 */ @GET("api/v4/tabs/follow") fun getFollowInfo(): Observable /** * 全部作者 */ @GET("api/v4/pgcs/all") fun getAllAuthor(): Observable /** * 作者详细信息 */ @GET("api/v4/pgcs/detail/tab") fun getAuthorTagDetail(@Query("id") id: String): Observable /////////////////////////////////////////////////////////////////////////// // 公共接口 /////////////////////////////////////////////////////////////////////////// /** * 根据url,获取数据 * @url tab请求地址 */ @GET fun getDataInfoFromUrl(@Url url: String?): Observable /** * 根据url,获取更多信息 * @param url 下一页请求地址 */ @GET fun getMoreAndyInfo(@Url url: String?): Observable /** * 根据url,获取更多信息 * @param url 下一页请求地址 */ @GET fun getMoreJenniferInfo(@Url url: String?): Observable /////////////////////////////////////////////////////////////////////////// // 视频相关 /////////////////////////////////////////////////////////////////////////// /** * 根据视频id,获取相关信息 */ @GET("api/v2/video/{id}") fun getVideoInfoById(@Path("id") id: String): Observable /** * 获取相关视频信息 * @id 视频id */ @GET("api/v4/video/related") fun getRelatedVideo(@Query("id") id: String): Observable /** * 每日编辑精选 */ @GET("api/v2/feed?num=3") fun getDailyElite(): Observable /** * 下载视频 */ @Streaming @GET fun downloadVideo(@Url url: String): Call } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/net/Extras.kt ================================================ package com.jennifer.andy.simpleeyes.net /** * Author: andy.xwt * Date: 2017/10/31 14:16 * Description: */ object Extras { /** * 更新参数 */ const val UPDATE_PARAMS = "update_params" const val VIDEO_INFO = "video_info" const val VIDEO_INFO_INDEX = "video_info_index" const val VIDEO_LIST_INFO = "video_list_info" const val SLOGAN_ENGLISH = "slogan_english" const val SLOGAN_CHINESE = "slogan_chinese" const val API_URL = "api_url" const val ID = "id" const val TAB_INDEX = "tab_index" const val TITLE = "title" } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/net/RetrofitConfig.kt ================================================ package com.jennifer.andy.simpleeyes.net import com.google.gson.GsonBuilder import com.jennifer.andy.simpleeyes.AndyApplication import okhttp3.Cache import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import java.io.File import java.util.concurrent.TimeUnit /** * Author: andy.xwt * Date: 2017/10/10 22:41 * Description: */ class RetrofitConfig private constructor() { private lateinit var mRetrofit: Retrofit private lateinit var mHttpLoggingInterceptor: HttpLoggingInterceptor private lateinit var mRequestInterceptor: Interceptor private lateinit var mOkHttpClient: OkHttpClient private lateinit var mCache: Cache companion object { private const val READ_TIME_OUT = 5000L// 读取超过时间 单位:毫秒 private const val WRITE_TIME_OUT = 5000L//写超过时间 单位:毫秒 private const val FILE_CACHE_SIZE = 1024 * 1024 * 100L//缓存大小100Mb private val Instance: RetrofitConfig by lazy { RetrofitConfig() } /** * 获取默认请求接口 */ @JvmStatic fun getDefaultService(): ApiService { return Instance.apiService } } private lateinit var apiService: ApiService init { initRequestInterceptor() initLoggingInterceptor() initCachePathAndSize() initOkHttpClient() initRetrofit() } /** * 初始化请求拦截,添加缓存头,与全局请求参数 */ private fun initRequestInterceptor() { mRequestInterceptor = Interceptor { chain -> //注意全局请求参数都是死的 val urlBuilder = chain.request().url() .newBuilder() .setEncodedQueryParameter("udid", "d0f6190461864a3a978bdbcb3fe9b48709f1f390") .setEncodedQueryParameter("vc", "225") .setEncodedQueryParameter("vn", "3.12.0") .setEncodedQueryParameter("deviceModel", "Redmi%204") .setEncodedQueryParameter("first_channel", "eyepetizer_xiaomi_market") .setEncodedQueryParameter("last_channel", "eyepetizer_xiaomi_market") .setEncodedQueryParameter("system_version_code", "23") val request = chain.request().newBuilder() .addHeader("Content-Type", "application/json") .addHeader("Cookie", "ky_auth=;sdk=23") .addHeader("model", "Android") .url(urlBuilder.build()) .build() chain.proceed(request) } } /** * 初始化日志拦截 */ private fun initLoggingInterceptor() { mHttpLoggingInterceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) } /** * 配置缓存大小与缓存地址 */ private fun initCachePathAndSize() { val cacheFile = File(AndyApplication.INSTANCE.cacheDir, "cache") mCache = Cache(cacheFile, FILE_CACHE_SIZE) } /** * 配置okHttp */ private fun initOkHttpClient() { mOkHttpClient = OkHttpClient.Builder() .readTimeout(READ_TIME_OUT, TimeUnit.MILLISECONDS) .writeTimeout(WRITE_TIME_OUT, TimeUnit.MILLISECONDS) .cache(mCache) .addInterceptor(mRequestInterceptor) .addInterceptor(mHttpLoggingInterceptor) .build() } /** * 配置retrofit */ private fun initRetrofit() { val gson = GsonBuilder() .setDateFormat("yyyy-MM-dd hh:mm:ss") .create() mRetrofit = Retrofit.Builder() .client(mOkHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .baseUrl(Api.BASE_URL) .build() apiService = mRetrofit.create(ApiService::class.java) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/FileMediaDataSource.kt ================================================ package com.jennifer.andy.simpleeyes.player import tv.danmaku.ijk.media.player.misc.IMediaDataSource import java.io.File import java.io.IOException import java.io.RandomAccessFile /** * Author: andy.xwt * Date: 2020/3/10 13:29 * Description: */ class FileMediaDataSource(file: File) : IMediaDataSource { private var mFile: RandomAccessFile = RandomAccessFile(file, "r") var mFileSize = mFile.length() @Throws(IOException::class) override fun readAt(position: Long, buffer: ByteArray?, offset: Int, size: Int): Int { if (mFile.filePointer != position) mFile.seek(position) return if (size == 0) 0 else mFile.read(buffer, 0, size) } @Throws(IOException::class) override fun getSize(): Long { return mFileSize } @Throws(IOException::class) override fun close() { mFileSize = 0 mFile.close() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/IjkMediaController.kt ================================================ package com.jennifer.andy.simpleeyes.player import android.app.Activity import android.content.Context import android.graphics.PixelFormat import android.media.AudioManager import android.util.Log import android.view.* import android.view.View.OnLayoutChangeListener import android.view.View.OnTouchListener import android.widget.FrameLayout import android.widget.MediaController.MediaPlayerControl import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.player.event.VideoProgressEvent import com.jennifer.andy.simpleeyes.player.view.ControllerView import com.jennifer.andy.simpleeyes.player.view.ControllerViewFactory /** * Author: andy.xwt * Date: 2020/3/10 17:38 * Description:IjkMediaController 用于视频播放的UI,内部原理是通过window进行展示,其中window的位置与大小 * 是根据[mAnchor]的位置动态调整的。 * */ class IjkMediaController(context: Context) : FrameLayout(context) { private lateinit var player: MediaPlayerControl private lateinit var windowManager: WindowManager private lateinit var window: Window private lateinit var decorLayoutParams: WindowManager.LayoutParams private lateinit var decorView: View private lateinit var mAnchor: View//锚点view,用于更新window的位置 var isShowing = false//当前window是否展示 var currentVideoInfo: ContentBean? = null var totalCount = 0 var currentIndex = 0 private var showMode1 = ControllerViewFactory.TINY_MODE //当前window展示的类型,默认情况下是小视图 var controllerListener: ControllerListener? = null var controllerView: ControllerView? = null private val mControllerViewFactory = ControllerViewFactory() companion object { private const val WINDOW_TIME_OUT = 3500L //默认window消失时间 3.5秒 } init { initFloatingWindowLayout() initFloatingWindowConfig() } /** * 初始化window布局 */ private fun initFloatingWindowLayout() { decorLayoutParams = WindowManager.LayoutParams().apply { gravity = Gravity.TOP or Gravity.LEFT height = LayoutParams.WRAP_CONTENT x = 0 format = PixelFormat.TRANSLUCENT type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL flags = flags or (WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) token = null windowAnimations = 0 } } /** * 初始化window参数 */ private fun initFloatingWindowConfig() { windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager window = PolicyCompat().createPhoneWindow(context).apply { setWindowManager(windowManager, null, null) requestFeature(Window.FEATURE_NO_TITLE) setContentView(this@IjkMediaController) setBackgroundDrawableResource(R.color.transparent) volumeControlStream = AudioManager.STREAM_MUSIC } decorView = window.decorView decorView.setOnTouchListener(mTouchListener) isFocusable = true isFocusableInTouchMode = true descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS requestFocus() } /** * 初始化数据 * * @param currentIndex 当前视频角标 * @param totalCount 视频集合总数 * @param currentVideoInfo 当前视频信息 */ fun initData(currentIndex: Int, totalCount: Int, currentVideoInfo: ContentBean?) { this.currentIndex = currentIndex this.totalCount = totalCount this.currentVideoInfo = currentVideoInfo } /** * 当锚点布局发生改变时,动态更新window的位置,高度与宽度 */ private fun updateFloatingWindowLayout() { val anchorPos = IntArray(2) mAnchor.getLocationOnScreen(anchorPos) //重新测量 decorView.measure(MeasureSpec.makeMeasureSpec(mAnchor.width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(mAnchor.height, MeasureSpec.AT_MOST)) decorLayoutParams.apply { width = mAnchor.width x = anchorPos[0] y = anchorPos[1] height = mAnchor.height } } fun setMediaPlayer(player: MediaPlayerControl) { this.player = player switchControllerView(showMode1) } /** * 当锚点view布局发生改变的时,重新设置当前window的布局参数 */ private val mLayoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> updateFloatingWindowLayout() if (isShowing) windowManager.updateViewLayout(decorView, decorLayoutParams) } /** * 将控制层view与锚点的关联 */ fun setAnchorView(view: View) { mAnchor = view mAnchor.addOnLayoutChangeListener(mLayoutChangeListener) updateFloatingWindowLayout() } /////////////////////////////////////////////////////////////////////////// // Window显示相关 /////////////////////////////////////////////////////////////////////////// /** * 触摸监听,如果当前window正在显示,再次按下走默认时间内该window会消失 */ private val mTouchListener = OnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_DOWN) { if (isShowing) { removeCallbacks(mFadeOut) postDelayed(mFadeOut, WINDOW_TIME_OUT) } } false } /** * 隐藏控制层view */ private val mFadeOut = Runnable { if (isShowing) hide() } /** * 第一次显示,不显示控制层 */ fun firstShow() { if (!isShowing && controllerView != null) { controllerView?.display() } } /** * 将控制器显示在屏幕上,当到达过期时间时会自动消失。 * * @param timeout 过期时间(毫秒) 如果设置为0,那么会直到调用hide方法才会消失 */ @JvmOverloads fun show(timeout: Long = WINDOW_TIME_OUT) { if (!isShowing) { windowManager.addView(decorView, decorLayoutParams) isShowing = true } if (timeout != 0L) { removeCallbacks(mFadeOut) postDelayed(mFadeOut, timeout.toLong()) } controllerView?.display() controllerListener?.onShowController(true) } /** * 隐藏window */ fun hide() { try { removeCallbacks(mFadeOut) isShowing = false controllerListener?.onShowController(false) windowManager.removeView(decorView) } catch (ex: IllegalArgumentException) { Log.w("MediaController", "already removed") } } /** * 一直显示window */ fun showAllTheTime() { show(3600000) removeCallbacks(mFadeOut) } override fun dispatchKeyEvent(event: KeyEvent): Boolean { val keyCode = event.keyCode val uniqueDown = (event.repeatCount == 0 && event.action == KeyEvent.ACTION_DOWN) if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { if (uniqueDown) { hide() //如果传入的是activity直接退出 if (context is Activity) (context as Activity).finish() } return true } return super.dispatchKeyEvent(event) } /////////////////////////////////////////////////////////////////////////// // Window显示内容相关 /////////////////////////////////////////////////////////////////////////// /** * 根据显示模式切换展示的控制UI * * @param showMode */ fun switchControllerView(showMode: Int) { showMode1 = showMode removeAllViews() controllerView = mControllerViewFactory.create(showMode, context) controllerView?.initControllerAndData(player, this, currentVideoInfo) controllerView?.display() addView(controllerView) } /** * 显示错误布局 */ fun showErrorView() { if (!isShowing) { controllerView?.showErrorView() windowManager.addView(decorView, decorLayoutParams) isShowing = true } } fun updateProgressAndTime(videoProgressEvent: VideoProgressEvent?) { controllerView?.updateProgressAndTime(videoProgressEvent!!) } fun resetType() { controllerView?.showContent() } /////////////////////////////////////////////////////////////////////////// // 数据相关 /////////////////////////////////////////////////////////////////////////// /** * 判断是否拥有上一个视频 */ fun isHavePreVideo() = totalCount > 0 && currentIndex > 0 /** * 是否拥有下一个视频 */ fun isHaveNextVideo() = totalCount > 0 && currentIndex < totalCount - 1 /////////////////////////////////////////////////////////////////////////// // 回调监听 /////////////////////////////////////////////////////////////////////////// interface ControllerListener { //退出点击 fun onBackClick() //上一页点击 fun onPreClick() //下一页点击 fun onNextClick() //全屏点击 fun onFullScreenClick() //退出全屏 fun onTinyScreenClick() //错误界面点击 fun onErrorViewClick() //是否显示控制层 fun onShowController(isShowController: Boolean) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/IjkVideoView.kt ================================================ package com.jennifer.andy.simpleeyes.player import android.annotation.TargetApi import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.media.AudioManager import android.media.MediaPlayer import android.net.Uri import android.os.Build import android.text.TextUtils import android.util.AttributeSet import android.util.Log import android.view.Gravity import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.widget.FrameLayout import android.widget.MediaController.MediaPlayerControl import com.jennifer.andy.simpleeyes.player.event.VideoProgressEvent import com.jennifer.andy.simpleeyes.player.render.IRenderView import com.jennifer.andy.simpleeyes.player.render.IRenderView.IRenderCallback import com.jennifer.andy.simpleeyes.player.render.IRenderView.ISurfaceHolder import com.jennifer.andy.simpleeyes.player.render.SurfaceRenderView import com.jennifer.andy.simpleeyes.player.render.TextureRenderView import com.jennifer.andy.simpleeyes.player.view.ControllerViewFactory import com.jennifer.andy.simpleeyes.rx.RxBus import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import tv.danmaku.ijk.media.player.IMediaPlayer import tv.danmaku.ijk.media.player.IjkMediaPlayer import tv.danmaku.ijk.media.player.misc.IMediaDataSource import java.io.File import java.io.IOException import java.util.* import java.util.concurrent.TimeUnit /** * Author: andy.xwt * Date: 2020/3/12 23:03 * Description: */ class IjkVideoView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), MediaPlayerControl { private var mUri: Uri? = null private var mHeaders: Map? = null private var mSurfaceHolder: ISurfaceHolder? = null private var mMediaPlayer: IMediaPlayer? = null private var mVideoWidth = 0 private var mVideoHeight = 0 private var mSurfaceWidth = 0 private var mSurfaceHeight = 0 private var mRenderView: IRenderView? = null private var mCurrentRender = RENDER_NONE private var mRenderUIView: View? = null private var mVideoRotationDegree = 0 private var mVideoSarNum = 0 private var mVideoSarDen = 0 private var mCurrentAspectRatio = allAspectRatio[0] private var mMediaController: IjkMediaController? = null private var mOnCompletionListener: IMediaPlayer.OnCompletionListener? = null private var mOnPreparedListener: IMediaPlayer.OnPreparedListener? = null private var mOnErrorListener: IMediaPlayer.OnErrorListener? = null private var mOnInfoListener: IMediaPlayer.OnInfoListener? = null private var mCurrentBufferPercentage = 0 private var mSeekWhenPrepared = 0 private val mCanPause = true private val mCanSeekBack = true private val mCanSeekForward = true private lateinit var mAppContext: Context private var mCurrentState = STATE_IDLE private var mTargetState = STATE_IDLE var screenState = ControllerViewFactory.TINY_MODE private val mSHCallback: IRenderCallback = object : IRenderCallback { override fun onSurfaceChanged(holder: ISurfaceHolder, format: Int, w: Int, h: Int) { if (holder.renderView !== mRenderView) { Log.e(TAG, "onSurfaceChanged: unmatched render callback\n") return } mSurfaceWidth = w mSurfaceHeight = h val isValidState = mTargetState == STATE_PLAYING val hasValidSize = !mRenderView!!.shouldWaitForResize() || mVideoWidth == w && mVideoHeight == h if (mMediaPlayer != null && isValidState && hasValidSize) { if (mSeekWhenPrepared != 0) { seekTo(mSeekWhenPrepared) } start() } } override fun onSurfaceCreated(holder: ISurfaceHolder, width: Int, height: Int) { if (holder.renderView !== mRenderView) { Log.e(TAG, "onSurfaceCreated: unmatched render callback\n") return } mSurfaceHolder = holder if (mMediaPlayer != null) bindSurfaceHolder(mMediaPlayer, holder) else openVideo() } override fun onSurfaceDestroyed(holder: ISurfaceHolder) { if (holder.renderView !== mRenderView) { Log.e(TAG, "onSurfaceDestroyed: unmatched render callback\n") return } // after we return from this we can't use the surface any more mSurfaceHolder = null // REMOVED: if (mMediaController != null) mMediaController.hide(); // REMOVED: release(true); releaseWithoutStop() } } companion object { private const val STATE_ERROR = -1 private const val STATE_IDLE = 0 private const val STATE_PREPARING = 1 private const val STATE_PREPARED = 2 private const val STATE_PLAYING = 3 private const val STATE_PAUSED = 4 private const val STATE_PLAYBACK_COMPLETED = 5 private val allAspectRatio = intArrayOf( IRenderView.AR_ASPECT_FIT_PARENT, IRenderView.AR_ASPECT_FILL_PARENT, IRenderView.AR_ASPECT_WRAP_CONTENT, IRenderView.AR_16_9_FIT_PARENT, IRenderView.AR_4_3_FIT_PARENT) const val RENDER_NONE = 0 const val RENDER_SURFACE_VIEW = 1 const val RENDER_TEXTURE_VIEW = 2 private const val TAG = "IjkVideoView" } init { initVideoView(context) } fun toggleAspectRatio(currentAspectRatio: Int): Int { mRenderView?.setAspectRatio(currentAspectRatio) mCurrentAspectRatio = currentAspectRatio return mCurrentAspectRatio } private fun initVideoView(context: Context) { mAppContext = context.applicationContext initRenders() //初始化宽高 mVideoWidth = 0 mVideoHeight = 0 //获取焦点 isFocusable = true isFocusableInTouchMode = true requestFocus() //初始化状态 mCurrentState = STATE_IDLE mTargetState = STATE_IDLE //设置背景 background = ColorDrawable(Color.BLACK) } /** * 设置渲染Render * 4.0 及以上版本使用 TextureView * 4.0 版本以下使用 SurfaceView */ private fun initRenders() { mCurrentRender = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { //4.0以上 RENDER_TEXTURE_VIEW } else { RENDER_SURFACE_VIEW } setRender(mCurrentRender) } /** * 设置渲染Render */ private fun setRender(render: Int) { when (render) { RENDER_NONE -> setRenderView(null) RENDER_TEXTURE_VIEW -> { //TextureView val renderView = TextureRenderView(context) if (mMediaPlayer != null) { renderView.getSurfaceHolder().bindToMediaPlayer(mMediaPlayer) renderView.setVideoSize(mMediaPlayer!!.videoWidth, mMediaPlayer!!.videoHeight) renderView.setVideoSampleAspectRatio(mMediaPlayer!!.videoSarNum, mMediaPlayer!!.videoSarDen) renderView.setAspectRatio(mCurrentAspectRatio) } setRenderView(renderView) } RENDER_SURFACE_VIEW -> { //surfaceView val renderView = SurfaceRenderView(context) setRenderView(renderView) } else -> Log.e(TAG, String.format(Locale.getDefault(), "invalid render %d\n", render)) } } /** * 设置渲染界面,初始化当前的渲染界面参数,并设置渲染回调等初始化操作 */ private fun setRenderView(renderView: IRenderView?) { if (mRenderView != null) { if (mMediaPlayer != null) mMediaPlayer?.setDisplay(null) val renderUIView = mRenderView?.getView() mRenderView?.removeRenderCallback(mSHCallback) mRenderView = null removeView(renderUIView) } if (renderView == null) return mRenderView = renderView renderView.setAspectRatio(mCurrentAspectRatio) //设置适配比例 if (mVideoWidth > 0 && mVideoHeight > 0) renderView.setVideoSize(mVideoWidth, mVideoHeight) //设置视频的宽高 if (mVideoSarNum > 0 && mVideoSarDen > 0) //设置采样比例 renderView.setVideoSampleAspectRatio(mVideoSarNum, mVideoSarDen) mRenderUIView = mRenderView?.getView().apply { layoutParams = LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER) } addView(mRenderUIView) mRenderView?.addRenderCallback(mSHCallback) //添加回调 mRenderView?.setVideoRotation(mVideoRotationDegree) //设置视频旋转 } /** * 设置视频播放路径 * * @param path 视频路径 */ fun setVideoPath(path: String?) { setVideoURI(Uri.parse(path)) } /** * 设置视频URI * * @param uri 视频URI */ fun setVideoURI(uri: Uri) { setVideoURI(uri, null) } /** * 设置带header 的 视频URI * * * 注意,默认情况下允许跨域重定向, * 但可以通过以“android allow cross domain redirect”为键, * 以“0”或“1”为值的headers,以禁止或允许跨域重定向。 * * @param uri 视频URI. * @param headers URI请求需要的header */ private fun setVideoURI(uri: Uri, headers: Map?) { mUri = uri mHeaders = headers mSeekWhenPrepared = 0 openVideo() requestLayout() invalidate() } fun stopPlayback() { mMediaPlayer?.let { mMediaPlayer?.stop() mMediaPlayer?.release() mMediaPlayer = null mCurrentState = STATE_IDLE mTargetState = STATE_IDLE mMediaController?.let { mMediaController?.hide() mMediaController = null } val am = mAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager am.abandonAudioFocus(null) cancelProgressRunnable() } } /** * 打开视频 */ @TargetApi(Build.VERSION_CODES.M) private fun openVideo() { if (mUri == null || mSurfaceHolder == null) { // not ready for playback just yet, will try again later return } // we shouldn't clear the target state, because somebody might have // called start() previously release(false) val am = mAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) try { mMediaPlayer = createPlayer() mMediaPlayer?.setOnPreparedListener(mPreparedListener) mMediaPlayer?.setOnVideoSizeChangedListener(mSizeChangedListener) mMediaPlayer?.setOnCompletionListener(mCompletionListener) mMediaPlayer?.setOnErrorListener(mErrorListener) mMediaPlayer?.setOnInfoListener(mInfoListener) mMediaPlayer?.setOnBufferingUpdateListener(mBufferingUpdateListener) mMediaPlayer?.setOnSeekCompleteListener(mSeekCompleteListener) mCurrentBufferPercentage = 0 val scheme = mUri?.scheme if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && (TextUtils.isEmpty(scheme) || scheme.equals("file", ignoreCase = true))) { val dataSource: IMediaDataSource = FileMediaDataSource(File(mUri.toString())) mMediaPlayer?.setDataSource(dataSource) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { mMediaPlayer?.setDataSource(mAppContext, mUri, mHeaders) } else { mMediaPlayer?.dataSource = mUri.toString() } bindSurfaceHolder(mMediaPlayer, mSurfaceHolder) mMediaPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC) mMediaPlayer?.setScreenOnWhilePlaying(true) mMediaPlayer?.prepareAsync() mCurrentState = STATE_PREPARING attachMediaController() } catch (ex: IOException) { Log.w(TAG, "Unable to open content: $mUri", ex) mCurrentState = STATE_ERROR mTargetState = STATE_ERROR mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0) } catch (ex: IllegalArgumentException) { Log.w(TAG, "Unable to open content: $mUri", ex) mCurrentState = STATE_ERROR mTargetState = STATE_ERROR mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0) } finally { // REMOVED: mPendingSubtitleTracks.clear(); } } fun setMediaController(controller: IjkMediaController?) { mMediaController = controller attachMediaController() } fun getMediaController(): IjkMediaController? { return mMediaController } private fun attachMediaController() { if (mMediaPlayer != null && mMediaController != null) { mMediaController?.setMediaPlayer(this) val anchorView = if (this.parent is View) this.parent as View else this mMediaController?.setAnchorView(anchorView) mMediaController?.isEnabled = isInPlaybackState() } } var mSizeChangedListener = IMediaPlayer.OnVideoSizeChangedListener { mp, _, _, _, _ -> mVideoWidth = mp.videoWidth mVideoHeight = mp.videoHeight mVideoSarNum = mp.videoSarNum mVideoSarDen = mp.videoSarDen if (mVideoWidth != 0 && mVideoHeight != 0) { mRenderView?.let { mRenderView?.setVideoSize(mVideoWidth, mVideoHeight) mRenderView?.setVideoSampleAspectRatio(mVideoSarNum, mVideoSarDen) } // REMOVED: getHolder().setFixedSize(mVideoWidth, mVideoHeight); requestLayout() } } /** * 视频播放准备完成监听 */ var mPreparedListener = IMediaPlayer.OnPreparedListener { mp -> mCurrentState = STATE_PREPARED if (mOnPreparedListener != null) { mOnPreparedListener!!.onPrepared(mMediaPlayer) } if (mMediaController != null) { mMediaController!!.isEnabled = true } mVideoWidth = mp.videoWidth mVideoHeight = mp.videoHeight val seekToPosition = mSeekWhenPrepared // mSeekWhenPrepared may be changed after seekTo() call if (seekToPosition != 0) { seekTo(seekToPosition) } if (mVideoWidth != 0 && mVideoHeight != 0) { mRenderView?.let { mRenderView?.setVideoSize(mVideoWidth, mVideoHeight) mRenderView?.setVideoSampleAspectRatio(mVideoSarNum, mVideoSarDen) if (!mRenderView!!.shouldWaitForResize() || mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { // We didn't actually change the size (it was already at the size // we need), so we won't get a "surface changed" callback, so // start the video here instead of in the callback. if (mTargetState == STATE_PLAYING) { start() mMediaController?.firstShow() } else if (!isPlaying && (seekToPosition != 0 || currentPosition > 0)) { // Show the media controls when we're paused into a video and make 'em stick. mMediaController?.show(0) } } } } else { // We don't know the video size yet, but should start anyway. // The video size might be reported to us later. if (mTargetState == STATE_PLAYING) { start() } } } private val mCompletionListener = IMediaPlayer.OnCompletionListener { mCurrentState = STATE_PLAYBACK_COMPLETED mTargetState = STATE_PLAYBACK_COMPLETED mMediaController?.hide() mOnCompletionListener?.onCompletion(mMediaPlayer) } private val mInfoListener = IMediaPlayer.OnInfoListener { mp, arg1, arg2 -> mOnInfoListener?.onInfo(mp, arg1, arg2) when (arg1) { IMediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING -> Log.d(TAG, "MEDIA_INFO_VIDEO_TRACK_LAGGING:") IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START -> Log.d(TAG, "MEDIA_INFO_VIDEO_RENDERING_START:") IMediaPlayer.MEDIA_INFO_BUFFERING_START -> Log.d(TAG, "MEDIA_INFO_BUFFERING_START:") IMediaPlayer.MEDIA_INFO_BUFFERING_END -> Log.d(TAG, "MEDIA_INFO_BUFFERING_END:") IMediaPlayer.MEDIA_INFO_NETWORK_BANDWIDTH -> Log.d(TAG, "MEDIA_INFO_NETWORK_BANDWIDTH: $arg2") IMediaPlayer.MEDIA_INFO_BAD_INTERLEAVING -> Log.d(TAG, "MEDIA_INFO_BAD_INTERLEAVING:") IMediaPlayer.MEDIA_INFO_NOT_SEEKABLE -> Log.d(TAG, "MEDIA_INFO_NOT_SEEKABLE:") IMediaPlayer.MEDIA_INFO_METADATA_UPDATE -> Log.d(TAG, "MEDIA_INFO_METADATA_UPDATE:") IMediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE -> Log.d(TAG, "MEDIA_INFO_UNSUPPORTED_SUBTITLE:") IMediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT -> Log.d(TAG, "MEDIA_INFO_SUBTITLE_TIMED_OUT:") IMediaPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED -> { mVideoRotationDegree = arg2 Log.d(TAG, "MEDIA_INFO_VIDEO_ROTATION_CHANGED: $arg2") if (mRenderView != null) mRenderView!!.setVideoRotation(arg2) } IMediaPlayer.MEDIA_INFO_AUDIO_RENDERING_START -> Log.d(TAG, "MEDIA_INFO_AUDIO_RENDERING_START:") } true } private val mErrorListener = IMediaPlayer.OnErrorListener { mp, framework_err, impl_err -> Log.d(TAG, "Error: $framework_err,$impl_err") mCurrentState = STATE_ERROR mTargetState = STATE_ERROR /* If an error handler has been supplied, use it and finish. */if (mOnErrorListener != null) { if (mOnErrorListener!!.onError(mMediaPlayer, framework_err, impl_err)) { return@OnErrorListener true } } true } private val mBufferingUpdateListener = IMediaPlayer.OnBufferingUpdateListener { _, percent -> mCurrentBufferPercentage = percent } private val mSeekCompleteListener = IMediaPlayer.OnSeekCompleteListener { } /** * Register a callback to be invoked when the media file * is loaded and ready to go. * * @param l The callback that will be run */ fun setOnPreparedListener(l: IMediaPlayer.OnPreparedListener?) { mOnPreparedListener = l } /** * Register a callback to be invoked when the end of a media file * has been reached during playback. * * @param l The callback that will be run */ fun setOnCompletionListener(l: IMediaPlayer.OnCompletionListener?) { mOnCompletionListener = l } /** * Register a callback to be invoked when an error occurs * during playback or setup. If no listener is specified, * or if the listener returned false, VideoView will inform * the user of any errors. * * @param l The callback that will be run */ fun setOnErrorListener(l: IMediaPlayer.OnErrorListener?) { mOnErrorListener = l } /** * Register a callback to be invoked when an informational event * occurs during playback or setup. * * @param l The callback that will be run */ fun setOnInfoListener(l: IMediaPlayer.OnInfoListener?) { mOnInfoListener = l } // REMOVED: mSHCallback private fun bindSurfaceHolder(mp: IMediaPlayer?, holder: ISurfaceHolder?) { if (mp == null) return if (holder == null) { mp.setDisplay(null) return } holder.bindToMediaPlayer(mp) } fun releaseWithoutStop() { if (mMediaPlayer != null) mMediaPlayer!!.setDisplay(null) } /* * release the media player in any state */ fun release(clearTargetState: Boolean) { if (mMediaPlayer != null) { mMediaPlayer!!.reset() mMediaPlayer!!.release() mMediaPlayer = null mMediaController!!.hide() if (clearTargetState) { mTargetState = STATE_IDLE } setRenderView(null) initRenders() val am = mAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager am.abandonAudioFocus(null) cancelProgressRunnable() } } override fun onTrackballEvent(ev: MotionEvent?): Boolean { if (isInPlaybackState() && mMediaController != null) { toggleMediaControlsVisible() } return false } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { val isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && keyCode != KeyEvent.KEYCODE_VOLUME_UP && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && keyCode != KeyEvent.KEYCODE_MENU && keyCode != KeyEvent.KEYCODE_CALL && keyCode != KeyEvent.KEYCODE_ENDCALL if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { if (mMediaPlayer!!.isPlaying) { pause() mMediaController!!.show() } else { start() mMediaController!!.hide() } return true } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { if (!mMediaPlayer!!.isPlaying) { start() mMediaController!!.hide() } return true } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { if (mMediaPlayer!!.isPlaying) { pause() mMediaController!!.show() } return true } else { toggleMediaControlsVisible() } } return super.onKeyDown(keyCode, event) } fun toggleMediaControlsVisible() { if (mMediaController!!.isShowing) { mMediaController?.hide() } else { mMediaController?.show() } } private var mDisposable: Disposable? = null /** * 进度与时间更新线程 */ private fun startProgressRunnable() { if (mDisposable == null || mDisposable!!.isDisposed) { mDisposable = Observable.interval(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { val position = currentPosition val duration = duration if (duration >= 0 && bufferPercentage > 0) { val progress = 1000L * position / duration val secondProgress = bufferPercentage * 10 //发送进度 val event = VideoProgressEvent(duration, position, progress.toInt(), secondProgress) //这里在更新进度条时,有可能在切换控制器,所以这里要排除空的情况 if (mMediaController != null) { mMediaController!!.updateProgressAndTime(event) } RxBus.post(event) } } } } /** * 取消进度与时间更新线程 */ private fun cancelProgressRunnable() { if (mDisposable != null && !mDisposable!!.isDisposed) { mDisposable?.dispose() mDisposable = null } } override fun start() { if (isInPlaybackState()) { mMediaPlayer?.start() mCurrentState = STATE_PLAYING startProgressRunnable() } mTargetState = STATE_PLAYING } override fun pause() { if (isInPlaybackState()) { if (mMediaPlayer!!.isPlaying) { mMediaPlayer?.pause() mCurrentState = STATE_PAUSED cancelProgressRunnable() } } mTargetState = STATE_PAUSED } fun suspend() { release(false) cancelProgressRunnable() } fun resume() { openVideo() } override fun getDuration(): Int { return if (isInPlaybackState()) { mMediaPlayer?.duration!!.toInt() } else -1 } override fun getCurrentPosition(): Int { return if (isInPlaybackState()) { mMediaPlayer?.currentPosition!!.toInt() } else 0 } override fun seekTo(msec: Int) { mSeekWhenPrepared = if (isInPlaybackState()) { mMediaPlayer?.seekTo(msec.toLong()) 0 } else { msec } } override fun isPlaying(): Boolean { return isInPlaybackState() && mMediaPlayer!!.isPlaying } override fun getBufferPercentage(): Int { return if (mMediaPlayer != null) { mCurrentBufferPercentage } else 0 } fun isInPlaybackState(): Boolean { return mMediaPlayer != null && mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE && mCurrentState != STATE_PREPARING } override fun canPause() = mCanPause override fun canSeekBackward() = mCanSeekBack override fun canSeekForward() = mCanSeekForward override fun getAudioSessionId(): Int { return 0 } private fun createPlayer(): IMediaPlayer? { if (mUri != null) { return IjkMediaPlayer().apply { IjkMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG) setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0) setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0) setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32.toLong()) setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1) setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0) setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0) setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48) } } return null } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/IjkVideoViewWrapper.kt ================================================ package com.jennifer.andy.simpleeyes.player import android.app.Dialog import android.content.Context import android.content.pm.ActivityInfo import android.graphics.drawable.ColorDrawable import android.media.AudioManager import android.provider.Settings import android.provider.Settings.SettingNotFoundException import android.util.AttributeSet import android.view.* import android.widget.FrameLayout import android.widget.ProgressBar import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.player.view.ControllerViewFactory import com.jennifer.andy.simpleeyes.utils.* import tv.danmaku.ijk.media.player.IMediaPlayer import kotlin.math.abs /** * Author: andy.xwt * Date: 2020/3/14 13:53 * Description: IjkVideoViewWrapper 包含了对声音亮度的控制,同时代理了[IjkVideoView]的相关方法 */ class IjkVideoViewWrapper @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), GestureDetector.OnGestureListener { private val mIjkVideoView: IjkVideoView private val mPlaceImage: SimpleDraweeView private val mCenterProgress: ProgressBar private val mGestureDetector: GestureDetector private val mAudioManager: AudioManager private var mLightDialog: Dialog? = null private var mLightProgress: ProgressBar? = null private var mVolumeDialog: Dialog? = null private var mVolumeProgress: ProgressBar? = null private var isShowVolume = false private var isShowLight = false private var isShowPosition = false private val mScreenHeight: Int private var mParent: ViewGroup? = null companion object { private const val MIN_SCROLL = 3 } init { LayoutInflater.from(context).inflate(R.layout.layout_ijk_wrapper, this, true) mGestureDetector = GestureDetector(context, this) mAudioManager = getContext().getSystemService(Context.AUDIO_SERVICE) as AudioManager mScreenHeight = getContext().getScreenHeight() mIjkVideoView = findViewById(R.id.video_view) mPlaceImage = findViewById(R.id.iv_place_image) mCenterProgress = findViewById(R.id.progress) } fun setPlaceImageUrl(url: String?) { togglePlaceImage(true) mPlaceImage.setImageURI(url) } fun togglePlaceImage(visibility: Boolean) { mPlaceImage.visibility = if (visibility) View.VISIBLE else View.GONE mCenterProgress.visibility = if (visibility) View.VISIBLE else View.GONE } fun hidePlaceImage() { mPlaceImage.handler.postDelayed({ togglePlaceImage(false) }, 500) } override fun onTouchEvent(event: MotionEvent): Boolean { val relValue = mGestureDetector.onTouchEvent(event) val action = event.action if (action == MotionEvent.ACTION_UP) { //当手指抬起的时候 onUp() } else if (action == MotionEvent.ACTION_CANCEL) { onCancel() } return relValue } override fun onDown(e: MotionEvent?): Boolean { // 单击,触摸屏按下时立刻触发 return true } override fun onShowPress(e: MotionEvent?) { // 短按,触摸屏按下后片刻后抬起,会触发这个手势,如果迅速抬起则不会 } override fun onSingleTapUp(e: MotionEvent?): Boolean { // 抬起,手指离开触摸屏时触发(长按、滚动、滑动时,不会触发这个手势) if (mIjkVideoView.isInPlaybackState() && mIjkVideoView.getMediaController() != null && (!isShowVolume || !isShowLight || isShowPosition)) { mIjkVideoView.toggleMediaControlsVisible() } return true } // 滚动,触摸屏按下后移动 override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { if (mIjkVideoView.screenState == ControllerViewFactory.FULL_SCREEN_MODE) { //判断是否是全屏 val downX = e1.x val actuallyDy = e2.y - e1.y val actuallyDx = e2.x - e1.x val absDy = abs(actuallyDy) val absDx = abs(actuallyDx) //左右移动 if (absDx >= MIN_SCROLL && absDx > absDy) { showMoveToPositionDialog() } //上下移动 if (abs(distanceY) >= MIN_SCROLL && absDy > absDx) { if (downX <= mScreenHeight * 0.5f) { //左边,改变声音 showVolumeDialog(distanceY) } else { //右边改变亮度 showLightDialog(distanceY) } } } return true } /** * 移动到相应位置 */ private fun showMoveToPositionDialog() { // TODO: 2018/2/26 xwt 移动到相应的位置 isShowPosition = true } private fun onCancel() { onUp() } private fun onUp() { dismissLightDialog() dismissVolumeDialog() } //手势识别中不需要使用的方法 override fun onLongPress(e: MotionEvent?) {} //手势识别中不需要使用的方法 override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean { return false } /** * 显示声音控制对话框 */ private fun showVolumeDialog(deltaY: Float) { isShowVolume = true //记录滑动时候当前的声音 var currentVideoVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC) val maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) if (deltaY < 0) { //向下滑动 currentVideoVolume-- } else { //向下滑动 currentVideoVolume++ } //设置声音 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVideoVolume, 0) if (mVolumeDialog == null) { val view = LayoutInflater.from(context).inflate(R.layout.dialog_volume_controller, null) mVolumeProgress = view.findViewById(R.id.pb_volume_progress) mVolumeDialog = createDialogWithView(view, Gravity.START or Gravity.CENTER_VERTICAL) } if (!mVolumeDialog!!.isShowing) { mVolumeDialog!!.show() } //设置进度条 val nextVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC) var volumePercent = (nextVolume * 1f / maxVolume * 100f).toInt() if (volumePercent > 100) { volumePercent = 100 } else if (volumePercent < 0) { volumePercent = 0 } mVolumeProgress!!.progress = volumePercent } /** * 隐藏声音对话框 */ private fun dismissVolumeDialog() { if (mVolumeDialog != null) { mVolumeDialog!!.dismiss() isShowVolume = false } } /** * 显示控制亮度对话框 */ private fun showLightDialog(deltaY: Float) { isShowLight = true var screenBrightness = 0f //记录滑动前的亮度 val lp = getWindow(context)!!.attributes if (lp.screenBrightness < 0) { try { screenBrightness = Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS).toFloat() } catch (e: SettingNotFoundException) { e.printStackTrace() } } else { screenBrightness = lp.screenBrightness } if (deltaY < 0) { //向下滑动 screenBrightness -= 0.03f } else { //向下滑动 screenBrightness += 0.03f } when { screenBrightness >= 1 -> { lp.screenBrightness = 1f } screenBrightness < 0 -> { lp.screenBrightness = 0.1f } else -> { lp.screenBrightness = screenBrightness } } getWindow(context)!!.attributes = lp //设置亮度百分比 if (mLightProgress == null) { val view = LayoutInflater.from(context).inflate(R.layout.dialog_light_controller, null) mLightProgress = view.findViewById(R.id.pb_light_progress) mLightDialog = createDialogWithView(view, Gravity.END or Gravity.CENTER_VERTICAL) } if (!mLightDialog!!.isShowing) { mLightDialog?.show() } val lightPercent = (screenBrightness * 100f).toInt() mLightProgress?.progress = lightPercent } private fun dismissLightDialog() { mLightDialog?.let { mLightDialog!!.dismiss() isShowLight = false } } /** * 根据View与位置创建dialog * * @param localView 内容布局 * @param gravity 位置 */ fun createDialogWithView(localView: View?, gravity: Int): Dialog { return Dialog(context, R.style.VideoProgress).apply { setContentView(localView) window.apply { addFlags(Window.FEATURE_ACTION_BAR) addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL) addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) setLayout(-2, -2) setBackgroundDrawable(ColorDrawable()) val localLayoutParams = attributes localLayoutParams.gravity = gravity attributes = localLayoutParams } } } /** * 进入全屏 */ fun enterFullScreen() { hideActionBar(context) getActivity(context)!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE val contentView = getActivity(context)!!.findViewById(Window.ID_ANDROID_CONTENT) mParent = parent as ViewGroup mParent!!.removeView(this) mIjkVideoView.screenState = ControllerViewFactory.FULL_SCREEN_MODE val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) contentView.addView(this, params) } /** * 退出全屏 */ fun exitFullScreen() { showActionBar(context) val contentView = getActivity(context)!!.findViewById(Window.ID_ANDROID_CONTENT) getActivity(context)!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT contentView.removeView(this) mIjkVideoView.screenState = ControllerViewFactory.TINY_MODE val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) mParent!!.addView(this, params) } /////////////////////////////////////////////////////////////////////////// // 代理IjkVideoView方法 /////////////////////////////////////////////////////////////////////////// fun setVideoPath(playUrl: String) { mIjkVideoView.setVideoPath(playUrl) } fun start() { mIjkVideoView.start() } fun setMediaController(controller: IjkMediaController?) { mIjkVideoView.setMediaController(controller) } fun toggleAspectRatio(currentAspectRatio: Int) { mIjkVideoView.toggleAspectRatio(currentAspectRatio) } fun setOnPreparedListener(onPreparedListener: IMediaPlayer.OnPreparedListener) { mIjkVideoView.setOnPreparedListener(onPreparedListener) } fun setOnErrorListener(onErrorListener: IMediaPlayer.OnErrorListener) { mIjkVideoView.setOnErrorListener(onErrorListener) } fun setOnCompletionListener(onCompletionListener: IMediaPlayer.OnCompletionListener) { mIjkVideoView.setOnCompletionListener(onCompletionListener) } fun stopPlayback() { mIjkVideoView.stopPlayback() } fun isPlaying(): Boolean { return mIjkVideoView.isPlaying } fun pause() { mIjkVideoView.pause() } fun release(clearTargetState: Boolean) { mIjkVideoView.release(clearTargetState) } fun showErrorView(): Boolean { mCenterProgress.handler.postDelayed({ mCenterProgress.visibility = View.GONE mIjkVideoView.getMediaController()!!.showErrorView() }, 1000) return false } fun resetType() { mIjkVideoView.getMediaController()?.resetType() } fun setDragging(dragging: Boolean) { mIjkVideoView.getMediaController()!!.controllerView!!.isDragging = dragging } fun isDragging(): Boolean { return mIjkVideoView.getMediaController()!!.controllerView!!.isDragging } fun showControllerAllTheTime() { mIjkVideoView.getMediaController()?.showAllTheTime() } fun getDuration(): Int { return mIjkVideoView.duration } fun showController() { mIjkVideoView.getMediaController()!!.show() } fun seekTo(msec: Int) { mIjkVideoView.seekTo(msec) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/MeasureHelper.kt ================================================ package com.jennifer.andy.simpleeyes.player import android.view.View import com.jennifer.andy.simpleeyes.player.render.IRenderView import java.lang.ref.WeakReference /** * Author: andy.xwt * Date: 2020/3/9 15:09 * Description: */ /** * 测量帮助类,根据宽高比及方向,获得测量后的宽高 */ class MeasureHelper(view: View) { private val mWeakView: WeakReference? private var mVideoWidth = 0//视频布局的宽度 private var mVideoHeight = 0//视频布局的高度 private var mVideoSarNum = 0 //实际视频的宽 private var mVideoSarDen = 0//实际视频的高 private var mVideoRotationDegree = 0 //屏幕旋转度数 var measuredWidth = 0//测量后的宽 var measuredHeight = 0 //测量后的高 private var mCurrentAspectRatio = IRenderView.AR_ASPECT_FIT_PARENT //宽高比例 val view: View? get() = mWeakView?.get() init { mWeakView = WeakReference(view) } fun setVideoSize(videoWidth: Int, videoHeight: Int) { mVideoWidth = videoWidth mVideoHeight = videoHeight } fun setVideoSampleAspectRatio(videoSarNum: Int, videoSarDen: Int) { mVideoSarNum = videoSarNum mVideoSarDen = videoSarDen } fun setVideoRotation(videoRotationDegree: Int) { mVideoRotationDegree = videoRotationDegree } /** * 当 View.onMeasure(int, int) 时调用,用于输出视频校正后的宽高 * * @param widthMeasureSpec 宽度测量规则 * @param heightMeasureSpec 高度测量规则 */ fun doMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { var widthMeasureSpec = widthMeasureSpec var heightMeasureSpec = heightMeasureSpec if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) { //如果屏幕为横屏,将宽高进行对调 val tempSpec = widthMeasureSpec widthMeasureSpec = heightMeasureSpec heightMeasureSpec = tempSpec } //获取父布局宽高 var width = View.getDefaultSize(mVideoWidth, widthMeasureSpec) var height = View.getDefaultSize(mVideoHeight, heightMeasureSpec) if (mCurrentAspectRatio == IRenderView.AR_MATCH_PARENT) { //如果宽高比例是匹配父布局,宽高不变 width = widthMeasureSpec height = heightMeasureSpec } //设置的视频宽高大于0时 else if (mVideoWidth > 0 && mVideoHeight > 0) { val widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec) val widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec) val heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec) val heightSpecSize = View.MeasureSpec.getSize(heightMeasureSpec) //当前控件测量模式为包裹模式时,根据视频比例与渲染比例,计算出视频显示的宽高比 if (widthSpecMode == View.MeasureSpec.AT_MOST && heightSpecMode == View.MeasureSpec.AT_MOST) { val specAspectRatio = widthSpecSize.toFloat() / heightSpecSize.toFloat() var displayAspectRatio: Float when (mCurrentAspectRatio) { IRenderView.AR_16_9_FIT_PARENT -> { displayAspectRatio = 16.0f / 9.0f if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) displayAspectRatio = 1.0f / displayAspectRatio } IRenderView.AR_4_3_FIT_PARENT -> { displayAspectRatio = 4.0f / 3.0f if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) displayAspectRatio = 1.0f / displayAspectRatio } IRenderView.AR_ASPECT_FIT_PARENT, IRenderView.AR_ASPECT_FILL_PARENT, IRenderView.AR_ASPECT_WRAP_CONTENT -> { displayAspectRatio = mVideoWidth.toFloat() / mVideoHeight.toFloat() if (mVideoSarNum > 0 && mVideoSarDen > 0) displayAspectRatio = displayAspectRatio * mVideoSarNum / mVideoSarDen } else -> { displayAspectRatio = mVideoWidth.toFloat() / mVideoHeight.toFloat() if (mVideoSarNum > 0 && mVideoSarDen > 0) displayAspectRatio = displayAspectRatio * mVideoSarNum / mVideoSarDen } } val shouldBeWider = displayAspectRatio > specAspectRatio when (mCurrentAspectRatio) { //以下三种模式,以大的为固定边,小的边按比例缩放 IRenderView.AR_ASPECT_FIT_PARENT, IRenderView.AR_16_9_FIT_PARENT, IRenderView.AR_4_3_FIT_PARENT -> if (shouldBeWider) { //高度偏小,则宽度不变,对高进行校正 width = widthSpecSize height = (width / displayAspectRatio).toInt() } else { //宽度偏小,则高度不变,对宽进行校正 height = heightSpecSize width = (height * displayAspectRatio).toInt() } //填充模式下,以小的为固定边,大的边按比例缩放 IRenderView.AR_ASPECT_FILL_PARENT -> if (shouldBeWider) { //高度偏小,则高度不变,对宽进行校正 height = heightSpecSize width = (height * displayAspectRatio).toInt() } else { //宽度偏小,则宽度不变,对高进行校正 width = widthSpecSize height = (width / displayAspectRatio).toInt() } //包裹模式,以视频采样的宽高为准。 IRenderView.AR_ASPECT_WRAP_CONTENT -> if (shouldBeWider) { //高度偏小,将视频宽度与父布局宽度进行比较,取最小。并重新计算高度 width = mVideoWidth.coerceAtMost(widthSpecSize) height = (width / displayAspectRatio).toInt() } else { //宽度偏小,将视频高度与父布局高度进行比较,取最小。重新计算宽度 height = mVideoHeight.coerceAtMost(heightSpecSize) width = (height * displayAspectRatio).toInt() } //默认情况下 else -> if (shouldBeWider) { width = mVideoWidth.coerceAtMost(widthSpecSize) height = (width / displayAspectRatio).toInt() } else { height = mVideoHeight.coerceAtMost(heightSpecSize) width = (height * displayAspectRatio).toInt() } } } //视频宽高为准确模式下,以小的为固定边,大的边按比例缩放 else if (widthSpecMode == View.MeasureSpec.EXACTLY && heightSpecMode == View.MeasureSpec.EXACTLY) { width = widthSpecSize height = heightSpecSize if (mVideoWidth * height < width * mVideoHeight) {// videoWidth/videoHeight < width / height //宽度过大,重新调整宽度 width = height * mVideoWidth / mVideoHeight } else if (mVideoWidth * height > width * mVideoHeight) { //高度过大,重新调整高度 height = width * mVideoHeight / mVideoWidth } } // 只有宽在准确模式下,宽度以实际为准,高度不能超过父布局 else if (widthSpecMode == View.MeasureSpec.EXACTLY) { width = widthSpecSize height = width * mVideoHeight / mVideoWidth if (heightSpecMode == View.MeasureSpec.AT_MOST && height > heightSpecSize) { height = heightSpecSize } } //只有高在准确模式模式下,高度以实际为准,宽度不能超过父布局 else if (heightSpecMode == View.MeasureSpec.EXACTLY) { height = heightSpecSize width = height * mVideoWidth / mVideoHeight if (widthSpecMode == View.MeasureSpec.AT_MOST && width > widthSpecSize) { width = widthSpecSize } } // 当宽高都不固定时,使用实际的视频宽高 else { width = mVideoWidth height = mVideoHeight if (heightSpecMode == View.MeasureSpec.AT_MOST && height > heightSpecSize) { //太高,重新校正宽高 height = heightSpecSize width = height * mVideoWidth / mVideoHeight } if (widthSpecMode == View.MeasureSpec.AT_MOST && width > widthSpecSize) { //太宽,重新校正宽高 width = widthSpecSize height = width * mVideoHeight / mVideoWidth } } } else { // no size yet, just adopt the given spec sizes } measuredWidth = width measuredHeight = height } fun setAspectRatio(aspectRatio: Int) { mCurrentAspectRatio = aspectRatio } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/PolicyCompat.kt ================================================ package com.jennifer.andy.simpleeyes.player import android.content.Context import android.view.Window /** * Author: andy.xwt * Date: 2020/3/9 15:07 * Description: */ private const val PHONE_WINDOW_CLASS_NAME = "com.android.internal.policy.PhoneWindow" private const val POLICY_MANAGER_CLASS_NAME = "com.android.internal.policy.PolicyManager" class PolicyCompat { /** * 通过反射拿不到phoneWindow,当该方法失败时,通过[makeNewWindow] */ fun createPhoneWindow(context: Context): Window { return try { val clazz = Class.forName(PHONE_WINDOW_CLASS_NAME) val c = clazz.getConstructor(Context::class.java) c.newInstance(context) as Window } catch (e: Exception) { makeNewWindow(context) } } private fun makeNewWindow(context: Context): Window { return try { val clazz = Class.forName(POLICY_MANAGER_CLASS_NAME) val m = clazz.getMethod("makeNewWindow", Context::class.java) m.invoke(null, context) as Window } catch (e: Exception) { throw RuntimeException(e.message) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/event/VideoProgressEvent.kt ================================================ package com.jennifer.andy.simpleeyes.player.event /** * Author: andy.xwt * Date: 2020/3/9 10:02 PM * Description: 视频播放进度事件 */ data class VideoProgressEvent( var duration: Int,//总时间 var currentPosition: Int,//当前播放时间 var progress: Int,//第一进度(播放进度) var secondaryProgress: Int//第二进度(下载进度) ) ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/render/IRenderView.kt ================================================ package com.jennifer.andy.simpleeyes.player.render import android.graphics.SurfaceTexture import android.view.Surface import android.view.SurfaceHolder import android.view.View import tv.danmaku.ijk.media.player.IMediaPlayer /** * Author: andy.xwt * Date: 2020/3/9 22:44 * Description: */ interface IRenderView { companion object { const val AR_ASPECT_FIT_PARENT = 0 // 适应父布局模式 const val AR_ASPECT_FILL_PARENT = 1 //填充父布局模式 const val AR_ASPECT_WRAP_CONTENT = 2 //包裹模式 const val AR_MATCH_PARENT = 3 //匹配父布局模式 const val AR_16_9_FIT_PARENT = 4 //16:9适配屏幕 const val AR_4_3_FIT_PARENT = 5 //4:3适配屏幕 } /** * 获取当前view */ fun getView(): View? /** * 是否需要等待重新测量 */ fun shouldWaitForResize(): Boolean /** * 设置视屏的宽高 */ fun setVideoSize(videoWidth: Int, videoHeight: Int) /** * 设置视频采样方向比例 */ fun setVideoSampleAspectRatio(videoSarNum: Int, videoSarDen: Int) /** * 设置视频旋转角度 */ fun setVideoRotation(degree: Int) /** * 设置方向比例 */ fun setAspectRatio(aspectRatio: Int) /** * 设置渲染回调 */ fun addRenderCallback(callback: IRenderCallback) /** * 移除渲染回调 */ fun removeRenderCallback(callback: IRenderCallback) /** * surfaceHolder管理相关接口 */ interface ISurfaceHolder { /** * 绑定当前媒体播放器 */ fun bindToMediaPlayer(mp: IMediaPlayer?) /** * 获取当前渲染的view */ val renderView: IRenderView /** * 获取当前surfaceHolder */ val surfaceHolder: SurfaceHolder? /** * 打开surface */ fun openSurface(): Surface? /** * 获取SurfaceTexture */ val surfaceTexture: SurfaceTexture? } interface IRenderCallback { fun onSurfaceCreated(holder: ISurfaceHolder, width: Int, height: Int) fun onSurfaceChanged(holder: ISurfaceHolder, format: Int, width: Int, height: Int) fun onSurfaceDestroyed(holder: ISurfaceHolder) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/render/SurfaceRenderView.kt ================================================ package com.jennifer.andy.simpleeyes.player.render import android.annotation.TargetApi import android.content.Context import android.graphics.SurfaceTexture import android.os.Build import android.util.AttributeSet import android.util.Log import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import com.jennifer.andy.simpleeyes.player.MeasureHelper import com.jennifer.andy.simpleeyes.player.render.IRenderView.IRenderCallback import com.jennifer.andy.simpleeyes.player.render.IRenderView.ISurfaceHolder import tv.danmaku.ijk.media.player.IMediaPlayer import tv.danmaku.ijk.media.player.ISurfaceTextureHolder import java.lang.ref.WeakReference import java.util.concurrent.ConcurrentHashMap /** * Author: andy.xwt * Date: 2020/3/9 22:47 * Description:surfaceView渲染界面 */ class SurfaceRenderView : SurfaceView, IRenderView { private lateinit var mMeasureHelper: MeasureHelper constructor(context: Context) : super(context) { initView(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { initView(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initView(context) } @TargetApi(Build.VERSION_CODES.LOLLIPOP) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initView(context) } private fun initView(context: Context) { mMeasureHelper = MeasureHelper(this) mSurfaceCallback = SurfaceCallback(this) holder.addCallback(mSurfaceCallback) holder.setType(SurfaceHolder.SURFACE_TYPE_NORMAL) } override fun getView() = this override fun shouldWaitForResize() = true //-------------------- // Layout & Measure //-------------------- override fun setVideoSize(videoWidth: Int, videoHeight: Int) { if (videoWidth > 0 && videoHeight > 0) { mMeasureHelper.setVideoSize(videoWidth, videoHeight) holder.setFixedSize(videoWidth, videoHeight) requestLayout() } } override fun setVideoSampleAspectRatio(videoSarNum: Int, videoSarDen: Int) { if (videoSarNum > 0 && videoSarDen > 0) { mMeasureHelper.setVideoSampleAspectRatio(videoSarNum, videoSarDen) requestLayout() } } override fun setVideoRotation(degree: Int) { Log.e("", "SurfaceView doesn't support rotation ($degree)!\n") } override fun setAspectRatio(aspectRatio: Int) { mMeasureHelper.setAspectRatio(aspectRatio) requestLayout() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { mMeasureHelper.doMeasure(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension(mMeasureHelper.measuredWidth, mMeasureHelper.measuredHeight) } //-------------------- // SurfaceViewHolder //-------------------- private class InternalSurfaceHolder(private val surfaceView: SurfaceRenderView, override val surfaceHolder: SurfaceHolder?) : ISurfaceHolder { override fun bindToMediaPlayer(mp: IMediaPlayer?) { if (mp != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && mp is ISurfaceTextureHolder) { val textureHolder = mp as ISurfaceTextureHolder textureHolder.surfaceTexture = null } mp.setDisplay(surfaceHolder) } } override val renderView: IRenderView get() = surfaceView override val surfaceTexture: SurfaceTexture? get() = null override fun openSurface(): Surface? { return surfaceHolder?.surface } } //------------------------- // SurfaceHolder.Callback //------------------------- override fun addRenderCallback(callback: IRenderCallback) { mSurfaceCallback.addRenderCallback(callback) } override fun removeRenderCallback(callback: IRenderCallback) { mSurfaceCallback.removeRenderCallback(callback) } private lateinit var mSurfaceCallback: SurfaceCallback private class SurfaceCallback(surfaceView: SurfaceRenderView) : SurfaceHolder.Callback { private var mSurfaceHolder: SurfaceHolder? = null private var mIsFormatChanged = false private var mFormat = 0 private var mWidth = 0 private var mHeight = 0 private val mWeakSurfaceView: WeakReference = WeakReference(surfaceView) private val mRenderCallbackMap: MutableMap = ConcurrentHashMap() fun addRenderCallback(callback: IRenderCallback) { mRenderCallbackMap[callback] = callback var surfaceHolder: ISurfaceHolder? = null if (mSurfaceHolder != null) { if (surfaceHolder == null) surfaceHolder = InternalSurfaceHolder(mWeakSurfaceView.get()!!, mSurfaceHolder) callback.onSurfaceCreated(surfaceHolder, mWidth, mHeight) } if (mIsFormatChanged) { if (surfaceHolder == null) surfaceHolder = InternalSurfaceHolder(mWeakSurfaceView.get()!!, mSurfaceHolder) callback.onSurfaceChanged(surfaceHolder, mFormat, mWidth, mHeight) } } fun removeRenderCallback(callback: IRenderCallback) { mRenderCallbackMap.remove(callback) } override fun surfaceCreated(holder: SurfaceHolder) { mSurfaceHolder = holder mIsFormatChanged = false mFormat = 0 mWidth = 0 mHeight = 0 //调用监听 val surfaceHolder: ISurfaceHolder = InternalSurfaceHolder(mWeakSurfaceView.get()!!, mSurfaceHolder) for (renderCallback in mRenderCallbackMap.keys) { renderCallback.onSurfaceCreated(surfaceHolder, 0, 0) } } override fun surfaceDestroyed(holder: SurfaceHolder) { mSurfaceHolder = null mIsFormatChanged = false mFormat = 0 mWidth = 0 mHeight = 0 val surfaceHolder: ISurfaceHolder = InternalSurfaceHolder(mWeakSurfaceView.get()!!, mSurfaceHolder) for (renderCallback in mRenderCallbackMap.keys) { renderCallback.onSurfaceDestroyed(surfaceHolder) } } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { mSurfaceHolder = holder mIsFormatChanged = true mFormat = format mWidth = width mHeight = height // mMeasureHelper.setVideoSize(width, height); val surfaceHolder: ISurfaceHolder = InternalSurfaceHolder(mWeakSurfaceView.get()!!, mSurfaceHolder) for (renderCallback in mRenderCallbackMap.keys) { renderCallback.onSurfaceChanged(surfaceHolder, format, width, height) } } } //-------------------- // Accessibility //-------------------- override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) { super.onInitializeAccessibilityEvent(event) event.className = SurfaceRenderView::class.java.name } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(info) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { info.className = SurfaceRenderView::class.java.name } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/render/TextureRenderView.kt ================================================ package com.jennifer.andy.simpleeyes.player.render import android.annotation.TargetApi import android.content.Context import android.graphics.SurfaceTexture import android.os.Build import android.util.AttributeSet import android.util.Log import android.view.Surface import android.view.SurfaceHolder import android.view.TextureView import android.view.View import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import com.jennifer.andy.simpleeyes.player.MeasureHelper import com.jennifer.andy.simpleeyes.player.render.IRenderView.IRenderCallback import com.jennifer.andy.simpleeyes.player.render.IRenderView.ISurfaceHolder import tv.danmaku.ijk.media.player.IMediaPlayer import tv.danmaku.ijk.media.player.ISurfaceTextureHolder import tv.danmaku.ijk.media.player.ISurfaceTextureHost import java.lang.ref.WeakReference import java.util.concurrent.ConcurrentHashMap /** * Author: andy.xwt * Date: 2020/3/9 22:56 * Description: */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) class TextureRenderView : TextureView, IRenderView { private lateinit var mMeasureHelper: MeasureHelper constructor(context: Context) : super(context) { initView(context) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { initView(context) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initView(context) } @TargetApi(Build.VERSION_CODES.LOLLIPOP) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initView(context) } private fun initView(context: Context) { mMeasureHelper = MeasureHelper(this) mSurfaceCallback = SurfaceCallback(this) surfaceTextureListener = mSurfaceCallback } override fun getView(): View { return this } override fun shouldWaitForResize(): Boolean { return false } override fun onDetachedFromWindow() { mSurfaceCallback.willDetachFromWindow() super.onDetachedFromWindow() mSurfaceCallback.didDetachFromWindow() } //-------------------- // Layout & Measure //-------------------- override fun setVideoSize(videoWidth: Int, videoHeight: Int) { if (videoWidth > 0 && videoHeight > 0) { mMeasureHelper.setVideoSize(videoWidth, videoHeight) requestLayout() } } override fun setVideoSampleAspectRatio(videoSarNum: Int, videoSarDen: Int) { if (videoSarNum > 0 && videoSarDen > 0) { mMeasureHelper.setVideoSampleAspectRatio(videoSarNum, videoSarDen) requestLayout() } } override fun setVideoRotation(degree: Int) { mMeasureHelper.setVideoRotation(degree) rotation = degree.toFloat() } override fun setAspectRatio(aspectRatio: Int) { mMeasureHelper.setAspectRatio(aspectRatio) requestLayout() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { mMeasureHelper.doMeasure(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension(mMeasureHelper.measuredWidth, mMeasureHelper.measuredHeight) } //-------------------- // TextureViewHolder //-------------------- fun getSurfaceHolder(): ISurfaceHolder { return InternalSurfaceHolder(this, mSurfaceCallback.mSurfaceTexture, mSurfaceCallback) } /** * 内部surfaceHolder */ private class InternalSurfaceHolder(private val textureView: TextureRenderView, override val surfaceTexture: SurfaceTexture?, private val mSurfaceTextureHost: ISurfaceTextureHost) : ISurfaceHolder { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) override fun bindToMediaPlayer(mp: IMediaPlayer?) { if (mp == null) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && mp is ISurfaceTextureHolder) { val textureHolder = mp as ISurfaceTextureHolder textureView.mSurfaceCallback.setOwnSurfaceTexture(false) val surfaceTexture = textureHolder.surfaceTexture if (surfaceTexture != null) { textureView.surfaceTexture = surfaceTexture } else { textureHolder.surfaceTexture = this.surfaceTexture textureHolder.setSurfaceTextureHost(textureView.mSurfaceCallback) } } else { mp.setSurface(openSurface()) } } override val renderView: IRenderView get() = textureView override val surfaceHolder: SurfaceHolder? get() = null override fun openSurface(): Surface? { return if (surfaceTexture == null) null else Surface(surfaceTexture) } } //------------------------- // SurfaceHolder.Callback //------------------------- override fun addRenderCallback(callback: IRenderCallback) { mSurfaceCallback.addRenderCallback(callback) } override fun removeRenderCallback(callback: IRenderCallback) { mSurfaceCallback.removeRenderCallback(callback) } private lateinit var mSurfaceCallback: SurfaceCallback /** * SurfaceTexture监听 */ private class SurfaceCallback(renderView: TextureRenderView) : SurfaceTextureListener, ISurfaceTextureHost { var mSurfaceTexture: SurfaceTexture? = null private var mIsFormatChanged = false private var mWidth = 0 private var mHeight = 0 private var mOwnSurfaceTexture = true private var mWillDetachFromWindow = false private var mDidDetachFromWindow = false private val mWeakRenderView: WeakReference = WeakReference(renderView) private val mRenderCallbackMap: MutableMap = ConcurrentHashMap() fun setOwnSurfaceTexture(ownSurfaceTexture: Boolean) { mOwnSurfaceTexture = ownSurfaceTexture } /** * 添加渲染回调 */ fun addRenderCallback(callback: IRenderCallback) { mRenderCallbackMap[callback] = callback var surfaceHolder: ISurfaceHolder? = null if (mSurfaceTexture != null) { if (surfaceHolder == null) surfaceHolder = InternalSurfaceHolder(mWeakRenderView.get()!!, mSurfaceTexture, this) callback.onSurfaceCreated(surfaceHolder, mWidth, mHeight) } if (mIsFormatChanged) { if (surfaceHolder == null) surfaceHolder = InternalSurfaceHolder(mWeakRenderView.get()!!, mSurfaceTexture, this) callback.onSurfaceChanged(surfaceHolder, 0, mWidth, mHeight) } } fun removeRenderCallback(callback: IRenderCallback) { mRenderCallbackMap.remove(callback) } /** * 当纹理面板可用的时候 */ override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { mSurfaceTexture = surface mIsFormatChanged = false mWidth = 0 mHeight = 0 val surfaceHolder: ISurfaceHolder = InternalSurfaceHolder(mWeakRenderView.get()!!, surface, this) for (renderCallback in mRenderCallbackMap.keys) { renderCallback.onSurfaceCreated(surfaceHolder, 0, 0) } } /** * 当纹理面板大小发生改变的时候 */ override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { mSurfaceTexture = surface mIsFormatChanged = true mWidth = width mHeight = height val surfaceHolder: ISurfaceHolder = InternalSurfaceHolder(mWeakRenderView.get()!!, surface, this) for (renderCallback in mRenderCallbackMap.keys) { renderCallback.onSurfaceChanged(surfaceHolder, 0, width, height) } } /** * 当表面纹理摧毁的时候 */ override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { mSurfaceTexture = surface mIsFormatChanged = false mWidth = 0 mHeight = 0 val surfaceHolder: ISurfaceHolder = InternalSurfaceHolder(mWeakRenderView.get()!!, surface, this) for (renderCallback in mRenderCallbackMap.keys) { renderCallback.onSurfaceDestroyed(surfaceHolder) } Log.d(TAG, "onSurfaceTextureDestroyed: destroy: $mOwnSurfaceTexture") return mOwnSurfaceTexture } /** * 当表面纹理更新的时候 */ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {} //------------------------- // ISurfaceTextureHost //------------------------- override fun releaseSurfaceTexture(surfaceTexture: SurfaceTexture) { if (surfaceTexture == null) { Log.d(TAG, "releaseSurfaceTexture: null") } else if (mDidDetachFromWindow) { if (surfaceTexture !== mSurfaceTexture) { Log.d(TAG, "releaseSurfaceTexture: didDetachFromWindow(): release different SurfaceTexture") surfaceTexture.release() } else if (!mOwnSurfaceTexture) { Log.d(TAG, "releaseSurfaceTexture: didDetachFromWindow(): release detached SurfaceTexture") surfaceTexture.release() } else { Log.d(TAG, "releaseSurfaceTexture: didDetachFromWindow(): already released by TextureView") } } else if (mWillDetachFromWindow) { if (surfaceTexture !== mSurfaceTexture) { Log.d(TAG, "releaseSurfaceTexture: willDetachFromWindow(): release different SurfaceTexture") surfaceTexture.release() } else if (!mOwnSurfaceTexture) { Log.d(TAG, "releaseSurfaceTexture: willDetachFromWindow(): re-attach SurfaceTexture to TextureView") setOwnSurfaceTexture(true) } else { Log.d(TAG, "releaseSurfaceTexture: willDetachFromWindow(): will released by TextureView") } } else { if (surfaceTexture !== mSurfaceTexture) { Log.d(TAG, "releaseSurfaceTexture: alive: release different SurfaceTexture") surfaceTexture.release() } else if (!mOwnSurfaceTexture) { Log.d(TAG, "releaseSurfaceTexture: alive: re-attach SurfaceTexture to TextureView") setOwnSurfaceTexture(true) } else { Log.d(TAG, "releaseSurfaceTexture: alive: will released by TextureView") } } } /** * 将要从window消失 */ fun willDetachFromWindow() { Log.d(TAG, "willDetachFromWindow()") mWillDetachFromWindow = true } /** * 已经从window消失 */ fun didDetachFromWindow() { Log.d(TAG, "didDetachFromWindow()") mDidDetachFromWindow = true } } //-------------------- // Accessibility //-------------------- override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) { super.onInitializeAccessibilityEvent(event) event.className = TextureRenderView::class.java.name } override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(info) info.className = TextureRenderView::class.java.name } companion object { private const val TAG = "TextureRenderView" } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/view/ControllerView.kt ================================================ package com.jennifer.andy.simpleeyes.player.view import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.MediaController.MediaPlayerControl import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.player.IjkMediaController import com.jennifer.andy.simpleeyes.player.event.VideoProgressEvent import com.jennifer.andy.simpleeyes.utils.stringForTime import java.util.* /** * Author: andy.xwt * Date: 2020/3/9 23:00 * Description: */ abstract class ControllerView(context: Context) : FrameLayout(context) { protected lateinit var mPlayer: MediaPlayerControl protected lateinit var mController: IjkMediaController protected var mCurrentVideoInfo: ContentBean? = null var isDragging = false//是否正在拖动 private var mErrorView: ErrorView? = null private val mContentViews: MutableList = ArrayList() abstract val layoutId: Int fun initControllerAndData(player: MediaPlayerControl, controller: IjkMediaController, currentVideoInfo: ContentBean?) { mPlayer = player mController = controller mCurrentVideoInfo = currentVideoInfo LayoutInflater.from(context).inflate(layoutId, this, true) initView() initData() } /** * 初始化布局 */ abstract fun initView() /** * 初始化数据 */ abstract fun initData() /** * 显示 */ fun display() { updateTogglePauseUI(mPlayer.isPlaying) } /** * 更新暂停切换UI * * @param isPlaying 是否播放 */ open fun updateTogglePauseUI(isPlaying: Boolean) {} /** * 更新当前视频播放进度 * * @param progress 第一进度 * @param secondaryProgress 第二进度 */ open fun updateProgress(progress: Int, secondaryProgress: Int) {} /** * 更新当前视频播放时间 * * @param currentTime 当前时间 * @param endTime 结束时间 */ open fun updateTime(currentTime: String, endTime: String) {} /** * 根据传入的[videoProgressEvent] 更新播放进度与时间 * * @param videoProgressEvent 播放进度事件 */ fun updateProgressAndTime(videoProgressEvent: VideoProgressEvent) { if (!isDragging) { //没在拖动的时候跟新进度条 updateProgress(videoProgressEvent.progress, videoProgressEvent.secondaryProgress) updateTime(stringForTime(videoProgressEvent.currentPosition), stringForTime(videoProgressEvent.duration)) } } /** * 暂停切换 */ fun togglePause() { if (mPlayer.isPlaying) { mPlayer.pause() } else { mPlayer.start() } updateTogglePauseUI(mPlayer.isPlaying) } internal enum class State { ERROR } override fun addView(child: View?, params: ViewGroup.LayoutParams?) { super.addView(child, params) if (child != null && child.tag !== State.ERROR) mContentViews.add(child) } /** * 显示错误界面 */ fun showErrorView() { for (contentView in mContentViews) { contentView.visibility = View.GONE } if (mErrorView == null) { mErrorView = ErrorView(context).apply { setController(mController) tag = State.ERROR } addView(mErrorView) } else { mErrorView!!.visibility = View.VISIBLE } } /** * 显示内容界面,默认会将错误界面隐藏 */ fun showContent() { for (contentView in mContentViews) { contentView.visibility = View.VISIBLE } if (mErrorView != null) { mErrorView!!.visibility = View.GONE } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/view/ControllerViewFactory.kt ================================================ package com.jennifer.andy.simpleeyes.player.view import android.content.Context /** * Author: andy.xwt * Date: 2020/3/9 23:26 * Description: */ class ControllerViewFactory { companion object { const val TINY_MODE = 0 const val FULL_SCREEN_MODE = 1 } fun create(showMode: Int, context: Context): ControllerView { return when (showMode) { TINY_MODE -> TinyControllerView(context) FULL_SCREEN_MODE -> FullScreenControllerView(context) else -> throw IllegalArgumentException("not correct showMode") } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/view/ErrorView.kt ================================================ package com.jennifer.andy.simpleeyes.player.view import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ObjectAnimator import android.content.Context import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.ImageView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.player.IjkMediaController /** * Author: andy.xwt * Date: 2020/3/9 9:19 PM * Description: 错误界面,用于[ControllerView]显示的网络错误界面。 */ class ErrorView(context: Context) : FrameLayout(context), View.OnClickListener { private val mIvReload: ImageView private lateinit var mController: IjkMediaController fun setController(controller: IjkMediaController) { mController = controller } override fun onClick(v: View) { val rotation = ObjectAnimator.ofFloat(mIvReload, "rotation", 0f, 360f) rotation.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { //执行选中动画,然后开始刷新操作 mController.hide() mController.controllerListener?.onErrorViewClick() } }) rotation.duration = 500 rotation.start() } init { LayoutInflater.from(context).inflate(R.layout.layout_video_error, this, true) mIvReload = findViewById(R.id.iv_reload) setOnClickListener(this) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/view/FullScreenControllerView.kt ================================================ package com.jennifer.andy.simpleeyes.player.view import android.content.Context import android.view.View import android.widget.ImageView import android.widget.SeekBar import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.stringForTime import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView /** * Author: andy.xwt * Date: 2020/3/9 23:17 * Description: */ class FullScreenControllerView(context: Context) : ControllerView(context), View.OnClickListener { private lateinit var mMinScreen: ImageView private lateinit var mPreButton: ImageView private lateinit var mPauseButton: ImageView private lateinit var mNextButton: ImageView private lateinit var mProgress: SeekBar private lateinit var mTitle: CustomFontTextView private lateinit var mEndTime: CustomFontTextView private lateinit var mCurrentTime: CustomFontTextView private var mChangeProgress = 0 companion object { const val FULL_SCREEN_ID = 100 } override val layoutId = R.layout.layout_media_controller_full_screen override fun initView() { mMinScreen = findViewById(R.id.iv_min_screen) mProgress = findViewById(R.id.sb_progress) mTitle = findViewById(R.id.tv_title) mPreButton = findViewById(R.id.iv_previous) mPauseButton = findViewById(R.id.iv_pause) mNextButton = findViewById(R.id.iv_next) mProgress = findViewById(R.id.sb_progress) mCurrentTime = findViewById(R.id.tv_currentTime) mEndTime = findViewById(R.id.tv_end_time) initListener() } private fun initListener() { mPreButton.setOnClickListener(this) mMinScreen.setOnClickListener(this) mPauseButton.setOnClickListener(this) mNextButton.setOnClickListener(this) setOnClickListener(this) mProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onStartTrackingTouch(bar: SeekBar) { //控制的时候停止更新进度条,同时禁止隐藏 isDragging = true mController.showAllTheTime() } override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) { if (fromUser) { //更新当前播放时间 val duration = mPlayer.duration.toLong() val newPosition = duration * progress / 1000L mChangeProgress = progress mCurrentTime.text = stringForTime(newPosition.toInt()) } } override fun onStopTrackingTouch(bar: SeekBar) { //定位都拖动位置 val newPosition = mPlayer.duration * mChangeProgress / 1000L mPlayer.seekTo(newPosition.toInt()) mController.show() //开启延时隐藏 isDragging = false } }) } override fun initData() { //初始化标题 mTitle.text = mCurrentVideoInfo!!.title //初始化播放时间 val position = mPlayer.currentPosition val duration = mPlayer.duration mCurrentTime.text = stringForTime(position) mEndTime.text = stringForTime(duration) //初始化进度条 mProgress.apply { max = 1000 if (duration >= 0 && mPlayer.bufferPercentage > 0) { val firstProgress = 1000L * position / duration val secondProgress = mPlayer.bufferPercentage * 10 progress = firstProgress.toInt() secondaryProgress = secondProgress } setPadding(0, 0, 0, 0) } updatePreNextButton() id = FULL_SCREEN_ID } //判断是否显示上一个按钮与下一个按钮 private fun updatePreNextButton() { mPreButton.visibility = if (mController.isHavePreVideo()) View.VISIBLE else View.GONE mNextButton.visibility = if (mController.isHaveNextVideo()) View.VISIBLE else View.GONE } override fun onClick(v: View) { when (v.id) { FULL_SCREEN_ID -> mController.hide() R.id.iv_pause -> { togglePause() mController.show() } R.id.iv_previous -> { mController.controllerListener?.onPreClick() updatePreNextButton() } R.id.iv_next -> { mController.controllerListener?.onNextClick() updatePreNextButton() } R.id.iv_min_screen -> { mController.switchControllerView(ControllerViewFactory.TINY_MODE) mController.controllerListener?.onTinyScreenClick() } } } override fun updateProgress(progress: Int, secondaryProgress: Int) { mProgress.progress = progress mProgress.secondaryProgress = secondaryProgress } override fun updateTime(currentTime: String, endTime: String) { mCurrentTime.text = currentTime mEndTime.text = endTime } override fun updateTogglePauseUI(isPlaying: Boolean) { if (isPlaying) { mPauseButton.setImageResource(R.drawable.ic_player_pause) } else { mPauseButton.setImageResource(R.drawable.ic_player_play) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/player/view/TinyControllerView.kt ================================================ package com.jennifer.andy.simpleeyes.player.view import android.content.Context import android.view.View import android.widget.ImageView import android.widget.TextView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.stringForTime /** * Author: andy.xwt * Date: 2020/3/9 23:37 * Description: */ class TinyControllerView(context: Context) : ControllerView(context), View.OnClickListener { private lateinit var mPauseButton: ImageView private lateinit var mPreButton: ImageView private lateinit var mBackButton: ImageView private lateinit var mNextButton: ImageView private lateinit var mFullScreen: ImageView private lateinit var mEndTime: TextView private lateinit var mCurrentTime: TextView override val layoutId = R.layout.layout_media_controller_tiny override fun initView() { mPauseButton = findViewById(R.id.iv_pause) mPreButton = findViewById(R.id.iv_previous) mBackButton = findViewById(R.id.iv_back) mNextButton = findViewById(R.id.iv_next) mFullScreen = findViewById(R.id.iv_full_screen) mEndTime = findViewById(R.id.tv_end_time) mCurrentTime = findViewById(R.id.tv_currentTime) initListener() } private fun initListener() { mPreButton.setOnClickListener(this) mPauseButton.setOnClickListener(this) mNextButton.setOnClickListener(this) mBackButton.setOnClickListener(this) mFullScreen.setOnClickListener(this) } override fun initData() { //初始化开始时间 val position = mPlayer.currentPosition val duration = mPlayer.duration mCurrentTime.text = stringForTime(position) mEndTime.text = "/${stringForTime(duration)}" updatePreNextButton() } //判断是否显示上一个按钮与下一个按钮 private fun updatePreNextButton() { mPreButton.visibility = if (mController.isHavePreVideo()) View.VISIBLE else View.GONE mNextButton.visibility = if (mController.isHaveNextVideo()) View.VISIBLE else View.GONE } override fun onClick(v: View) { when (v.id) { R.id.iv_pause -> { togglePause() mController.show() } R.id.iv_previous -> { mController.controllerListener?.onPreClick() updatePreNextButton() } R.id.iv_next -> { mController.controllerListener?.onNextClick() updatePreNextButton() } R.id.iv_back -> { mController.hide() mController.controllerListener?.onBackClick() } R.id.iv_full_screen -> { mController.switchControllerView(ControllerViewFactory.FULL_SCREEN_MODE) mController.controllerListener?.onFullScreenClick() } } } override fun updateProgress(progress: Int, secondaryProgress: Int) {} override fun updateTime(currentTime: String, endTime: String) { mCurrentTime.text = currentTime mEndTime.text = "/$endTime" } override fun updateTogglePauseUI(isPlaying: Boolean) { if (isPlaying) { mPauseButton.setImageResource(R.drawable.ic_player_pause) } else { mPauseButton.setImageResource(R.drawable.ic_player_play) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/router/EyesPathReplaceService.kt ================================================ package com.jennifer.andy.simpleeyes.router import android.content.Context import android.net.Uri import androidx.core.net.toUri import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.facade.service.PathReplaceService import java.util.regex.Pattern /** * Author: andy.xwt * Date: 2018/7/20 14:08 * Description: * 这里发现开眼的ActionUrl不是正常的scheme格式,因为当我们通过Uri跳转的时候会出现问题。所以这里要对其进行替换操作 */ @Route(path = "/github/AndyJennifer") class EyesPathReplaceService : PathReplaceService { companion object { const val HOST = "eyepetizer://github.com/" const val SCHEME = "eyepetizer" const val ONE_LEVEL = "AndyJennifer/" } override fun forString(path: String): String { return path } override fun forUri(uri: Uri): Uri { if (uri.scheme == SCHEME) { val uriStr = uri.toString() val split = uriStr.split("eyepetizer://") when { uriStr.contains("pgc/detail") -> //处理作者详情 return setPageDetailUrl(split) uriStr.contains("webview") -> //处理webview return setWebViewUrl(split) uriStr.contains("detail") -> //处理webview中点击跳转 return setWebVideoUrl(split) uriStr.contains("ranklist") ->//处理排行榜 return setRankListUrl(split) uriStr.contains("campaign") ->//处理专题 return setTopicUrl(split) uriStr.contains("tag") ->//处理360全景 return setTagUrl(split) uriStr.contains("category") ->//处理分类 return setCategoryUrl(split) uriStr.contains("common") ->//处理公共RecyclerView界面 return setCommonUrl(split) else -> { } } } return uri } /** * 处理作者详情url * eyepetizer://pgc/detail/1065/?title=DELISH%20KITCHEN%20%E5%8F%AF%E5%8F%A3%E5%8E%A8%E6%88%BF&userType=PGC&tabIndex=1 * 替换为: * eyepetizer://github.com/pgc/detail/?title=DELISH%20KITCHEN%20%E5%8F%AF%E5%8F%A3%E5%8E%A8%E6%88%BF&userType=PGC&tabIndex=1?id=1065 */ private fun setPageDetailUrl(split: List): Uri { //获取id val pt = Pattern.compile("/\\d+/") val matcher = pt.matcher(split[1]) var id = 0 if (matcher.find()) { id = matcher.group().replace("/", "").toInt() } //替换/1065/为空字符串 val uriWithoutNumber = split[1].replace(Regex("/\\d+/"), "") return "$HOST$uriWithoutNumber&id=$id".toUri() } /** * 处理webViewUrl * eyepetizer://webview/?title=%E5%B9%BF%E5%9C%BA&url=http%3A%2F%2Fwww.kaiyanapp.com%2Fcampaign%2Ftag_square%2Ftag_square.html%3Fnid%3D1207%26tid%3D845%26cookie%3D%26udid%3Dd0f6190461864a3a978bdbcb3fe9b48709f1f390%26shareable%3Dtrue * 替换为:eyepetizer://github.com/AndyJennifer/webview/?title=%E5%B9%BF%E5%9C%BA&url=http%3A%2F%2Fwww.kaiyanapp.com%2Fcampaign%2Ftag_square%2Ftag_square.html%3Fnid%3D1207%26tid%3D845%26cookie%3D%26udid%3Dd0f6190461864a3a978bdbcb3fe9b48709f1f390%26shareable%3Dtrue */ private fun setWebViewUrl(split: List): Uri { return Uri.parse("$HOST$ONE_LEVEL${split[1]}") } /** * 处理webView点击跳转视频信息 * eyepetizer://detail/114760 * 替换为:eyepetizer://github.com/AndyJennifer/detail?id =114760 */ private fun setWebVideoUrl(split: List): Uri { //获取id val pt = Pattern.compile("/\\d+") val matcher = pt.matcher(split[1]) var id = 0 if (matcher.find()) { id = matcher.group().replace("/", "").toInt() } //替换/1065/为空字符串 val uriWithoutNumber = split[1].replace(Regex("/\\d+"), "") return "$HOST$ONE_LEVEL$uriWithoutNumber?id=$id".toUri() } /** * 处理排行榜 * eyepetizer://ranklist/ * 替换为:eyepetizer://github.com/AndyJennifer/ranklist */ private fun setRankListUrl(split: List): Uri { val str = split[1].replace("/", "") return "$HOST$ONE_LEVEL$str".toUri() } /** * 处理热门专题, * eyepetizer://campaign/list/?title=%E4%B8%93%E9%A2%98 * 替换为:eyepetizer://github.com/campaign/list?title=%E4%B8%93%E9%A2%98 */ private fun setTopicUrl(split: List): Uri { val str = split[1].replace("/?", "?") return "$HOST$str".toUri() } /** * 处理360全景 * eyepetizer://tag/658/?title=360%C2%B0%E5%85%A8%E6%99%AF * 替换为:eyepetizer://github.com/AndyJennifer/tag?id=658&title=360%C2%B0%E5%85%A8%E6%99%AF */ private fun setTagUrl(split: List): Uri { //获取id val pt = Pattern.compile("/\\d+/") val matcher = pt.matcher(split[1]) var id = 0 if (matcher.find()) { id = matcher.group().replace("/", "").toInt() } //替换/658/为空字符串 val uriWithoutNumber = split[1].replace(Regex("/\\d+/"), "") return "$HOST$ONE_LEVEL$uriWithoutNumber&id=$id".toUri() } /** * 处理360全景下面的所有的种类 * eyepetizer://category/14/?title=%E5%B9%BF%E5%91%8A * 替换为:eyepetizer://github.com/AndyJennifer/category?id=14%title=%E5%B9%BF%E5%91%8A */ private fun setCategoryUrl(split: List): Uri { val pt = Pattern.compile("/\\d+/") val matcher = pt.matcher(split[1]) var id = 0 if (matcher.find()) { id = matcher.group().replace("/", "").toInt() } val uriWithoutNumber = split[1].replace(Regex("/\\d+/"), "") return "$HOST$ONE_LEVEL$uriWithoutNumber&id=$id".toUri() } /** * 处理公共界面中包含RecyclerVie的 * eyepetizer://common/?title=VLOG&url=http%3A%2F%2Fbaobab.kaiyanapp.com%2Fapi%2Fv4%2Fplaylists%2F3946%2Fvideos * 替换为:eyepetizer://github.com/AndyJennifer/common?title=VLOG&url=http%3A%2F%2Fbaobab.kaiyanapp.com%2Fapi%2Fv4%2Fplaylists%2F3946%2Fvideos */ private fun setCommonUrl(split: List): Uri { val str = split[1].replace("/?", "?") return "$HOST$ONE_LEVEL$str".toUri() } override fun init(context: Context?) { } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/router/RouteIntercept.kt ================================================ package com.jennifer.andy.simpleeyes.router import android.content.Context import com.alibaba.android.arouter.facade.Postcard import com.alibaba.android.arouter.facade.annotation.Interceptor import com.alibaba.android.arouter.facade.callback.InterceptorCallback import com.alibaba.android.arouter.facade.template.IInterceptor import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.UserPreferences /** * Author: andy.xwt * Date: 2019/1/14 19:04 * Description: 1.登录拦截,判断用户是否登录 */ @Interceptor(priority = 8, name = "登录拦截器") class RouteIntercept : IInterceptor { companion object { const val SHOULD_LOGIN = 1 ushr 30 } override fun process(postcard: Postcard, callback: InterceptorCallback) { when (postcard.extra) { SHOULD_LOGIN -> judgeUserIsLogin(postcard, callback) else -> { callback.onContinue(postcard) } } } /** * 判断用户是否登录,如果没有登录则显示登录界面 */ private fun judgeUserIsLogin(postcard: Postcard, callback: InterceptorCallback) { val userIsLogin = UserPreferences.getUserIsLogin() if (!userIsLogin) //如果用户没登录,直接跳转到登录界面 ARouter.getInstance().build("/github/Login").navigation() else callback.onContinue(postcard) } override fun init(context: Context?) { } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/router/SchemeFilterActivity.kt ================================================ package com.jennifer.andy.simpleeyes.router import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.alibaba.android.arouter.facade.Postcard import com.alibaba.android.arouter.facade.callback.NavCallback import com.alibaba.android.arouter.launcher.ARouter /** * Author: andy.xwt * Date: 2018/7/19 19:57 * Description: 中间跳转页 */ class SchemeFilterActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val uri = intent.data ARouter.getInstance().build(uri).navigation(this, object : NavCallback() { override fun onArrival(postcard: Postcard?) { finish() } }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/rx/RxBus.kt ================================================ package com.jennifer.andy.simpleeyes.rx import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import io.reactivex.functions.Consumer import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.PublishSubject import java.util.concurrent.ConcurrentHashMap /** * Author: andy.xwt * Date: 2018/2/12 14:06 * Description: 参考地址:https://mcxiaoke.gitbooks.io/rxdocs/content/Subject.html */ object RxBus { private val mBus = PublishSubject.create().toSerialized() private val mSubjects = ConcurrentHashMap() /** * 发送一个事件 */ @JvmStatic fun post(o: Any) { mBus.onNext(o) } /** * 根据传递的 eventType 类型返回特定类型的(eventType)的 被观察者 */ private fun toObservable(eventType: Class): Observable { return mBus.ofType(eventType) } /** * 订阅 * * @param eventType 接受的事件类型 * @param action 处理方法 * @param error 错误处理方法 * @param 接受事件的泛型 * @return */ private fun doSubscribe(eventType: Class, action: Consumer, error: Consumer): Disposable { return toObservable(eventType) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(action, error) } /** * 保存订阅后的disposable */ private fun addDisposable(any: Any, disposable: Disposable) { val key = any.javaClass.name if (mSubjects[key] != null) { mSubjects[key]?.add(disposable) } else { val disposables = CompositeDisposable() disposables.add(disposable) mSubjects[key] = disposables } } /** * 处理注册事件 * * @param event 事件 * @param action 处理方式 * @param subscribe 订阅者 * @param 事件类型泛型 */ @JvmStatic fun register(subscribe: Any, event: Class, action: Consumer) { val disposable = doSubscribe(event, action, Consumer { throwable -> throw RuntimeException(throwable.message) }) addDisposable(subscribe, disposable) } /** * 注销监听事件 * * @param subscribe 订阅者 */ @JvmStatic fun unRegister(subscribe: Any) { val key = subscribe.javaClass.name if (mSubjects.containsKey(key) && mSubjects[key] != null) { mSubjects[key]?.dispose() } mSubjects.remove(key) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/rx/RxThreadHelper.kt ================================================ package com.jennifer.andy.simpleeyes.rx import io.reactivex.FlowableTransformer import io.reactivex.MaybeTransformer import io.reactivex.ObservableTransformer import io.reactivex.SingleTransformer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers /** * Author: andy.xwt * Date: 2017/10/24 10:42 * Description:网络请求帮助类处理。默认将观察者的方法切换到主线程中运行 */ object RxThreadHelper { /** * 将Observable类型的观察者切换到主线程中运行 */ fun switchObservableThread(): ObservableTransformer { return ObservableTransformer { it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) } } /** * 将Flowable类型的观察者切换到主线程中运行 */ fun switchFlowableThread(): FlowableTransformer { return FlowableTransformer { it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) } } /** * 将Single类型的观察者切换到主线程中运行 */ fun switchSingleThread(): SingleTransformer { return SingleTransformer { it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) } } /** * 将Maybe类型的观察者切换到主线程中运行 */ fun switchMaybeThread(): MaybeTransformer { return MaybeTransformer { it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/rx/error/FlowableRetryDelay.kt ================================================ package com.jennifer.andy.simpleeyes.rx.error import io.reactivex.Flowable import io.reactivex.functions.Function import org.reactivestreams.Publisher import java.util.concurrent.TimeUnit /** * Author: andy.xwt * Date: 2019-11-17 21:24 * Description:该类逻辑与[ObservableRetryDelay]一样 */ class FlowableRetryDelay( val retryConfigProvider: (Throwable) -> RetryConfig ) : Function, Publisher<*>> { private var retryCount: Int = 0 override fun apply(throwableFlowable: Flowable): Publisher<*> { return throwableFlowable .flatMap { error -> val (maxRetries, delay, retryTransform) = retryConfigProvider(error) if (++retryCount <= maxRetries) { retryTransform() .flatMapPublisher { retry -> if (retry) Flowable.timer(delay.toLong(), TimeUnit.MILLISECONDS) else Flowable.error(error) } } else Flowable.error(error) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/rx/error/GlobalErrorProcessor.kt ================================================ package com.jennifer.andy.simpleeyes.rx.error import com.jennifer.andy.simpleeyes.utils.toast import retrofit2.HttpException /** * Author: andy.xwt * Date: 2019-11-17 22:04 * Description: */ fun globalHandleError(): GlobalErrorTransformer = GlobalErrorTransformer( globalDoOnErrorConsumer = { error -> when (error) { is HttpException -> { when (error.code()) { 401 -> toast { "401 Unauthorized" } 404 -> toast { "404 failure" } 500 -> toast { "500 server failure" } else -> toast { "network failure" } } } else -> { toast { error.message.toString() } } } } ) ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/rx/error/GlobalErrorTransformer.kt ================================================ package com.jennifer.andy.simpleeyes.rx.error import io.reactivex.* /** * Author: andy.xwt * Date: 2019-11-17 21:22 * Description: RxJava全局错误处理,包括: * 1.全局的onNext拦截器[globalOnNextInterceptor] * 2.全局的错误重试处理器[globalOnErrorResume],该属性用于当发生错误时,重新发送新的Observable * 3.全局重试配置参数[RetryConfig],该属性用于配置重试次数以及重试的时间,是否需要重试,需要通过内部的condition进行判断 * 4.全局的错误处理器[globalDoOnErrorConsumer],该属性会在观察者的onError方法之前运行 */ class GlobalErrorTransformer constructor( private val globalOnNextInterceptor: (T) -> Observable = { Observable.just(it) }, private val globalOnErrorResume: (Throwable) -> Observable = { Observable.error(it) }, private val retryConfigProvider: (Throwable) -> RetryConfig = { RetryConfig() }, private val globalDoOnErrorConsumer: (Throwable) -> Unit = { } ) : ObservableTransformer, FlowableTransformer, SingleTransformer, MaybeTransformer, CompletableTransformer { override fun apply(upstream: Observable): Observable = upstream .flatMap { globalOnNextInterceptor(it) } .onErrorResumeNext { throwable: Throwable -> globalOnErrorResume(throwable) } .retryWhen(ObservableRetryDelay(retryConfigProvider)) .doOnError(globalDoOnErrorConsumer) override fun apply(upstream: Completable): Completable = upstream .onErrorResumeNext { globalOnErrorResume(it).ignoreElements() } .retryWhen(FlowableRetryDelay(retryConfigProvider)) .doOnError(globalDoOnErrorConsumer) override fun apply(upstream: Flowable): Flowable = upstream .flatMap { globalOnNextInterceptor(it) .toFlowable(BackpressureStrategy.BUFFER) } .onErrorResumeNext { throwable: Throwable -> globalOnErrorResume(throwable) .toFlowable(BackpressureStrategy.BUFFER) } .retryWhen(FlowableRetryDelay(retryConfigProvider)) .doOnError(globalDoOnErrorConsumer) override fun apply(upstream: Maybe): Maybe = upstream .flatMap { globalOnNextInterceptor(it) .firstElement() } .onErrorResumeNext { throwable: Throwable -> globalOnErrorResume(throwable) .firstElement() } .retryWhen(FlowableRetryDelay(retryConfigProvider)) .doOnError(globalDoOnErrorConsumer) override fun apply(upstream: Single): Single = upstream .flatMap { globalOnNextInterceptor(it) .firstOrError() } .onErrorResumeNext { throwable -> globalOnErrorResume(throwable) .firstOrError() } .retryWhen(FlowableRetryDelay(retryConfigProvider)) .doOnError(globalDoOnErrorConsumer) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/rx/error/ObservabeRetryDelay.kt ================================================ package com.jennifer.andy.simpleeyes.rx.error import io.reactivex.Observable import io.reactivex.ObservableSource import io.reactivex.functions.Function import java.util.concurrent.TimeUnit /** * Author: andy.xwt * Date: 2019-11-17 21:27 * Description:该类会获取配置的重试参数。判断当前重试的次数是否大于配置的重试次数,如果比配置的小,那么会判断重试 * 的条件,如果为true,那么就会重试,反之直接发送Error信息 */ class ObservableRetryDelay( val retryConfigProvider: (Throwable) -> RetryConfig ) : Function, ObservableSource<*>> { private var retryCount: Int = 0 override fun apply(throwableObs: Observable): ObservableSource<*> { return throwableObs .flatMap { error -> val (maxRetries, delay, retryCondition) = retryConfigProvider(error) if (++retryCount <= maxRetries) { retryCondition().flatMapObservable { retry -> if (retry) Observable.timer(delay.toLong(), TimeUnit.MILLISECONDS) else Observable.error(error) } } else Observable.error(error) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/rx/error/RetryConfig.kt ================================================ package com.jennifer.andy.simpleeyes.rx.error import io.reactivex.Single /** * Author: andy.xwt * Date: 2019-11-17 21:12 * Description: 重试配置,默认情况下不会进行重试,如果需要重试配置,需要重写condition */ private const val DEFAULT_RETRY_TIMES = 1 //默认重试次数 1次 private const val DEFAULT_DELAY_DURATION = 1000 //默认推迟时间为1秒 data class RetryConfig(val maxRetries: Int = DEFAULT_RETRY_TIMES, val delay: Int = DEFAULT_DELAY_DURATION, val condition: () -> Single = { Single.just(false) }) ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/MainActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui import android.os.Bundle import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatActivity import com.jennifer.andy.simpleeyes.ui.feed.FeedFragment import com.jennifer.andy.simpleeyes.ui.follow.FollowFragment import com.jennifer.andy.simpleeyes.ui.home.HomeFragment import com.jennifer.andy.simpleeyes.ui.profile.ProfileFragment import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.BottomBar import com.jennifer.andy.simpleeyes.widget.BottomItem import me.yokeyword.fragmentation.SupportFragment /** * Author: andy.xwt * Date: 2017/9/19 18:54 * Description:主界面 */ class MainActivity : BaseAppCompatActivity() { private var mFragments = arrayOfNulls(4) private val mBottomNavigation: BottomBar by bindView(R.id.bottom_navigation_bar) companion object { private const val FIRST = 0 private const val SECOND = 1 private const val THIRD = 2 private const val FOURTH = 3 } override fun initView(savedInstanceState: Bundle?) { mFragments[FIRST] = HomeFragment.newInstance() mFragments[SECOND] = FeedFragment.newInstance() mFragments[THIRD] = FollowFragment.newInstance() mFragments[FOURTH] = ProfileFragment.newInstance() loadMultipleRootFragment(R.id.fl_container, FIRST, *mFragments) initBottomNavigation() } private fun initBottomNavigation() { val home = BottomItem(R.drawable.ic_tab_strip_icon_feed_selected, getString(R.string.home)) home.setUnSelectedDrawable(R.drawable.ic_tab_strip_icon_feed) val discover = BottomItem(R.drawable.ic_tab_strip_icon_category_selected, getString(R.string.discover)) discover.setUnSelectedDrawable(R.drawable.ic_tab_strip_icon_category) val focus = BottomItem(R.drawable.ic_tab_strip_icon_follow_selected, getString(R.string.focus)) focus.setUnSelectedDrawable(R.drawable.ic_tab_strip_icon_follow) val mine = BottomItem(R.drawable.ic_tab_strip_icon_profile_selected, getString(R.string.mine)) mine.setUnSelectedDrawable(R.drawable.ic_tab_strip_icon_profile) with(mBottomNavigation) { addItem(home) addItem(discover) addItem(focus) addItem(mine) initialise() setOnTabSelectedListener(object : BottomBar.TabSelectedListener { override fun onTabSelected(position: Int, prePosition: Int) { showHideFragment(mFragments[position]) } override fun onTabUnselected(position: Int) { } override fun onTabReselected(position: Int) { if (position == FIRST) { val categoryFragment = mFragments[FIRST] as HomeFragment categoryFragment.scrollToTop() } } }) } } override fun getContentViewLayoutId() = R.layout.activity_main } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/author/AuthorTagDetailActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.author import android.animation.ArgbEvaluator import android.graphics.Color import android.os.Bundle import android.widget.ImageView import android.widget.RelativeLayout import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Tab import com.jennifer.andy.simpleeyes.entity.TabInfo import com.jennifer.andy.simpleeyes.net.Extras import com.jennifer.andy.simpleeyes.ui.author.presenter.AuthorTagDetailPresenter import com.jennifer.andy.simpleeyes.ui.author.ui.AuthorTagDetailView import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.BaseFragmentItemAdapter import com.jennifer.andy.simpleeyes.ui.feed.TagDetailInfoFragment import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.showKeyboard import com.jennifer.andy.simpleeyes.widget.StickyNavLayout import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.font.FontType import com.jennifer.andy.simpleeyes.widget.tab.ShortTabLayout /** * Author: andy.xwt * Date: 2019-07-13 22:38 * Description: 作者详细信息界面 */ @Route(path = "/pgc/detail") class AuthorTagDetailActivity : BaseActivity(), AuthorTagDetailView { private val mToolbar: RelativeLayout by bindView(R.id.tool_bar) private val mTvTitle: CustomFontTextView by bindView(R.id.tv_title) private val mStickyNavLayout: StickyNavLayout by bindView(R.id.stick_layout) private val mViewPager: ViewPager by bindView(R.id.id_sticky_nav_layout_viewpager) private val mTabLayout: ShortTabLayout by bindView(R.id.id_sticky_nav_layout_nav_view) private val mTvName: CustomFontTextView by bindView(R.id.tv_name) private val mTvDesc: CustomFontTextView by bindView(R.id.tv_desc) private val mTvBrief: CustomFontTextView by bindView(R.id.tv_brief) private val mIvHead: SimpleDraweeView by bindView(R.id.iv_head) @Autowired @JvmField var tabIndex: String? = null @Autowired @JvmField var title: String? = null @Autowired @JvmField var id: String? = null override fun getBundleExtras(extras: Bundle) { with(extras) { tabIndex = getString(Extras.TAB_INDEX) title = getString(Extras.TITLE) id = getString(Extras.ID) } } override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) initToolBar(title, 0f) mPresenter.getAuthorTagDetail(id!!) } override fun loadInfoSuccess(tab: Tab) { mViewPager.adapter = BaseFragmentItemAdapter(supportFragmentManager, initFragments(tab.tabInfo), initTitles(tab.tabInfo)) mTabLayout.setupWithViewPager(mViewPager) mViewPager.currentItem = tabIndex?.toInt() ?: 0 mStickyNavLayout.setScrollChangeListener(object : StickyNavLayout.ScrollChangeListener { override fun onScroll(moveRatio: Float) { initToolBar(title, moveRatio) } }) //设置topView信息 with(tab.pgcInfo) { mTvName.text = name mTvDesc.text = description mTvBrief.text = brief mIvHead.setImageURI(icon) } } private fun initFragments(tabInfo: TabInfo): MutableList { val fragments = mutableListOf() for (i in tabInfo.tabList.indices) { fragments.add(TagDetailInfoFragment.newInstance(tabInfo.tabList[i].apiUrl)) } return fragments } private fun initTitles(tabInfo: TabInfo): MutableList { val titles = mutableListOf() for (i in tabInfo.tabList.indices) { titles.add(tabInfo.tabList[i].name) } return titles } private fun initToolBar(title: String? = null, titleAlpha: Float = 1f) { findViewById(R.id.iv_back).setOnClickListener { showKeyboard(false) finish() } //设置背景渐变 val color = ArgbEvaluator().evaluate(titleAlpha, 0x00FFFFFF, Color.WHITE) as Int mToolbar.setBackgroundColor(color) mTvTitle.apply { setFontType(fontType = FontType.BOLD) text = title alpha = titleAlpha } } override fun getContentViewLayoutId() = R.layout.activity_author_tag_detail } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/author/model/AuthorModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.author.model import com.jennifer.andy.simpleeyes.entity.Tab import com.jennifer.andy.simpleeyes.net.Api import com.jennifer.andy.simpleeyes.rx.RxThreadHelper import com.jennifer.andy.simpleeyes.rx.error.globalHandleError import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel import io.reactivex.Observable /** * Author: andy.xwt * Date: 2019-07-13 22:34 * Description: */ class AuthorModel : BaseModel { /** * 获取作者 */ fun getAuthorTagDetail(id: String): Observable = Api.getDefault() .getAuthorTagDetail(id) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/author/presenter/AuthorTagDetailPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.author.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.author.model.AuthorModel import com.jennifer.andy.simpleeyes.ui.author.ui.AuthorTagDetailView import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2019-07-13 22:40 * Description: */ class AuthorTagDetailPresenter : BasePresenter() { private var mAuthorModel: AuthorModel = AuthorModel() fun getAuthorTagDetail(id: String) { mAuthorModel.getAuthorTagDetail(id).autoDispose(mScopeProvider).subscribe({ mView?.showContent() mView?.loadInfoSuccess(it) }, { mView?.showNetError(View.OnClickListener { getAuthorTagDetail(id) }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/author/ui/AuthorTagDetailView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.author.ui import com.jennifer.andy.simpleeyes.entity.Tab import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2019-07-13 22:39 * Description: */ interface AuthorTagDetailView : BaseView { fun loadInfoSuccess(tab: Tab) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/BaseActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base import android.os.Bundle import android.view.View import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.utils.getGenericInstance /** * Author: andy.xwt * Date: 2017/9/5 19:04 * Description: 基础类activity */ @Suppress("UNCHECKED_CAST") abstract class BaseActivity> : BaseAppCompatActivity(), BaseView { protected lateinit var mPresenter: T override fun onCreate(savedInstanceState: Bundle?) { mPresenter = getGenericInstance(this, 1) lifecycle.addObserver(mPresenter) mPresenter.attachView(this as V, this) super.onCreate(savedInstanceState) } override fun showLoading() { mMultipleStateView.showLoading() } override fun showNetError(onClickListener: View.OnClickListener) { mMultipleStateView.showNetError(onClickListener) } override fun showEmpty(onClickListener: View.OnClickListener) { mMultipleStateView.showEmpty(onClickListener) } override fun showContent() { mMultipleStateView.showContent() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/BaseAppCompatActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base import android.content.Context import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.StringRes import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.manager.ActivityManager import com.jennifer.andy.simpleeyes.utils.showKeyboard import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.font.FontType import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView import me.yokeyword.fragmentation.SupportActivity /** * Author: andy.xwt * Date: 2017/8/10 22:37 * Description: */ abstract class BaseAppCompatActivity : SupportActivity() { /** * 上下文对象 */ protected lateinit var mContext: Context protected lateinit var mMultipleStateView: MultipleStateView /** * 跳转到其他Activity启动或者退出的模式 */ enum class TransitionMode { LEFT, RIGHT, TOP, BOTTOM, SCALE, FADE } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initData() initView(savedInstanceState) } override fun onStart() { super.onStart() ActivityManager.getInstance().addActivity(this) } private fun initData() { overrideTransitionAnimation() val extras = intent.extras if (extras != null) { getBundleExtras(extras) } mContext = this //添加相应的布局 if (getContentViewLayoutId() != 0) { mMultipleStateView = MultipleStateView(this) val view = View.inflate(this, getContentViewLayoutId(), mMultipleStateView) setContentView(view) } else { throw IllegalArgumentException("You must return layout id") } } /** * 设置activity进入动画 */ private fun overrideTransitionAnimation() { if (toggleOverridePendingTransition()) { when (getOverridePendingTransition()) { TransitionMode.TOP -> overridePendingTransition(R.anim.top_in, R.anim.no_anim) TransitionMode.BOTTOM -> overridePendingTransition(R.anim.bottom_in, R.anim.no_anim) TransitionMode.LEFT -> overridePendingTransition(R.anim.left_in, R.anim.no_anim) TransitionMode.RIGHT -> overridePendingTransition(R.anim.right_in, R.anim.no_anim) TransitionMode.FADE -> overridePendingTransition(R.anim.fade_in, R.anim.no_anim) TransitionMode.SCALE -> overridePendingTransition(R.anim.scale_in, R.anim.no_anim) } } } override fun finish() { super.finish() if (toggleOverridePendingTransition()) { when (getOverridePendingTransition()) { TransitionMode.TOP -> overridePendingTransition(0, R.anim.top_out) TransitionMode.BOTTOM -> overridePendingTransition(0, R.anim.bottom_out) TransitionMode.LEFT -> overridePendingTransition(0, R.anim.left_out) TransitionMode.RIGHT -> overridePendingTransition(0, R.anim.right_out) TransitionMode.FADE -> overridePendingTransition(0, R.anim.fade_out) TransitionMode.SCALE -> overridePendingTransition(0, R.anim.scale_out) } } } override fun onStop() { super.onStop() ActivityManager.getInstance().removeActivity(this) } /** * 初始化工具栏,默认情况下加粗 */ protected fun initToolBar(toolbar: ViewGroup, title: String? = null, fontType: FontType = FontType.BOLD) { val ivBack = toolbar.findViewById(R.id.iv_back) ivBack.setOnClickListener { showKeyboard(false) finish() } val tvTitle = toolbar.findViewById(R.id.tv_title) tvTitle.setFontType(fontType) tvTitle.text = title } /** * 初始化工具栏,默认情况下加粗 */ protected fun initToolBar(toolbar: ViewGroup, @StringRes id: Int? = null, fontType: FontType = FontType.BOLD) { val ivBack = toolbar.findViewById(R.id.iv_back) ivBack.setOnClickListener { showKeyboard(false) finish() } val tvTitle = toolbar.findViewById(R.id.tv_title) tvTitle.setFontType(fontType) tvTitle.setText(id!!) } abstract fun initView(savedInstanceState: Bundle?) /** * 获取bundle 中的数据 */ open fun getBundleExtras(extras: Bundle) {} /** * 是否有切换动画 */ protected open fun toggleOverridePendingTransition() = false /** * 获得切换动画的模式 */ protected open fun getOverridePendingTransition(): TransitionMode? = null /** * 获取当前布局id */ abstract fun getContentViewLayoutId(): Int } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/BaseAppCompatFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView import me.yokeyword.fragmentation.SupportFragment /** * Author: andy.xwt * Date: 2017/9/5 19:06 * Description: */ abstract class BaseAppCompatFragment : SupportFragment() { protected lateinit var LOG_TAG: String protected lateinit var mMultipleStateView: MultipleStateView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LOG_TAG = this.javaClass.simpleName if (arguments != null) { getBundleExtras(arguments!!) } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return if (getContentViewLayoutId() != 0) { mMultipleStateView = MultipleStateView(context!!) return View.inflate(context, getContentViewLayoutId(), mMultipleStateView) } else { super.onCreateView(inflater, container, savedInstanceState) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initView(savedInstanceState) } /** * 获取bundle中相应data */ open fun getBundleExtras(extras: Bundle) {} /** * 获取资源id */ abstract fun getContentViewLayoutId(): Int /** * 初始化view */ open fun initView(savedInstanceState: Bundle?) { } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/BaseFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.utils.getGenericInstance /** * Author: andy.xwt * Date: 2017/9/5 19:06 * Description: */ @Suppress("UNCHECKED_CAST") abstract class BaseFragment> : BaseAppCompatFragment(), BaseView { protected lateinit var mPresenter: T override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mPresenter = getGenericInstance(this, 1) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { mPresenter.attachView(this as V, this) return super.onCreateView(inflater, container, savedInstanceState) } override fun showLoading() { mMultipleStateView.showLoading() } override fun showNetError(onClickListener: View.OnClickListener) { mMultipleStateView.showNetError(onClickListener) } override fun showEmpty(onClickListener: View.OnClickListener) { mMultipleStateView.showEmpty(onClickListener) } override fun showContent() { mMultipleStateView.showContent() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/BaseFragmentItemAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter /** * Author: andy.xwt * Date: 2018/7/4 13:59 * Description:基础FragmentPagerAdapter适配器 */ open class BaseFragmentItemAdapter(fragmentManager: FragmentManager, private val fragments: MutableList, private val titles: MutableList) : FragmentPagerAdapter(fragmentManager) { override fun getItem(position: Int) = fragments[position] override fun getCount() = fragments.size override fun getPageTitle(position: Int) = titles[position] } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/BaseView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base import android.view.View /** * Author: andy.xwt * Date: 2017/9/5 19:09 * Description:基础view */ interface BaseView { /** * 显示正在加载 */ fun showLoading() /** * 显示内容 */ fun showContent() /** * 显示网络异常 */ fun showNetError(onClickListener: View.OnClickListener) /** * 显示空界面 */ fun showEmpty(onClickListener: View.OnClickListener) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/LoadMoreView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base /** * Author: andy.xwt * Date: 2018/6/30 01:50 * Description: */ interface LoadMoreView : BaseView { /** * 加载更多信息成功 */ fun loadMoreSuccess(data: T) /** * 没有更多 */ fun showNoMore() } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/adapter/BaseDataAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base.adapter import android.content.Intent import android.net.Uri import android.text.TextUtils import android.view.Gravity import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.alibaba.android.arouter.launcher.ARouter import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseQuickAdapter.OnItemClickListener import com.chad.library.adapter.base.BaseViewHolder import com.chad.library.adapter.base.util.MultiTypeDelegate import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.ui.home.adapter.CollectionCardCoverAdapter import com.jennifer.andy.simpleeyes.ui.home.adapter.SquareCollectionAdapter import com.jennifer.andy.simpleeyes.ui.search.adapter.CollectionBriefAdapter import com.jennifer.andy.simpleeyes.ui.video.VideoDetailActivity import com.jennifer.andy.simpleeyes.utils.dip2px import com.jennifer.andy.simpleeyes.utils.getElapseTimeForShow import com.jennifer.andy.simpleeyes.widget.EliteImageView import com.jennifer.andy.simpleeyes.widget.ItemHeaderView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.font.FontType import com.jennifer.andy.simpleeyes.widget.image.imageloader.FrescoImageLoader import com.jennifer.andy.simpleeyes.widget.viewpager.MarginWithIndicatorViewPager import com.youth.banner.Banner import com.youth.banner.BannerConfig import java.util.* /** * Author: andy.xwt * Date: 2017/10/27 16:35 * Description: 基础信息适配器 */ open class BaseDataAdapter(data: MutableList) : BaseQuickAdapter(data) { /** * 卡片类型 */ companion object { const val BANNER_TYPE = 0 const val FOLLOW_CARD_TYPE = 1 const val HORIZONTAL_SCROLL_CARD_TYPE = 2 const val VIDEO_COLLECTION_WITH_COVER_TYPE = 3 const val SQUARE_CARD_COLLECTION_TYPE = 4 const val VIDEO_COLLECTION_OF_HORIZONTAL_SCROLL_CARD_TYPE = 5 const val VIDEO_COLLECTION_WITH_BRIEF_TYPE = 6 const val TEXT_CARD_TYPE = 7 const val BRIEF_CARD_TYPE = 8 const val BLANK_CARD_TYPE = 9 const val SQUARE_CARD_TYPE = 10 const val RECTANGLE_CARD_TYPE = 11 const val VIDEO_TYPE = 12 const val VIDEO_BANNER_THREE_TYPE = 13 const val VIDEO_SMALL_CARD_TYPE = 14 const val BANNER = "banner" const val FOLLOW_CARD = "followCard" const val HORIZONTAL_CARD = "horizontalScrollCard" const val VIDEO_COLLECTION_WITH_COVER = "videoCollectionWithCover" const val SQUARE_CARD_COLLECTION = "squareCardCollection" const val VIDEO_COLLECTION_OF_HORIZONTAL_SCROLL_CARD = "videoCollectionOfHorizontalScrollCard" const val VIDEO_COLLECTION_WITH_BRIEF = "videoCollectionWithBrief" const val TEXT_CARD = "textCard" const val BRIEF_CARD = "briefCard" const val BLANK_CARD = "blankCard" const val SQUARE_CARD = "squareCard" const val RECTANGLE_CARD = "rectangleCard" const val VIDEO = "video" const val VIDEO_BANNER_THREE = "banner3" const val VIDEO_SMALL_CARD = "videoSmallCard" } init { multiTypeDelegate = object : MultiTypeDelegate() { override fun getItemType(andyInfoItem: Content?): Int { when (andyInfoItem?.type) { BANNER -> return BANNER_TYPE FOLLOW_CARD -> return FOLLOW_CARD_TYPE HORIZONTAL_CARD -> return HORIZONTAL_SCROLL_CARD_TYPE VIDEO_COLLECTION_WITH_COVER -> return VIDEO_COLLECTION_WITH_COVER_TYPE SQUARE_CARD_COLLECTION -> return SQUARE_CARD_COLLECTION_TYPE VIDEO_COLLECTION_OF_HORIZONTAL_SCROLL_CARD -> return VIDEO_COLLECTION_OF_HORIZONTAL_SCROLL_CARD_TYPE VIDEO_COLLECTION_WITH_BRIEF -> return VIDEO_COLLECTION_WITH_BRIEF_TYPE TEXT_CARD -> return TEXT_CARD_TYPE BRIEF_CARD -> return BRIEF_CARD_TYPE BLANK_CARD -> return BLANK_CARD_TYPE SQUARE_CARD -> return SQUARE_CARD_TYPE RECTANGLE_CARD -> return RECTANGLE_CARD_TYPE VIDEO -> return VIDEO_TYPE VIDEO_BANNER_THREE -> return VIDEO_BANNER_THREE_TYPE VIDEO_SMALL_CARD -> return VIDEO_SMALL_CARD_TYPE } return FOLLOW_CARD_TYPE } } with(multiTypeDelegate) { registerItemType(BANNER_TYPE, R.layout.layout_card_banner) registerItemType(FOLLOW_CARD_TYPE, R.layout.layout_follow_card) registerItemType(HORIZONTAL_SCROLL_CARD_TYPE, R.layout.layout_horizontal_scroll_card) registerItemType(VIDEO_COLLECTION_WITH_COVER_TYPE, R.layout.layout_collection_with_cover) registerItemType(SQUARE_CARD_COLLECTION_TYPE, R.layout.layout_square_collection) registerItemType(VIDEO_COLLECTION_OF_HORIZONTAL_SCROLL_CARD_TYPE, R.layout.item_collection_of_horizontal_scroll_card) registerItemType(VIDEO_COLLECTION_WITH_BRIEF_TYPE, R.layout.layout_collection_with_brief) registerItemType(TEXT_CARD_TYPE, R.layout.layout_single_text) registerItemType(BRIEF_CARD_TYPE, R.layout.layout_brife_card) registerItemType(BLANK_CARD_TYPE, R.layout.layout_blank_card) registerItemType(SQUARE_CARD_TYPE, R.layout.item_square_card) registerItemType(RECTANGLE_CARD_TYPE, R.layout.item_square_card) registerItemType(VIDEO_TYPE, R.layout.layout_single_video) registerItemType(VIDEO_BANNER_THREE_TYPE, R.layout.layout_follow_card) registerItemType(VIDEO_SMALL_CARD_TYPE, R.layout.layout_video_small_card) } //设置倒数第5个开启预加载 setPreLoadNumber(5) } override fun convert(helper: BaseViewHolder?, item: Content) { when (helper?.itemViewType) { BANNER_TYPE -> setBannerInfo(helper, item) FOLLOW_CARD_TYPE -> setFollowCardInfo(helper, item) HORIZONTAL_SCROLL_CARD_TYPE -> setHorizontalScrollCardInfo(helper, item.data.itemList) VIDEO_COLLECTION_WITH_COVER_TYPE -> setCollectionCardWithCoverInfo(helper, item.data) SQUARE_CARD_COLLECTION_TYPE -> setSquareCollectionInfo(helper, item.data) VIDEO_COLLECTION_OF_HORIZONTAL_SCROLL_CARD_TYPE -> setCollectionOfHorizontalScrollCardInfo(helper, item) VIDEO_COLLECTION_WITH_BRIEF_TYPE -> setCollectionBriefInfo(helper, item) TEXT_CARD_TYPE -> setSingleText(helper, item) BRIEF_CARD_TYPE -> setBriefCardInfo(helper, item) BLANK_CARD_TYPE -> setBlankCardInfo(helper, item) SQUARE_CARD_TYPE -> setSquareCardInfo(helper, item.data) RECTANGLE_CARD_TYPE -> setRectangleCardInfo(helper, item.data) VIDEO_TYPE -> setSingleVideoInfo(helper, item) VIDEO_BANNER_THREE_TYPE -> setBanner3Info(helper, item.data) VIDEO_SMALL_CARD_TYPE -> setSmallCardInfo(helper, item.data) } } /** * 设置视频banner信息 */ private fun setBannerInfo(helper: BaseViewHolder, item: Content) { with(helper) { getView(R.id.iv_image).setImageURI(item.data.image) itemView.setOnClickListener { if (!TextUtils.isEmpty(item.data.actionUrl)) { mContext.startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(item.data.actionUrl) addCategory(Intent.CATEGORY_DEFAULT) addCategory(Intent.CATEGORY_BROWSABLE) }) } } } } /** * 设置单视频卡片信息 */ private fun setFollowCardInfo(helper: BaseViewHolder, item: Content) { with(helper) { val info = item.data //设置头布局 info.header?.let { getView(R.id.item_header_view).setHeader(it, info.type) } getView(R.id.elite_view).apply { setImageUrl(info.content!!.data.cover.feed) setDailyVisible(info.content!!.data.library == "DAILY") setOnClickListener { //跳转到视频详细界面 VideoDetailActivity.start(mContext!!, item.data.content?.data!!, mData as ArrayList, mData.indexOf(item)) } } } } /** * 设置水平滚动卡片集合信息 */ private fun setCollectionOfHorizontalScrollCardInfo(helper: BaseViewHolder, content: Content) { with(helper) { //设置头布局 val data = content.data data.header?.let { getView(R.id.item_header_view).setHeader(it, content.type) } //跳转到视频详细界面 getView(R.id.view_pager).apply { pageViewClickListener = { val content = data.itemList[it].data VideoDetailActivity.start(mContext!!, content, data.itemList as ArrayList, it) } setData(data.itemList) } } } /** * 设置带关注人的视频集合信息 */ private fun setCollectionBriefInfo(helper: BaseViewHolder, content: Content) { with(helper) { getView(R.id.rv_recycler).apply { isNestedScrollingEnabled = false layoutManager = LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false) adapter = CollectionBriefAdapter(content.data.itemList).apply { onItemClickListener = OnItemClickListener { _, _, position -> //跳转到播放视频详情 val item = getItem(position) VideoDetailActivity.start(mContext, item!!.data, data as ArrayList, position) } } } //设置头布局 content.data.header?.let { getView(R.id.item_header_view).setHeader(it, content.data.type) } } } /** * 设置文字信息 */ private fun setSingleText(helper: BaseViewHolder, item: Content) { helper.getView(R.id.tv_text).apply { text = item.data.text when (item.data.type) { "header1" -> { setFontType(item.data.header?.font, FontType.LOBSTER) gravity = Gravity.CENTER } "header2" -> { setFontType(item.data.header?.font, FontType.BOLD) gravity = Gravity.CENTER } else -> { gravity = Gravity.START setFontType(item.data.header?.font, FontType.NORMAL) } } } } /** * 设置合作作者信息 */ private fun setBriefCardInfo(helper: BaseViewHolder, content: Content) { helper.getView(R.id.item_header_view).setAuthorHeader(content.data) } /** * 设置空白信息高度 */ private fun setBlankCardInfo(helper: BaseViewHolder, content: Content) { helper.getView(R.id.view).apply { val lp = layoutParams lp.height = mContext.dip2px(content.data.height.toFloat()) layoutParams = lp } } /** * 设置banner3(广告信息)信息 */ private fun setBanner3Info(helper: BaseViewHolder, item: ContentBean) { with(helper) { //设置头布局 item.header?.let { getView(R.id.item_header_view).setHeader(it, item.type) } getView(R.id.elite_view).apply { setImageUrl(item.image) setDailyVisible(false) setTranslateText(mContext.getString(R.string.advert)) } } } /** * 设置水平滚动卡片信息 */ private fun setHorizontalScrollCardInfo(helper: BaseViewHolder, itemList: MutableList) { helper.getView(R.id.banner).apply { setImageLoader(FrescoImageLoader()) setImages(getHorizonTalCardUrl(itemList)) setBannerStyle(BannerConfig.CIRCLE_INDICATOR) setIndicatorGravity(BannerConfig.CENTER) isAutoPlay(true) start() setDelayTime(5000) setOnBannerListener { //跳转到过滤界面 mContext.startActivity(Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(itemList[it].data.actionUrl) addCategory(Intent.CATEGORY_DEFAULT) addCategory(Intent.CATEGORY_BROWSABLE) }) } } } /** * 获取水平卡片图片地址 */ private fun getHorizonTalCardUrl(itemList: MutableList) = itemList.map { it.data.image } /** * 设置集合卡片信息 */ private fun setCollectionCardWithCoverInfo(helper: BaseViewHolder, item: ContentBean) { with(helper) { getView(R.id.iv_image).apply { item.header?.cover?.let { setImageUrl(it) } setArrowVisible(true) } getView(R.id.rv_collection_cover_recycler).apply { isNestedScrollingEnabled = false layoutManager = LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false) adapter = CollectionCardCoverAdapter(item.itemList) } } } /** * 设置矩形卡片信息 */ private fun setSquareCollectionInfo(helper: BaseViewHolder, item: ContentBean) { val itemList: MutableList = item.itemList with(helper) { getView(R.id.rv_square_recycler).apply { isNestedScrollingEnabled = false layoutManager = LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false) adapter = SquareCollectionAdapter(itemList).apply { onItemClickListener = OnItemClickListener { _, _, position -> ARouter.getInstance() .build(Uri.parse(itemList[position].data.actionUrl)) .navigation() } } } item.header?.let { getView(R.id.item_header_view).setHeader(it, item.type) } } } /** * 设置长方形卡片信息 */ private fun setSquareCardInfo(helper: BaseViewHolder, item: ContentBean) { helper.apply { getView(R.id.iv_simple_image).setImageURI(item.image) setText(R.id.tv_title, item.title) } } /** * 设置正方形卡片信息 */ private fun setRectangleCardInfo(helper: BaseViewHolder, item: ContentBean) { helper.apply { getView(R.id.iv_simple_image).setImageURI(item.image) setText(R.id.tv_title, item.title) } } /** * 设置单视频信息 */ private fun setSingleVideoInfo(helper: BaseViewHolder, item: Content) { with(helper) { getView(R.id.iv_image).setImageURI(item.data.cover.feed) setText(R.id.tv_single_title, item.data.title) val description = "#${item.data.category} / ${getElapseTimeForShow(item.data.duration)}" setText(R.id.tv_single_desc, description) //设置label if (item.data.label != null) { setGone(R.id.tv_label, true) setText(R.id.tv_label, item.data.label?.text) } else { setGone(R.id.tv_label, false) } //点击跳转到视频界面 itemView.setOnClickListener { VideoDetailActivity.start(mContext, item.data, data as ArrayList) } } } /** * 设置小视频卡片信息 */ private fun setSmallCardInfo(helper: BaseViewHolder, data: ContentBean) { with(helper) { getView(R.id.iv_image).setImageURI(data.cover.feed) setText(R.id.tv_title, data.title) val description = "#${data.category} / ${getElapseTimeForShow(data.duration)}" setText(R.id.tv_desc, description) itemView.setOnClickListener { VideoDetailActivity.start(mContext, data, arrayListOf()) } } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/model/BaseModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base.model import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.JenniferInfo import com.jennifer.andy.simpleeyes.net.Api import com.jennifer.andy.simpleeyes.rx.RxThreadHelper import com.jennifer.andy.simpleeyes.rx.error.globalHandleError import io.reactivex.Observable /** * Author: andy.xwt * Date: 2017/9/5 19:08 * Description: */ interface BaseModel { /** * 加载更多信息,针对于AndyInfo数据类型 */ fun loadMoreAndyInfo(nextPageUrl: String?): Observable = Api.getDefault() .getMoreAndyInfo(nextPageUrl) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 加载更多信息,针对于JenniferInfo数据类型 */ fun loadMoreJenniferInfo(nextPageUrl: String?): Observable = Api.getDefault() .getMoreJenniferInfo(nextPageUrl) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 根据url,获取数据 */ fun getDataInfoFromUrl(url: String?): Observable = Api.getDefault() .getDataInfoFromUrl(url) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/presenter/BasePresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base.presenter import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider /** * Author: andy.xwt * Date: 2017/9/5 19:10 * Description: */ open class BasePresenter : LifecycleObserver { protected var mView: V? = null protected lateinit var mScopeProvider: AndroidLifecycleScopeProvider /** * 与view进行关联 */ fun attachView(view: V, lifecycleOwner: LifecycleOwner) { this.mView = view this.mScopeProvider = AndroidLifecycleScopeProvider.from(lifecycleOwner) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/base/presenter/LoadMorePresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.base.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2019-07-10 22:24 * Description: 加载更多Presenter。 * 第一个泛型参数T:为加载更多对应的数据类型 * 第二个泛型参数M:为对应model * 第三个泛型参数V:为实现了加载更多的View */ open class LoadMorePresenter> : BasePresenter() { protected var mNextPageUrl: String? = null protected open lateinit var mBaseModel: M fun loadMoreInfo() { if (mNextPageUrl != null) { mBaseModel.loadMoreAndyInfo(mNextPageUrl).autoDispose(mScopeProvider).subscribe({ mView?.showContent() if (mNextPageUrl == null) { mView?.showNoMore() } else { mNextPageUrl = it.nextPageUrl mView?.loadMoreSuccess(it as T) } }, { mView?.showNetError(View.OnClickListener { loadMoreInfo() }) }) } else { mView?.showNoMore() } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/common/CommonModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.common import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel /** * Author: andy.xwt * Date: 2019-08-26 20:37 * Description: */ class CommonModel:BaseModel ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/common/CommonPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.common import android.view.View import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.presenter.LoadMorePresenter import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2019-08-26 19:51 * Description: */ class CommonPresenter : LoadMorePresenter() { override var mBaseModel = CommonModel() /** * 获取专题信息 */ fun loadDataInfoFromUrl(url: String?) { mBaseModel.getDataInfoFromUrl(url).autoDispose(mScopeProvider).subscribe({ mView?.showLoadSuccess(it.itemList) mNextPageUrl = it.nextPageUrl }, { mView?.showNetError(View.OnClickListener { loadDataInfoFromUrl(url) }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/common/CommonRecyclerActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.common import android.os.Bundle import android.widget.RelativeLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.CustomLoadMoreView /** * Author: andy.xwt * Date: 2019-08-26 19:47 * Description:公共RecyclerView界面,根据url加载数据 */ @Route(path = "/AndyJennifer/common") class CommonRecyclerActivity : BaseActivity(), CommonView { @Autowired @JvmField var title: String? = null @Autowired @JvmField var url: String? = null private val mToolbar: RelativeLayout by bindView(R.id.tool_bar) private val mRecycler by bindView(R.id.rv_recycler) private var mCommonAdapter: BaseDataAdapter? = null override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) initToolBar(mToolbar, title) mPresenter.loadDataInfoFromUrl(url) } override fun showLoadSuccess(itemList: MutableList) { with(mRecycler) { mCommonAdapter = BaseDataAdapter(itemList).apply { setOnLoadMoreListener({ mPresenter.loadMoreInfo() }, mRecycler) setLoadMoreView(CustomLoadMoreView()) } layoutManager = LinearLayoutManager(mContext) adapter = mCommonAdapter } } override fun loadMoreSuccess(data: AndyInfo) { mCommonAdapter?.addData(data.itemList) mCommonAdapter?.loadMoreComplete() } override fun showNoMore() { mCommonAdapter?.loadMoreEnd() } override fun getContentViewLayoutId() = R.layout.activity_common_recyclerview } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/common/CommonView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.common import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView /** * Author: andy.xwt * Date: 2019-08-26 19:48 * Description: */ interface CommonView: LoadMoreView{ fun showLoadSuccess(itemList: MutableList) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/AllCategoryActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.net.Uri import android.os.Bundle import android.view.View import android.widget.RelativeLayout import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.alibaba.android.arouter.launcher.ARouter import com.chad.library.adapter.base.BaseQuickAdapter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter.Companion.RECTANGLE_CARD_TYPE import com.jennifer.andy.simpleeyes.ui.feed.presenter.AllCategoryPresenter import com.jennifer.andy.simpleeyes.ui.feed.view.AllCategoryView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.GridItemDecoration import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView /** * Author: andy.xwt * Date: 2018/7/16 14:17 * Description: 全部分类 */ class AllCategoryActivity : BaseActivity(), AllCategoryView { private val mToolBar: RelativeLayout by bindView(R.id.tool_bar) private val mRecyclerView: RecyclerView by bindView(R.id.rv_recycler) private val mStateView: MultipleStateView by bindView(R.id.multiple_state_view) override fun initView(savedInstanceState: Bundle?) { initToolBar(mToolBar, R.string.all_category) mPresenter.loadAllCategoriesInfo() } override fun loadAllCategoriesSuccess(andyInfo: AndyInfo) { val adapter = BaseDataAdapter(andyInfo.itemList) adapter.setSpanSizeLookup { _, position -> if (adapter.getItemViewType(position) == RECTANGLE_CARD_TYPE) 2 else 1 } //根据actionUrl跳转到相应界面 adapter.onItemClickListener = BaseQuickAdapter.OnItemClickListener { _, _, position -> ARouter.getInstance() .build(Uri.parse(adapter.data[position].data.actionUrl)) .navigation() } mRecyclerView.layoutManager = GridLayoutManager(mContext, 2) mRecyclerView.addItemDecoration(GridItemDecoration(2, 4, true)) mRecyclerView.adapter = adapter } override fun showNetError(onClickListener: View.OnClickListener) { mStateView.showNetError(onClickListener) } override fun showContent() { mStateView.showContent() } override fun getContentViewLayoutId() = R.layout.activity_all_category } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/CategoryTabActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.animation.ArgbEvaluator import android.graphics.Color import android.os.Bundle import android.widget.ImageView import androidx.annotation.DrawableRes import androidx.core.graphics.drawable.DrawableCompat import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Category import com.jennifer.andy.simpleeyes.entity.TabInfo import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.BaseFragmentItemAdapter import com.jennifer.andy.simpleeyes.ui.feed.presenter.CategoryTabPresenter import com.jennifer.andy.simpleeyes.ui.feed.view.CategoryTabView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.showKeyboard import com.jennifer.andy.simpleeyes.widget.StickyNavLayout import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.font.FontType import com.jennifer.andy.simpleeyes.widget.tab.ShortTabLayout /** * Author: andy.xwt * Date: 2018/8/6 10:46 * Description:种类界面,包括广告、剧情等同类型的分类。 */ @Route(path = "/AndyJennifer/category") class CategoryTabActivity : BaseActivity(), CategoryTabView { private val mStickyNavLayout: StickyNavLayout by bindView(R.id.stick_layout) private val mViewPager: ViewPager by bindView(R.id.id_sticky_nav_layout_viewpager) private val mTabLayout: ShortTabLayout by bindView(R.id.id_sticky_nav_layout_nav_view) private val mImageView: SimpleDraweeView by bindView(R.id.iv_image) private val mTvSubTitle: CustomFontTextView by bindView(R.id.tv_sub_title) private val mTvDesc: CustomFontTextView by bindView(R.id.tv_desc) private val mTvFollow: CustomFontTextView by bindView(R.id.tv_follow) @Autowired @JvmField var title: String? = null @Autowired @JvmField var id: String? = null @Autowired @JvmField var tabIndex: String? = null override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) mPresenter.getTabInfo(id!!) initToolBar(R.drawable.ic_action_back_white, title, 0f) } override fun showLoadTabSuccess(category: Category) { mImageView.setImageURI(category.categoryInfo.headerImage) mTvSubTitle.text = category.categoryInfo.name mTvDesc.text = category.categoryInfo.description mViewPager.adapter = BaseFragmentItemAdapter(supportFragmentManager, initFragments(category.tabInfo), initTitles(category.tabInfo)) mTabLayout.setupWithViewPager(mViewPager) mViewPager.currentItem = tabIndex?.toInt() ?: 0 mStickyNavLayout.setScrollChangeListener(object : StickyNavLayout.ScrollChangeListener { override fun onScroll(moveRatio: Float) { if (moveRatio < 1) initToolBar(R.drawable.ic_action_back_white, title, moveRatio) else initToolBar(R.drawable.ic_action_back_black, title, moveRatio) } }) } private fun initFragments(tabInfo: TabInfo): MutableList { val fragments = mutableListOf() for (i in tabInfo.tabList.indices) { fragments.add(TagDetailInfoFragment.newInstance(tabInfo.tabList[i].apiUrl)) } return fragments } private fun initTitles(tabInfo: TabInfo): MutableList { val titles = mutableListOf() for (i in tabInfo.tabList.indices) { titles.add(tabInfo.tabList[i].name) } return titles } private fun initToolBar(@DrawableRes backResId: Int, title: String? = null, titleAlpha: Float = 1f) { val ivBack = findViewById(R.id.iv_back) ivBack.setOnClickListener { showKeyboard(false) finish() } //设置渐变 val color = ArgbEvaluator().evaluate(titleAlpha, Color.WHITE, Color.BLACK) as Int val wrapDrawable = DrawableCompat.wrap(getDrawable(backResId)) wrapDrawable.setTint(color) ivBack.setImageDrawable(wrapDrawable) val tvTitle = findViewById(R.id.tv_title) tvTitle.setFontType(fontType = FontType.BOLD) tvTitle.text = title tvTitle.alpha = titleAlpha } override fun getContentViewLayoutId() = R.layout.activity_category_tab } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/FeedFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.os.Bundle import android.view.View import android.widget.ImageView import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.TabInfo import com.jennifer.andy.simpleeyes.ui.base.BaseFragment import com.jennifer.andy.simpleeyes.ui.base.BaseFragmentItemAdapter import com.jennifer.andy.simpleeyes.ui.feed.presenter.FeedPresenter import com.jennifer.andy.simpleeyes.ui.feed.view.FeedView import com.jennifer.andy.simpleeyes.ui.search.SearchHotActivity import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.readyGo import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView import com.jennifer.andy.simpleeyes.widget.tab.ShortTabLayout /** * Author: andy.xwt * Date: 2017/9/22 13:51 * Description:发现 */ class FeedFragment : BaseFragment(), FeedView { private val mViewPager: ViewPager by bindView(R.id.view_pager) private val mTvAllCategory: CustomFontTextView by bindView(R.id.tv_all_category) private val mIvSearch: ImageView by bindView(R.id.iv_search) private val mTabLayout: ShortTabLayout by bindView(R.id.tab_layout) private val mStateView: MultipleStateView by bindView(R.id.multiple_state_view) companion object { @JvmStatic fun newInstance(): FeedFragment = FeedFragment() } override fun initView(savedInstanceState: Bundle?) { mPresenter.getDiscoveryTab() //跳转到搜索界面 mIvSearch.setOnClickListener { readyGo() } //跳转到全部分类界面 mTvAllCategory.setOnClickListener { readyGo() } } override fun loadTabSuccess(tabInfo: TabInfo) { mViewPager.adapter = BaseFragmentItemAdapter(childFragmentManager, initFragments(tabInfo), initTitles(tabInfo)) mViewPager.offscreenPageLimit = tabInfo.tabList.size mTabLayout.setupWithViewPager(mViewPager) } private fun initFragments(tabInfo: TabInfo): MutableList { val fragments = mutableListOf() for (i in tabInfo.tabList.indices) { fragments.add(TagDetailInfoFragment.newInstance(tabInfo.tabList[i].apiUrl)) } return fragments } private fun initTitles(tabInfo: TabInfo): MutableList { val titles = mutableListOf() for (i in tabInfo.tabList.indices) { titles.add(tabInfo.tabList[i].name) } return titles } override fun showNetError(onClickListener: View.OnClickListener) { mStateView.showNetError(onClickListener) } override fun showContent() { mStateView.showContent() } override fun getContentViewLayoutId() = R.layout.fragment_feed } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/RankListActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.os.Bundle import android.view.View import android.widget.RelativeLayout import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.TabInfo import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.BaseFragmentItemAdapter import com.jennifer.andy.simpleeyes.ui.feed.presenter.RankListPresenter import com.jennifer.andy.simpleeyes.ui.feed.view.RankListView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.font.FontType import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView import com.jennifer.andy.simpleeyes.widget.tab.ShortTabLayout /** * Author: andy.xwt * Date: 2018/7/26 18:05 * Description:排行榜 */ @Route(path = "/AndyJennifer/ranklist") class RankListActivity : BaseActivity(), RankListView { private val mToolbar: RelativeLayout by bindView(R.id.tool_bar) private val mViewPager: ViewPager by bindView(R.id.view_pager) private val mTabLayout: ShortTabLayout by bindView(R.id.tab_layout) private val mStateView: MultipleStateView by bindView(R.id.multiple_state_view) @Autowired @JvmField var tabIndex: String? = null//哇,这里ARouter居然不能支持Int类型 override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) initToolBar(mToolbar, R.string.open_eyes_english, FontType.LOBSTER) mPresenter.getRankListTab() } override fun loadTabSuccess(tabInfo: TabInfo) { mTabLayout.visibility = View.VISIBLE mViewPager.adapter = BaseFragmentItemAdapter(supportFragmentManager, initFragments(tabInfo), initTitles(tabInfo)) mViewPager.offscreenPageLimit = tabInfo.tabList.size tabIndex?.let { mViewPager.currentItem = it.toInt() } mTabLayout.setupWithViewPager(mViewPager) } private fun initFragments(tabInfo: TabInfo): MutableList { val fragments = mutableListOf() for (i in tabInfo.tabList.indices) { fragments.add(TagDetailInfoFragment.newInstance(tabInfo.tabList[i].apiUrl)) } return fragments } private fun initTitles(tabInfo: TabInfo): MutableList { val titles = mutableListOf() for (i in tabInfo.tabList.indices) { titles.add(tabInfo.tabList[i].name) } return titles } override fun showNetError(onClickListener: View.OnClickListener) { mStateView.showNetError(onClickListener) } override fun showContent() { mStateView.showContent() } override fun getContentViewLayoutId() = R.layout.activity_rank } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/TagActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.os.Bundle import android.widget.RelativeLayout import androidx.fragment.app.Fragment import androidx.viewpager.widget.ViewPager import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.TabDetailInfo import com.jennifer.andy.simpleeyes.entity.TabInfo import com.jennifer.andy.simpleeyes.net.Api import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatActivity import com.jennifer.andy.simpleeyes.ui.base.BaseFragmentItemAdapter import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.tab.ShortTabLayout /** * Author: andy.xwt * Date: 2018/8/3 14:00 * Description: 360 全景 */ @Route(path = "/AndyJennifer/tag") class TagActivity : BaseAppCompatActivity() { private val mToolbar: RelativeLayout by bindView(R.id.tool_bar) private val mTabLayout: ShortTabLayout by bindView(R.id.tab_layout) private val mViewPager: ViewPager by bindView(R.id.view_pager) @Autowired @JvmField var title: String? = null @Autowired @JvmField var id: String? = null override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) initToolBar(mToolbar, title) initTabInfo() } private fun initTabInfo() { /** * 这里拼数据,是因为抓包的时候,没有发现获取tab信息的接口,所以自己拼的数据 */ val tabInfoDetailList = mutableListOf() tabInfoDetailList.add(TabDetailInfo(0, "按时间排序", "${Api.BASE_URL}api/v3/tag/videos?tagId=$id&strategy=date")) tabInfoDetailList.add(TabDetailInfo(1, "按分享排序", "${Api.BASE_URL}api/v3/tag/videos?tagId=$id&strategy=shareCount")) val tabInfo = TabInfo(tabInfoDetailList, 0) mViewPager.adapter = BaseFragmentItemAdapter(supportFragmentManager, initFragments(tabInfo), initTitles(tabInfo)) mViewPager.offscreenPageLimit = tabInfo.tabList.size mTabLayout.setupWithViewPager(mViewPager) } private fun initFragments(tabInfo: TabInfo): MutableList { val fragments = mutableListOf() for (i in tabInfo.tabList.indices) { fragments.add(TagDetailInfoFragment.newInstance(tabInfo.tabList[i].apiUrl)) } return fragments } private fun initTitles(tabInfo: TabInfo): MutableList { val titles = mutableListOf() for (i in tabInfo.tabList.indices) { titles.add(tabInfo.tabList[i].name) } return titles } override fun getContentViewLayoutId() = R.layout.activity_tag } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/TagDetailInfoFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.os.Bundle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.net.Extras import com.jennifer.andy.simpleeyes.ui.base.BaseFragment import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.ui.feed.presenter.TagDetailInfoPresenter import com.jennifer.andy.simpleeyes.ui.feed.view.TagDetailInfoView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.CustomLoadMoreView /** * Author: andy.xwt * Date: 2018/7/3 11:29 * Description:详细的Tab界面 */ class TagDetailInfoFragment : BaseFragment(), TagDetailInfoView { private val mRecyclerView: RecyclerView by bindView(R.id.rv_recycler) private var mAdapter: BaseDataAdapter? = null private lateinit var mApiUrl: String companion object { @JvmStatic fun newInstance(apiUrl: String): TagDetailInfoFragment { val categoryFragment = TagDetailInfoFragment() val bundle = Bundle() bundle.putString(Extras.API_URL, apiUrl) categoryFragment.arguments = bundle return categoryFragment } } override fun getBundleExtras(extras: Bundle) { mApiUrl = extras.getString(Extras.API_URL) } override fun onLazyInitView(savedInstanceState: Bundle?) { mPresenter.getDetailInfo(mApiUrl) } override fun showGetTabInfoSuccess(andyInfo: AndyInfo) { if (mAdapter == null) { mAdapter = BaseDataAdapter(andyInfo.itemList) mAdapter?.setLoadMoreView(CustomLoadMoreView()) mAdapter?.setOnLoadMoreListener({ mPresenter.loadMoreInfo() }, mRecyclerView) mRecyclerView.adapter = mAdapter mRecyclerView.layoutManager = LinearLayoutManager(context) } else { mAdapter?.setNewData(andyInfo.itemList) } } override fun loadMoreSuccess(data: AndyInfo) { mAdapter?.addData(data.itemList) mAdapter?.loadMoreComplete() } override fun showNoMore() { mAdapter?.loadMoreEnd() } override fun getContentViewLayoutId() = R.layout.fragment_tag_detail_info } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/TopicActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.os.Bundle import android.view.View import android.widget.RelativeLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.ui.feed.presenter.TopicPresenter import com.jennifer.andy.simpleeyes.ui.feed.view.TopicView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.CustomLoadMoreView import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView /** * Author: andy.xwt * Date: 2018/7/31 15:37 * Description:专题 */ @Route(path = "/campaign/list") class TopicActivity : BaseActivity(), TopicView { private val mToolbar: RelativeLayout by bindView(R.id.tool_bar) private val mRecyclerView: RecyclerView by bindView(R.id.rv_recycler) private val mStateView: MultipleStateView by bindView(R.id.multiple_state_view) private var mAdapter: BaseDataAdapter? = null @Autowired @JvmField var title: String? = null override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) initToolBar(mToolbar, title) mPresenter.getTopicInfo() } override fun showGetTopicInfoSuccess(itemList: MutableList) { if (mAdapter == null) { mAdapter = BaseDataAdapter(itemList) mAdapter?.setOnLoadMoreListener({ mPresenter.loadMoreInfo() }, mRecyclerView) mAdapter?.setLoadMoreView(CustomLoadMoreView()) mRecyclerView.adapter = mAdapter mRecyclerView.layoutManager = LinearLayoutManager(mContext) } else { mAdapter?.setNewData(itemList) } } override fun loadMoreSuccess(data: AndyInfo) { mAdapter?.addData(data.itemList) mAdapter?.loadMoreComplete() } override fun showNoMore() { mAdapter?.loadMoreEnd() } override fun showNetError(onClickListener: View.OnClickListener) { mStateView.showNetError(onClickListener) } override fun getContentViewLayoutId() = R.layout.activity_topic } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/WebViewActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed import android.os.Build import android.os.Bundle import android.webkit.WebResourceRequest import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import android.widget.ImageView import android.widget.RelativeLayout import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatActivity import com.jennifer.andy.simpleeyes.utils.bindView /** * Author: andy.xwt * Date: 2018/7/20 16:24 * Description:网页展示(设置了标题,设置了能否分享) * jvmField注解注释的不会生成get与set方法。且为public */ @Route(path = "/AndyJennifer/webview/") class WebViewActivity : BaseAppCompatActivity() { private val mToolbar: RelativeLayout by bindView(R.id.tool_bar) private val mIvShare: ImageView by bindView(R.id.iv_share) private val mWebView: WebView by bindView(R.id.web_view) @Autowired @JvmField var title: String? = null @Autowired @JvmField var url: String? = null @Autowired @JvmField var nid: String? = null @Autowired @JvmField var tid: String? = null @Autowired @JvmField var cookie: String? = null @Autowired @JvmField var udid: String? = null @Autowired @JvmField var shareable: Boolean? = false override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) initToolBar(mToolbar, title) initWebView() } private fun initWebView() { mWebView.apply { settings.apply { javaScriptEnabled = true loadsImagesAutomatically = true defaultTextEncodingName = "utf-8" //5.0以上需要支持混合模式 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW } } webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = false } }.loadUrl(url) } override fun getContentViewLayoutId() = R.layout.activity_webview } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/model/FeedModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.model import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.Category import com.jennifer.andy.simpleeyes.entity.Tab import com.jennifer.andy.simpleeyes.net.Api import com.jennifer.andy.simpleeyes.rx.RxThreadHelper import com.jennifer.andy.simpleeyes.rx.error.globalHandleError import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel import io.reactivex.Observable /** * Author: andy.xwt * Date: 2017/9/22 13:52 * Description: */ class FeedModel : BaseModel { /** * 获取发现tab栏 */ fun getDiscoveryTab(): Observable = Api.getDefault() .getDiscoveryTab() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 获取全部分类信息 */ fun loadAllCategoriesInfo(): Observable = Api.getDefault() .getAllCategoriesInfo() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 获取排行榜tab栏 */ fun getRankListTab(): Observable = Api.getDefault() .getRankListTab() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 获取专题信息 */ fun getTopicInfo(): Observable = Api.getDefault() .getTopicInfo() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 获取种类下tab信息 */ fun getCategoryTabIno(id: String): Observable = Api.getDefault() .getCategoryTabInfo(id) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/presenter/AllCategoryPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.feed.model.FeedModel import com.jennifer.andy.simpleeyes.ui.feed.view.AllCategoryView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/7/16 14:18 * Description: */ class AllCategoryPresenter : BasePresenter() { private var mFeedModel: FeedModel = FeedModel() fun loadAllCategoriesInfo() { mFeedModel.loadAllCategoriesInfo().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mView?.loadAllCategoriesSuccess(it) }, { mView?.showNetError(View.OnClickListener { loadAllCategoriesInfo() }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/presenter/CategoryTabPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.feed.model.FeedModel import com.jennifer.andy.simpleeyes.ui.feed.view.CategoryTabView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/8/6 10:48 * Description: */ class CategoryTabPresenter : BasePresenter() { private var mFeedModel: FeedModel = FeedModel() fun getTabInfo(id: String) { mFeedModel.getCategoryTabIno(id).autoDispose(mScopeProvider).subscribe({ mView?.showContent() mView?.showLoadTabSuccess(it) }, { mView?.showNetError(View.OnClickListener { getTabInfo(id) }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/presenter/FeedPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.feed.model.FeedModel import com.jennifer.andy.simpleeyes.ui.feed.view.FeedView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2017/9/22 13:52 * Description: */ class FeedPresenter : BasePresenter() { private var mFeedModel: FeedModel = FeedModel() /** * 获取导航栏信息 */ fun getDiscoveryTab() { mFeedModel.getDiscoveryTab().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mView?.loadTabSuccess(it.tabInfo) }, { mView?.showNetError(View.OnClickListener { getDiscoveryTab() }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/presenter/RankListPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.feed.model.FeedModel import com.jennifer.andy.simpleeyes.ui.feed.view.RankListView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/7/26 18:44 * Description: */ class RankListPresenter : BasePresenter() { private var mFeedModel: FeedModel = FeedModel() fun getRankListTab() { mFeedModel.getRankListTab().autoDispose(mScopeProvider).subscribe({ mView?.loadTabSuccess(it.tabInfo) }, { mView?.showNetError(View.OnClickListener { getRankListTab() }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/presenter/TagDetailInfoPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.presenter import android.view.View import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.presenter.LoadMorePresenter import com.jennifer.andy.simpleeyes.ui.feed.model.FeedModel import com.jennifer.andy.simpleeyes.ui.feed.view.TagDetailInfoView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/7/3 11:30 * Description: */ class TagDetailInfoPresenter : LoadMorePresenter() { override var mBaseModel: FeedModel = FeedModel() /** * 获取tab栏下信息 */ fun getDetailInfo(url: String) { mBaseModel.getDataInfoFromUrl(url).autoDispose(mScopeProvider).subscribe({ mView?.showContent() mView?.showGetTabInfoSuccess(it) mNextPageUrl = it.nextPageUrl if (mNextPageUrl == null) mView?.showNoMore() }, { mView?.showNetError(View.OnClickListener { getDetailInfo(url) }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/presenter/TopicPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.presenter import android.view.View import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.presenter.LoadMorePresenter import com.jennifer.andy.simpleeyes.ui.feed.model.FeedModel import com.jennifer.andy.simpleeyes.ui.feed.view.TopicView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/7/31 15:40 * Description: */ class TopicPresenter : LoadMorePresenter() { override var mBaseModel: FeedModel = FeedModel() /** * 获取专题信息 */ fun getTopicInfo() { mBaseModel.getTopicInfo().autoDispose(mScopeProvider).subscribe({ mView?.showGetTopicInfoSuccess(it.itemList) mNextPageUrl = it.nextPageUrl }, { mView?.showNetError(View.OnClickListener { getTopicInfo() }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/view/AllCategoryView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.view import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2018/7/16 14:18 * Description: */ interface AllCategoryView : BaseView { fun loadAllCategoriesSuccess(andyInfo: AndyInfo) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/view/CategoryTabView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.view import com.jennifer.andy.simpleeyes.entity.Category import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2018/8/6 10:47 * Description: */ interface CategoryTabView : BaseView { /** * 加载分类标签成功 */ fun showLoadTabSuccess(category: Category) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/view/FeedView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.view import com.jennifer.andy.simpleeyes.entity.TabInfo import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2017/9/22 13:53 * Description: */ interface FeedView : BaseView { fun loadTabSuccess(tabInfo: TabInfo) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/view/RankListView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.view import com.jennifer.andy.simpleeyes.entity.TabInfo import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2018/7/26 18:06 * Description: */ interface RankListView : BaseView { fun loadTabSuccess(tabInfo: TabInfo) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/view/TagDetailInfoView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.view import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView /** * Author: andy.xwt * Date: 2018/7/3 11:30 * Description: */ interface TagDetailInfoView : LoadMoreView { fun showGetTabInfoSuccess(andyInfo: AndyInfo) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/feed/view/TopicView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.feed.view import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView /** * Author: andy.xwt * Date: 2018/7/31 15:38 * Description: */ interface TopicView : LoadMoreView { fun showGetTopicInfoSuccess(itemList: MutableList) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/follow/AllAuthorActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.follow import android.os.Bundle import android.view.View import android.widget.RelativeLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.ui.follow.presenter.AllAuthorPresenter import com.jennifer.andy.simpleeyes.ui.follow.view.AllAuthorView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.CustomLoadMoreView import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView /** * Author: andy.xwt * Date: 2019-07-09 18:22 * Description: 全部作者 */ class AllAuthorActivity : BaseActivity(), AllAuthorView { private val mToolBar: RelativeLayout by bindView(R.id.tool_bar) private val mStateView: MultipleStateView by bindView(R.id.multiple_state_view) private val mRecyclerView: RecyclerView by bindView(R.id.rv_recycler) private var mAdapter: BaseDataAdapter? = null override fun initView(savedInstanceState: Bundle?) { initToolBar(mToolBar, R.string.all_author) mPresenter.getAllAuthorInfo() } override fun loadAllAuthorSuccess(it: AndyInfo) { mAdapter = BaseDataAdapter(it.itemList) mAdapter?.setLoadMoreView(CustomLoadMoreView()) mAdapter?.setOnLoadMoreListener({ mPresenter.loadMoreInfo() }, mRecyclerView) mRecyclerView.adapter = mAdapter mRecyclerView.layoutManager = LinearLayoutManager(this) } override fun showNetError(onClickListener: View.OnClickListener) { mStateView.showNetError(onClickListener) } override fun showContent() { mStateView.showContent() } override fun loadMoreSuccess(data: AndyInfo) { mAdapter?.addData(data.itemList) mAdapter?.loadMoreComplete() } override fun showNoMore() { mAdapter?.loadMoreEnd() } override fun getContentViewLayoutId() = R.layout.activity_all_author } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/follow/FollowFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.follow import android.os.Bundle import android.view.View import android.widget.ImageView import androidx.recyclerview.widget.LinearLayoutManager import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.BaseFragment import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.ui.follow.presenter.FollowPresenter import com.jennifer.andy.simpleeyes.ui.follow.view.FollowView import com.jennifer.andy.simpleeyes.ui.search.SearchHotActivity import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.readyGo import com.jennifer.andy.simpleeyes.widget.CustomLoadMoreView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.pull.refresh.PullToRefreshRecyclerView import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView /** * Author: andy.xwt * Date: 2017/9/22 13:51 * Description: 关注 */ class FollowFragment : BaseFragment(), FollowView { private val mTvAllAuthor: CustomFontTextView by bindView(R.id.tv_all_author) private val mIvSearch: ImageView by bindView(R.id.iv_search) private val mStateView: MultipleStateView by bindView(R.id.multiple_state_view) private val mRecyclerView: PullToRefreshRecyclerView by bindView(R.id.rv_follow_recycler) private var mAdapter: BaseDataAdapter? = null companion object { @JvmStatic fun newInstance(): FollowFragment = FollowFragment() } override fun initView(savedInstanceState: Bundle?) { //获取主界面信息 mPresenter.getFollowInfo() //跳转到搜索界面 mIvSearch.setOnClickListener { readyGo() } //跳转到全部作者界面 mTvAllAuthor.setOnClickListener { readyGo() } mRecyclerView.refreshListener = { mPresenter.refresh() } } override fun loadFollowInfoSuccess(andyInfo: AndyInfo) { if (mAdapter == null) { mAdapter = BaseDataAdapter(andyInfo.itemList) mAdapter?.setLoadMoreView(CustomLoadMoreView()) mAdapter?.setOnLoadMoreListener({ mPresenter.loadMoreInfo() }, mRecyclerView.rootView) mRecyclerView.setAdapterAndLayoutManager(mAdapter!!, LinearLayoutManager(context)) } else { mAdapter?.setNewData(andyInfo.itemList) } } override fun showNetError(onClickListener: View.OnClickListener) { mStateView.showNetError(onClickListener) } override fun showContent() { mStateView.showContent() } override fun loadMoreSuccess(data: AndyInfo) { mAdapter?.addData(data.itemList) mAdapter?.loadMoreComplete() } override fun showNoMore() { mAdapter?.loadMoreEnd() } override fun refreshSuccess(andyInfo: AndyInfo) { loadFollowInfoSuccess(andyInfo) mRecyclerView.refreshComplete() } override fun getContentViewLayoutId() = R.layout.fragment_follow } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/follow/model/FollowModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.follow.model import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.net.Api import com.jennifer.andy.simpleeyes.rx.RxThreadHelper import com.jennifer.andy.simpleeyes.rx.error.globalHandleError import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel import io.reactivex.Observable /** * Author: andy.xwt * Date: 2017/9/22 13:54 * Description: */ class FollowModel : BaseModel { /** * 获取关注首页信息 */ fun getFollowInfo(): Observable = Api.getDefault() .getFollowInfo() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 获取全部作者 */ fun getAllAuthor(): Observable = Api.getDefault() .getAllAuthor() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/follow/presenter/AllAuthorPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.follow.presenter import android.view.View import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.presenter.LoadMorePresenter import com.jennifer.andy.simpleeyes.ui.follow.model.FollowModel import com.jennifer.andy.simpleeyes.ui.follow.view.AllAuthorView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2019-07-09 18:23 * Description: */ class AllAuthorPresenter : LoadMorePresenter() { override var mBaseModel: FollowModel = FollowModel() fun getAllAuthorInfo() { mBaseModel.getAllAuthor().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mNextPageUrl = it.nextPageUrl mView?.loadAllAuthorSuccess(it) }, { mView?.showNetError(View.OnClickListener { getAllAuthorInfo() }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/follow/presenter/FollowPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.follow.presenter import android.view.View import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.presenter.LoadMorePresenter import com.jennifer.andy.simpleeyes.ui.follow.model.FollowModel import com.jennifer.andy.simpleeyes.ui.follow.view.FollowView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2017/9/22 13:54 * Description: */ class FollowPresenter : LoadMorePresenter() { override var mBaseModel: FollowModel = FollowModel() fun getFollowInfo() { mBaseModel.getFollowInfo().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mNextPageUrl = it.nextPageUrl mView?.loadFollowInfoSuccess(it) }, { mView?.showNetError(View.OnClickListener { getFollowInfo() }) }) } fun refresh() { mBaseModel.getFollowInfo().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mNextPageUrl = it.nextPageUrl mView?.refreshSuccess(it) }, { mView?.showNetError(View.OnClickListener { refresh() }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/follow/view/AllAuthorView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.follow.view import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView /** * Author: andy.xwt * Date: 2019-07-09 18:22 * Description: */ interface AllAuthorView : LoadMoreView { fun loadAllAuthorSuccess(it: AndyInfo) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/follow/view/FollowView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.follow.view import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView /** * Author: andy.xwt * Date: 2017/9/22 13:54 * Description: */ interface FollowView : LoadMoreView { fun loadFollowInfoSuccess(andyInfo: AndyInfo) fun refreshSuccess(andyInfo: AndyInfo) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/DailyEliteActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home import android.os.Bundle import android.view.View import android.widget.ImageView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.ui.home.adapter.DailyEliteAdapter import com.jennifer.andy.simpleeyes.ui.home.presenter.DailyElitePresenter import com.jennifer.andy.simpleeyes.ui.home.view.DailyEliteView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.pull.refresh.LinearLayoutManagerWithSmoothScroller import com.jennifer.andy.simpleeyes.widget.pull.refresh.PullToRefreshRecyclerView import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView /** * Author: andy.xwt * Date: 2018/4/20 16:09 * Description:每日编辑精选 */ class DailyEliteActivity : BaseActivity(), DailyEliteView { private val mBackImageView: ImageView by bindView(R.id.iv_back) private val mRecycler: PullToRefreshRecyclerView by bindView(R.id.rv_recycler) private val mTvDate: CustomFontTextView by bindView(R.id.tv_date) private val mStateView: MultipleStateView by bindView(R.id.multiple_state_view) private var mDailyEliteAdapter: DailyEliteAdapter? = null private lateinit var mLinearLayoutManager: LinearLayoutManagerWithSmoothScroller override fun initView(savedInstanceState: Bundle?) { mPresenter.getDailyElite() mRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { //设置当前选择日期 val position = mLinearLayoutManager.findFirstVisibleItemPosition() if (mDailyEliteAdapter?.getItemViewType(position) == BaseDataAdapter.TEXT_CARD_TYPE) { mTvDate.text = mDailyEliteAdapter?.getItem(position)?.data?.text } } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { val layoutManager = mRecycler.rootView.layoutManager as LinearLayoutManager //滚动到日期及完毕 if (newState == RecyclerView.SCROLL_STATE_IDLE && layoutManager.findFirstVisibleItemPosition() == mDailyEliteAdapter!!.getCurrentDayPosition()) { mRecycler.refreshComplete() } } }) mRecycler.refreshListener = { mPresenter.refresh() } mBackImageView.setOnClickListener { finish() } } override fun showGetDailySuccess(content: MutableList) { if (mDailyEliteAdapter == null) { mDailyEliteAdapter = DailyEliteAdapter(content) mDailyEliteAdapter?.setOnLoadMoreListener({ mPresenter.loadMoreResult() }, mRecycler.rootView) mLinearLayoutManager = LinearLayoutManagerWithSmoothScroller(mContext) mRecycler.setAdapterAndLayoutManager(mDailyEliteAdapter!!, mLinearLayoutManager) } else { mDailyEliteAdapter?.setNewData(content) } //这里有可能加载的每日精选,没有text类型,就会导致刷新完毕后,刷新界面没有消失 if (mDailyEliteAdapter!!.getCurrentDayPosition() != 0) { //这里发送延时消息,是因为数据还有可能没有装载完毕 mRecycler.handler.postDelayed({ mRecycler.smoothScrollToPosition(mDailyEliteAdapter!!.getCurrentDayPosition()) }, 200) } else { mRecycler.refreshComplete() } } override fun showRefreshSuccess(content: MutableList) { showGetDailySuccess(content) } override fun loadMoreSuccess(data: MutableList) { mDailyEliteAdapter?.addData(data) mDailyEliteAdapter?.loadMoreComplete() } override fun showNoMore() { mDailyEliteAdapter?.loadMoreEnd() } override fun showNetError(onClickListener: View.OnClickListener) { mStateView.showNetError(onClickListener) } override fun getContentViewLayoutId() = R.layout.activity_daily_elite } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/HomeFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home import android.os.Bundle import android.view.ViewGroup import android.widget.LinearLayout import androidx.recyclerview.widget.LinearLayoutManager import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.BaseFragment import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter import com.jennifer.andy.simpleeyes.ui.home.presenter.HomePresenter import com.jennifer.andy.simpleeyes.ui.home.view.HomeView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.getScreenHeight import com.jennifer.andy.simpleeyes.utils.getScreenWidth import com.jennifer.andy.simpleeyes.widget.CustomLoadMoreView import com.jennifer.andy.simpleeyes.widget.pull.head.HomePageHeaderView import com.jennifer.andy.simpleeyes.widget.pull.zoom.PullToZoomBase import com.jennifer.andy.simpleeyes.widget.pull.zoom.PullToZoomRecyclerView /** * Author: andy.xwt * Date: 2017/9/19 18:45 * Description:首页 */ class HomeFragment : BaseFragment(), HomeView { private val mPullToZoomRecycler: PullToZoomRecyclerView by bindView(R.id.rv_home_recycler) private lateinit var mHomePageHeaderView: HomePageHeaderView private var mCateGoryAdapter: BaseDataAdapter? = null companion object { @JvmStatic fun newInstance(): HomeFragment = HomeFragment() } override fun initView(savedInstanceState: Bundle?) { mPullToZoomRecycler.setOnPullZoomListener(object : PullToZoomBase.OnPullZoomListener { override fun onPullZooming(scrollValue: Int) { mHomePageHeaderView.showRefreshCover(scrollValue) } override fun onPullZoomEnd() { if (mHomePageHeaderView.judgeCanRefresh()) {//只有达到刷新的阀值,上升才刷新,其他情况不刷新 mPresenter.refreshCategoryData() } else { mHomePageHeaderView.hideRefreshCover() } } }) mPresenter.loadCategoryData() } override fun loadDataSuccess(andyInfo: AndyInfo) { if (mCateGoryAdapter == null) { setHeaderInfo(andyInfo) setAdapterAndListener(andyInfo) } else { mCateGoryAdapter?.setNewData(andyInfo.itemList) } } private fun setAdapterAndListener(andyInfo: AndyInfo) { val recyclerView = mPullToZoomRecycler.getPullRootView() recyclerView.setItemViewCacheSize(10) mCateGoryAdapter = BaseDataAdapter(andyInfo.itemList) mCateGoryAdapter?.setOnLoadMoreListener({ mPresenter.loadMoreCategoryData() }, recyclerView) mCateGoryAdapter?.setLoadMoreView(CustomLoadMoreView()) mPullToZoomRecycler.setAdapterAndLayoutManager(mCateGoryAdapter!!, LinearLayoutManager(_mActivity)) } private fun setHeaderInfo(andyInfo: AndyInfo) { mHomePageHeaderView = HomePageHeaderView(context!!) val lp = ViewGroup.LayoutParams(context!!.getScreenWidth(), context!!.getScreenHeight() / 2) mPullToZoomRecycler.setHeaderViewLayoutParams(LinearLayout.LayoutParams(lp)) mHomePageHeaderView.setHeaderInfo(andyInfo.topIssue, andyInfo.topIssue.data.itemList, this) mPullToZoomRecycler.setHeaderView(mHomePageHeaderView) } override fun refreshDataSuccess(andyInfo: AndyInfo) { mCateGoryAdapter?.removeAllFooterView() mCateGoryAdapter?.setNewData(andyInfo.itemList) mHomePageHeaderView.hideRefreshCover() } override fun loadMoreSuccess(andyInfo: AndyInfo) { mCateGoryAdapter?.addData(andyInfo.itemList) mCateGoryAdapter?.loadMoreComplete() } override fun showNoMore() { mCateGoryAdapter?.loadMoreEnd() } fun scrollToTop() { mPullToZoomRecycler.scrollToTop() } override fun getContentViewLayoutId() = R.layout.fragment_home } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/adapter/CollectionCardCoverAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.adapter import android.widget.TextView import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseViewHolder import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.utils.getElapseTimeForShow /** * Author: andy.xwt * Date: 2017/11/6 16:14 * Description:集合卡片适配器 */ class CollectionCardCoverAdapter(data: MutableList) : BaseQuickAdapter(R.layout.item_collection_card_cover, data) { override fun convert(helper: BaseViewHolder, item: Content) { val imageCover = helper.getView(R.id.iv_image) val title = helper.getView(R.id.tv_title) val desc = helper.getView(R.id.tv_desc) helper.setGone(R.id.iv_daily, item.data.library == "DAILY") if (item.type != "actionCard") {//集合 imageCover.setImageURI(item.data.cover.feed) title.text = item.data.title val description = "#${item.data.category} / ${getElapseTimeForShow(item.data.duration)}" val elite = if (item.data.library == "DAILY") " / ${mContext.getString(R.string.elite)}" else "" desc.text = description + elite } else {//显示全部 helper.setGone(R.id.tv_show_all, true) helper.setGone(R.id.ll_container, false) helper.addOnClickListener(R.id.tv_show_all) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/adapter/DailyEliteAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.adapter import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.adapter.BaseDataAdapter /** * Author: andy.xwt * Date: 2018/4/20 17:04 * Description:每日编辑精选 */ class DailyEliteAdapter(data: MutableList) : BaseDataAdapter(data) { /** * 获取第一个日期位置 */ fun getCurrentDayPosition(): Int { for (i in 0 until mData.size) { if (getItemViewType(i) == TEXT_CARD_TYPE) { return i } } return 0 } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/adapter/SquareCollectionAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.adapter import android.widget.FrameLayout import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseViewHolder import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.widget.font.FontType import com.jennifer.andy.simpleeyes.widget.font.TypefaceManager /** * Author: andy.xwt * Date: 2017/11/6 10:49 * Description:矩形卡片适配器 */ class SquareCollectionAdapter(data: MutableList) : BaseQuickAdapter(R.layout.item_square_collection, data) { override fun convert(helper: BaseViewHolder, item: Content) { val imageView = helper.getView(R.id.iv_simple_image) val coverView = helper.getView(R.id.fl_cover) if (item.type != "actionCard") {//分类 imageView.setImageURI(item.data.image) helper.setText(R.id.tv_title, item.data.title) coverView.foreground = mContext?.resources?.getDrawable(R.drawable.selector_item_square_foreground,null) } else {//显示全部 imageView.setImageDrawable(mContext.resources.getDrawable(R.drawable.shape_show_all_border,null)) helper.setText(R.id.tv_title, item.data.text) helper.setTextColor(R.id.tv_title, mContext.resources.getColor(R.color.black)) helper.setTypeface(R.id.tv_title, TypefaceManager.getTypeFace(FontType.NORMAL)) coverView.foreground = null } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/model/HomeModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.model import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.JenniferInfo import com.jennifer.andy.simpleeyes.net.Api import com.jennifer.andy.simpleeyes.rx.RxThreadHelper import com.jennifer.andy.simpleeyes.rx.error.globalHandleError import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel import io.reactivex.Observable import java.util.concurrent.TimeUnit /** * Author: andy.xwt * Date: 2017/9/19 18:00 * Description: */ class HomeModel : BaseModel { /** * 加载首页信息 */ fun loadCategoryInfo(): Observable = Api.getDefault() .getHomeInfo() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 刷新主页信息,延迟1秒执行 */ fun refreshCategoryInfo(): Observable = Api.getDefault() .getHomeInfo() .delay(1000, TimeUnit.MILLISECONDS) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 热门关键词获取 */ fun getHotWord(): Observable> = Api.getDefault() .getHotWord() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 根据关键字搜索视频 */ fun searchVideoByWord(word: String): Observable = Api.getDefault() .searchVideoByWord(word) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 获取每日编辑精选 */ fun getDailyElite(): Observable = Api.getDefault() .getDailyElite() .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/presenter/DailyElitePresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.presenter import android.view.View import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.entity.JenniferInfo import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.home.model.HomeModel import com.jennifer.andy.simpleeyes.ui.home.view.DailyEliteView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/4/20 16:12 * Description: */ class DailyElitePresenter : BasePresenter() { private var mHomeModel: HomeModel = HomeModel() private var mNextPageUrl: String? = null fun getDailyElite() { mHomeModel.getDailyElite().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mNextPageUrl = it.nextPageUrl mView?.showGetDailySuccess(combineContentInfo(it)) }, { mView?.showNetError(View.OnClickListener { getDailyElite() }) }) } /** * 刷新 */ fun refresh() { mHomeModel.getDailyElite().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mNextPageUrl = it.nextPageUrl mView?.showRefreshSuccess(combineContentInfo(it)) }, { mView?.showNetError(View.OnClickListener { refresh() }) }) } /** * 加載更多 */ fun loadMoreResult() { mHomeModel.loadMoreJenniferInfo(mNextPageUrl).autoDispose(mScopeProvider).subscribe({ mNextPageUrl = it.nextPageUrl mView?.loadMoreSuccess(combineContentInfo(it)) }, { mView?.showNetError(View.OnClickListener { getDailyElite() }) }) } private fun combineContentInfo(jenniferInfo: JenniferInfo): MutableList { val list = mutableListOf() val issueList = jenniferInfo.issueList for (contentBean in issueList) { val itemList = contentBean.itemList for (content in itemList) { list.add(content) } } return list } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/presenter/HomePresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.home.model.HomeModel import com.jennifer.andy.simpleeyes.ui.home.view.HomeView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2017/9/19 17:58 * Description: */ class HomePresenter : BasePresenter() { private var mHomeModel: HomeModel = HomeModel() private var mNextPageUrl: String? = null /** * 加载首页信息 */ fun loadCategoryData() { mView?.showLoading() mHomeModel.loadCategoryInfo().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mNextPageUrl = it.nextPageUrl mView?.loadDataSuccess(it) }, { mView?.showNetError(View.OnClickListener { loadCategoryData() }) }) } /** * 刷新首页信息延迟1秒执行 */ fun refreshCategoryData() { mHomeModel.refreshCategoryInfo().autoDispose(mScopeProvider).subscribe({ mView?.showContent() mNextPageUrl = it.nextPageUrl mView?.refreshDataSuccess(it) }, { mView?.showNetError(View.OnClickListener { refreshCategoryData() }) }) } /** * 加载更多首页信息 */ fun loadMoreCategoryData() { if (mNextPageUrl != null) { mHomeModel.loadMoreAndyInfo(mNextPageUrl).autoDispose(mScopeProvider).subscribe({ mView?.showContent() if (it.nextPageUrl == null) { mView?.showNoMore() } else { mNextPageUrl = it.nextPageUrl mView?.loadMoreSuccess(it) } }, { mView?.showNetError(View.OnClickListener { loadMoreCategoryData() }) }) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/view/DailyEliteView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.view import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView /** * Author: andy.xwt * Date: 2018/4/20 16:13 * Description: */ interface DailyEliteView : LoadMoreView> { fun showGetDailySuccess(content: MutableList) fun showRefreshSuccess(content: MutableList) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/home/view/HomeView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.home.view import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2017/9/19 17:59 * Description: */ interface HomeView : BaseView { /** * 加载信息成功 */ fun loadDataSuccess(andyInfo: AndyInfo) /** * 加载信息成功 */ fun refreshDataSuccess(andyInfo: AndyInfo) /** * 加载更多成功 */ fun loadMoreSuccess(andyInfo: AndyInfo) /** * 没有更多 */ fun showNoMore() } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/login/LoginActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.login import android.os.Bundle import android.widget.ImageView import com.alibaba.android.arouter.facade.annotation.Route import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatActivity import com.jennifer.andy.simpleeyes.utils.bindView /** * Author: andy.xwt * Date: 2019/1/14 19:22 * Description: */ @Route(path = "/github/Login") class LoginActivity : BaseAppCompatActivity() { private val mIvClose: ImageView by bindView(R.id.iv_close) override fun initView(savedInstanceState: Bundle?) { mIvClose.setOnClickListener { finish() } } override fun getContentViewLayoutId() = R.layout.activity_login } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/CacheFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile import android.os.Bundle import android.view.View import android.widget.ImageView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.ui.base.BaseFragment import com.jennifer.andy.simpleeyes.ui.profile.presenter.CachePresenter import com.jennifer.andy.simpleeyes.ui.profile.view.CacheView import com.jennifer.andy.simpleeyes.utils.bindView /** * Author: andy.xwt * Date: 2019-09-12 15:51 * Description:我的缓存 */ class CacheFragment : BaseFragment(), CacheView, View.OnClickListener { private val mIvBack: ImageView by bindView(R.id.iv_back) companion object { @JvmStatic fun newInstance(): CacheFragment = CacheFragment() } override fun initView(savedInstanceState: Bundle?) { super.initView(savedInstanceState) mIvBack.setOnClickListener(this) } override fun onClick(view: View?) { when (view?.id) { R.id.iv_back -> { pop() } else -> { } } //todo 这里打算用WorkManager来做 } override fun getContentViewLayoutId() = R.layout.fragment_cache } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/ProfileFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile import android.os.Bundle import android.view.View import android.widget.FrameLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.alibaba.android.arouter.launcher.ARouter import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.ui.base.BaseFragment import com.jennifer.andy.simpleeyes.ui.profile.adapter.ProfileSettingAdapter import com.jennifer.andy.simpleeyes.ui.profile.presenter.ProfilePresenter import com.jennifer.andy.simpleeyes.ui.profile.view.ProfileView import com.jennifer.andy.simpleeyes.utils.bindView /** * Author: andy.xwt * Date: 2017/9/22 13:51 * Description:我的 */ class ProfileFragment : BaseFragment(), ProfileView, View.OnClickListener { private val mRecycler: RecyclerView by bindView(R.id.rv_profile_recycler) private val mIvAvatar: SimpleDraweeView by bindView(R.id.iv_avatar) private val mFlComment: FrameLayout by bindView(R.id.fl_comment_container) companion object { @JvmStatic fun newInstance(): ProfileFragment = ProfileFragment() private const val MESSAGE = 0 //我的消息 private const val FOCUS = 1 //我的关注 private const val CACHE_POSITION = 2//我的缓存 private const val WATH_HISTORY = 3//观看记录 } override fun initView(savedInstanceState: Bundle?) { mIvAvatar.setOnClickListener(this) mFlComment.setOnClickListener(this) mRecycler.apply { val stringArray = resources.getStringArray(R.array.profile_setting) adapter = ProfileSettingAdapter(arrayListOf(*stringArray)) .apply { setOnItemClickListener { _, _, position -> when (position) { CACHE_POSITION -> {//我的缓存 start(CacheFragment.newInstance()) } } } } layoutManager = LinearLayoutManager(context) } } override fun onClick(view: View) { when (view.id) { R.id.iv_avatar, R.id.fl_comment_container -> {//跳转到登录界面 ARouter.getInstance().build("/github/Login").navigation() } } } override fun getContentViewLayoutId() = R.layout.fragment_profile } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/adapter/ProfileSettingAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile.adapter import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseViewHolder import com.jennifer.andy.simpleeyes.R /** * Author: andy.xwt * Date: 2019/1/15 18:59 * Description: */ class ProfileSettingAdapter(data: MutableList?) : BaseQuickAdapter(R.layout.item_profile_setting, data) { override fun convert(helper: BaseViewHolder, item: String) { helper.setText(R.id.tv_text, item) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/model/ProfileModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile.model import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel /** * Author: andy.xwt * Date: 2017/9/22 13:55 * Description: */ class ProfileModel : BaseModel ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/presenter/CachePresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile.presenter import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.profile.view.CacheView /** * Author: andy.xwt * Date: 2019-09-13 16:09 * Description: */ class CachePresenter : BasePresenter() { } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/presenter/ProfilePresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile.presenter import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.profile.view.ProfileView /** * Author: andy.xwt * Date: 2017/9/22 13:55 * Description: */ class ProfilePresenter : BasePresenter() { } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/view/CacheView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile.view import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2019-09-13 16:08 * Description: */ interface CacheView : BaseView { } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/profile/view/ProfileView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.profile.view import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2017/9/22 13:56 * Description: */ interface ProfileView : BaseView { } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/search/SearchHotActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.search import android.animation.ValueAnimator import android.graphics.drawable.Drawable import android.os.Bundle import android.text.Spannable import android.text.SpannableStringBuilder import android.view.Gravity import android.view.View import android.view.animation.AccelerateInterpolator import android.widget.RelativeLayout import android.widget.TextView import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.chad.library.adapter.base.BaseQuickAdapter import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.search.adapter.SearchHotAdapter import com.jennifer.andy.simpleeyes.ui.search.adapter.SearchVideoAdapter import com.jennifer.andy.simpleeyes.ui.search.presenter.SearchPresenter import com.jennifer.andy.simpleeyes.ui.search.view.SearchHotView import com.jennifer.andy.simpleeyes.ui.video.VideoDetailActivity import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.dip2px import com.jennifer.andy.simpleeyes.utils.showKeyboard import com.jennifer.andy.simpleeyes.widget.CustomLoadMoreView import com.jennifer.andy.simpleeyes.widget.SearchHotRemindView import com.jennifer.andy.simpleeyes.widget.image.CenterAlignImageSpan import com.jennifer.andy.simpleeyes.widget.state.MultipleStateView /** * Author: andy.xwt * Date: 2018/4/3 10:54 * Description:搜索界面 */ class SearchHotActivity : BaseActivity(), SearchHotView { private val mSearchRemind: SearchHotRemindView by bindView(R.id.rl_search_remind) private val mSearchView: SearchView by bindView(R.id.searchView) private val mTvCancel: TextView by bindView(R.id.tv_cancel) private val multipleStateView: MultipleStateView by bindView(R.id.multiple_state_view) private val mRecycler: RecyclerView by bindView(R.id.rv_search_recycler) private lateinit var mHotSearchAdapter: SearchHotAdapter private lateinit var mSearchVideoAdapter: SearchVideoAdapter override fun initView(savedInstanceState: Bundle?) { initSearchView() mTvCancel.setOnClickListener { finish() } mPresenter.searchHot() } private fun initSearchView() { mSearchView.isIconified = false mSearchView.setIconifiedByDefault(false) //设置输入框提示文字样式 val searchComplete = mSearchView.findViewById(R.id.search_src_text) searchComplete.gravity = Gravity.CENTER searchComplete.setHintTextColor(resources.getColor(R.color.gray_66A2A2A2)) searchComplete.textSize = 13f searchComplete.hint = getDecoratedHint(searchComplete.hint, getDrawable(R.drawable.ic_action_search_no_padding), 50) //添加搜索监听 mSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { showKeyboard(false) mPresenter.searchVideoByWord(query) return true } override fun onQueryTextChange(newText: String?): Boolean { return false } }) } /** * 设置输入框提示文字 */ private fun getDecoratedHint(hintText: CharSequence, searchHintIcon: Drawable, drawableSize: Int): CharSequence { searchHintIcon.setBounds(0, 0, drawableSize, drawableSize) val ssb = SpannableStringBuilder(" ") ssb.setSpan(CenterAlignImageSpan(searchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) ssb.append(hintText) return ssb } override fun getHotWordSuccess(hotList: MutableList) { setRecyclerMargin() startContentAnimation() mHotSearchAdapter = SearchHotAdapter(hotList) mHotSearchAdapter.setOnItemClickListener { _, _, position -> showKeyboard(false) mPresenter.searchVideoByWord(mHotSearchAdapter.getItem(position)!!) } val flexBoxLayoutManager = FlexboxLayoutManager(mContext, FlexDirection.ROW) flexBoxLayoutManager.justifyContent = JustifyContent.CENTER mRecycler.layoutManager = flexBoxLayoutManager mRecycler.adapter = mHotSearchAdapter } /** * 进入内容动画 */ private fun startContentAnimation() { val valueAnimator = ValueAnimator.ofInt(multipleStateView.measuredHeight, 0) valueAnimator.duration = 500 valueAnimator.interpolator = AccelerateInterpolator() valueAnimator.addUpdateListener { val value = it.animatedValue as Int multipleStateView.scrollTo(0, value) } valueAnimator.start() } override fun showSearchSuccess(queryWord: String, andyInfo: AndyInfo) { //设置搜索结果 mSearchRemind.setSearchResult(queryWord, andyInfo.total) //添加了过滤规则,防止搜索的时候崩溃 mSearchVideoAdapter = SearchVideoAdapter(andyInfo.itemList.filter { it.type == SearchVideoAdapter.VIDEO_COLLECTION_WITH_BRIEF || it.type == SearchVideoAdapter.VIDEO }) //跳转到播放界面 mSearchVideoAdapter.onItemClickListener = BaseQuickAdapter.OnItemClickListener { _, _, position -> val item = mSearchVideoAdapter.getItem(position) if (item?.type == SearchVideoAdapter.VIDEO) { VideoDetailActivity.start(mContext, item.data, arrayListOf(), position) } } if (andyInfo.itemList.size > 0) { resetRecyclerMargin() } else { mSearchVideoAdapter.setEmptyView(R.layout.empty_search_word, mRecycler) } mSearchVideoAdapter.setOnLoadMoreListener({ mPresenter.loadMoreInfo() }, mRecycler) mSearchVideoAdapter.setLoadMoreView(CustomLoadMoreView()) mRecycler.layoutManager = LinearLayoutManager(mContext) mRecycler.adapter = mSearchVideoAdapter //处理没有更多的情况 if (andyInfo.nextPageUrl == null) { mSearchVideoAdapter.loadMoreEnd() } } override fun loadMoreSuccess(andyInfo: AndyInfo) { mSearchVideoAdapter.addData(andyInfo.itemList) mSearchVideoAdapter.loadMoreComplete() } override fun showNoMore() { mSearchVideoAdapter.loadMoreEnd() } override fun showLoading() { multipleStateView.showLoading() } override fun showContent() { multipleStateView.showContent() } override fun showNetError(onClickListener: View.OnClickListener) { multipleStateView.showNetError(onClickListener) } /** * 重置RecyclerMargin */ private fun resetRecyclerMargin() { val lp = mRecycler.layoutParams as RelativeLayout.LayoutParams lp.marginEnd = 0 lp.marginStart = 0 mRecycler.layoutParams = lp } /** * 设置RecyclerMargin */ private fun setRecyclerMargin() { val lp = mRecycler.layoutParams as RelativeLayout.LayoutParams lp.marginEnd = dip2px(30f) lp.marginStart = dip2px(30f) mRecycler.layoutParams = lp } override fun toggleOverridePendingTransition() = true override fun getOverridePendingTransition() = TransitionMode.TOP override fun getContentViewLayoutId() = R.layout.fragment_search_hot } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/search/adapter/CollectionBriefAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.search.adapter import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseViewHolder import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.utils.getElapseTimeForShow /** * Author: andy.xwt * Date: 2018/4/9 11:01 * Description: */ class CollectionBriefAdapter(data: MutableList) : BaseQuickAdapter(R.layout.item_collection_brief, data) { override fun convert(helper: BaseViewHolder, item: Content) { with(helper) { getView(R.id.iv_image).setImageURI(item.data.cover.feed) setText(R.id.tv_title, item.data.title) val description = "#${item.data.category} / ${getElapseTimeForShow(item.data.duration)}" setText(R.id.tv_desc, description) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/search/adapter/SearchHotAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.search.adapter import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseViewHolder import com.jennifer.andy.simpleeyes.R /** * Author: andy.xwt * Date: 2018/4/8 10:16 * Description: */ class SearchHotAdapter(data: MutableList) : BaseQuickAdapter(R.layout.item_hot_search, data) { override fun convert(helper: BaseViewHolder, item: String) { helper.setText(R.id.tv_text, item) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/search/adapter/SearchVideoAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.search.adapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseQuickAdapter.OnItemClickListener import com.chad.library.adapter.base.BaseViewHolder import com.chad.library.adapter.base.util.MultiTypeDelegate import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.video.VideoDetailActivity import com.jennifer.andy.simpleeyes.utils.getElapseTimeForShow import com.jennifer.andy.simpleeyes.widget.ItemHeaderView import java.util.* /** * Author: andy.xwt * Date: 2018/4/9 10:19 * Description: */ class SearchVideoAdapter(data: List?) : BaseQuickAdapter(data) { /** * 卡片类型 */ companion object { const val VIDEO_COLLECTION_WITH_BRIEF_TYPE = 0 const val VIDEO_TYPE = 1 const val VIDEO_COLLECTION_WITH_BRIEF = "videoCollectionWithBrief" const val VIDEO = "video" } init { multiTypeDelegate = object : MultiTypeDelegate() { override fun getItemType(content: Content?): Int { when (content?.type) { VIDEO_COLLECTION_WITH_BRIEF -> return VIDEO_COLLECTION_WITH_BRIEF_TYPE VIDEO -> return VIDEO_TYPE } return VIDEO_TYPE } } with(multiTypeDelegate) { registerItemType(VIDEO_COLLECTION_WITH_BRIEF_TYPE, R.layout.layout_collection_with_brief) registerItemType(VIDEO_TYPE, R.layout.layout_single_video) } } override fun convert(helper: BaseViewHolder?, content: Content) { when (helper?.itemViewType) { VIDEO_COLLECTION_WITH_BRIEF_TYPE -> setCollectionBriefInfo(helper, content) VIDEO_TYPE -> setSingleVideoInfo(helper, content) } } /** * 设置带关注人的视频集合信息 */ private fun setCollectionBriefInfo(helper: BaseViewHolder, content: Content) { //设置视频集合信息 with(helper) { getView(R.id.rv_recycler).apply { isNestedScrollingEnabled = false layoutManager = LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false) adapter = CollectionBriefAdapter(content.data.itemList).apply { onItemClickListener = OnItemClickListener { _, _, position -> //跳转到播放视频详情 val item = getItem(position) VideoDetailActivity.start(mContext, item!!.data, data as ArrayList, position) } } } //设置头布局 content.data.header?.let { getView(R.id.item_header_view).setHeader(it, content.data.dataType) } } } /** * 设置单视频信息 */ private fun setSingleVideoInfo(helper: BaseViewHolder, item: Content) { val imageView = helper.getView(R.id.iv_image) imageView.setImageURI(item.data.cover.feed) helper.setText(R.id.tv_single_title, item.data.title) val description = "#${item.data.category} / ${getElapseTimeForShow(item.data.duration)}" helper.setText(R.id.tv_single_desc, description) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/search/presenter/SearchPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.search.presenter import android.view.View import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.presenter.LoadMorePresenter import com.jennifer.andy.simpleeyes.ui.home.model.HomeModel import com.jennifer.andy.simpleeyes.ui.search.view.SearchHotView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/4/3 11:05 * Description: */ class SearchPresenter : LoadMorePresenter() { override var mBaseModel: HomeModel = HomeModel() /** * 获取热门搜索 */ fun searchHot() { mBaseModel.getHotWord().autoDispose(mScopeProvider).subscribe { mView?.getHotWordSuccess(it) } } /** * 根据关键字搜索 */ fun searchVideoByWord(word: String) { mView?.showLoading() mBaseModel.searchVideoByWord(word).autoDispose(mScopeProvider).subscribe({ mView?.showContent() mView?.showSearchSuccess(word, it) mNextPageUrl = it.nextPageUrl }, { mView?.showNetError(View.OnClickListener { searchVideoByWord(word) }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/search/view/SearchView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.search.view import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.ui.base.LoadMoreView /** * Author: andy.xwt * Date: 2018/4/3 11:06 * Description: */ interface SearchHotView : LoadMoreView { /** * 获取热门关键词成功 */ fun getHotWordSuccess(hotList: MutableList) /** * 搜索成功 */ fun showSearchSuccess(queryWord: String, andyInfo: AndyInfo) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/splash/LandingActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.splash import android.os.Bundle import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.UserPreferences import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatActivity /** * Author: andy.xwt * Date: 2018/8/3 09:45 * Description:加载页,用于区分显示广告,还是视频,还是本地的加载页 */ class LandingActivity : BaseAppCompatActivity() { override fun initView(savedInstanceState: Bundle?) { if (UserPreferences.getUserIsFirstLogin()) {//如果是第一次进入就加载视频界面 loadRootFragment(R.id.fl_container, VideoLandingFragment.newInstance()) } else {//如果不是加载常规加载界面 loadRootFragment(R.id.fl_container, LocalCommonLandingFragment.newInstance()) } } override fun getContentViewLayoutId(): Int = R.layout.activity_landing } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/splash/LocalCommonLandingFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.splash import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.os.Bundle import android.view.View import android.widget.ImageView import android.widget.RelativeLayout import android.widget.TextView import androidx.core.animation.doOnEnd import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.UserPreferences import com.jennifer.andy.simpleeyes.ui.MainActivity import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatFragment import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.dip2px import com.jennifer.andy.simpleeyes.utils.getDateString import com.jennifer.andy.simpleeyes.utils.readyGoThenKillSelf import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import java.util.* /** * Author: andy.xwt * Date: 2018/8/3 09:56 * Description:本地的加载页(上升动画) */ class LocalCommonLandingFragment : BaseAppCompatFragment() { private val mIvBackground by bindView(R.id.iv_background) private val mLoadingContainer by bindView(R.id.rl_loading_container) private val mMoveContainer by bindView(R.id.ll_move_container) private val mHeadOuter by bindView(R.id.iv_head_outer) private val mHeadInner by bindView(R.id.iv_head_inner) private val mName by bindView(R.id.tv_name) private val mForToday by bindView(R.id.tv_today) private val mDate by bindView(R.id.tv_date) private val mTodayChose by bindView(R.id.tv_today_chose) companion object { @JvmStatic fun newInstance(): LocalCommonLandingFragment = LocalCommonLandingFragment() } override fun initView(savedInstanceState: Bundle?) { //如果用户没有执行上升动画,则执行,反之则执行缩放动画 if (!UserPreferences.getShowUserAnim()) { doUpAnimator() doBackgroundAnimator() } else { doScaleAnimator() } } /** * 执行上升动画 */ private fun doUpAnimator() { val moveY = _mActivity.dip2px(100f) ObjectAnimator.ofFloat(mMoveContainer, "translationY", 0f, -moveY.toFloat()).apply { addUpdateListener { if (it.currentPlayTime in 600..1500) { mHeadOuter.setImageResource(R.drawable.ic_eye_white_outer) mHeadInner.setImageResource(R.drawable.ic_eye_white_inner) mName.setTextColor(resources.getColor(R.color.gray_B7B9B8)) } else if (it.currentPlayTime in 1500..2000) { mHeadOuter.setImageResource(R.drawable.ic_eye_black_outer) mHeadInner.setImageResource(R.drawable.ic_eye_black_inner) mName.setTextColor(resources.getColor(R.color.black_444444)) } } duration = 2000 }.start() } /** * 执行背景动画 */ private fun doBackgroundAnimator() { ValueAnimator.ofArgb(0, 0xffffffff.toInt()).apply { addUpdateListener { mLoadingContainer.setBackgroundColor(it.animatedValue as Int) } doOnEnd { doTextAnimator() } duration = 2000 }.start() } /** * 显示 today 文字与精选 */ private fun doTextAnimator() { ValueAnimator.ofArgb(0, 0xff444444.toInt()).apply { addUpdateListener { val color = it.animatedValue as Int setTextColor(mForToday, color) setTextColor(mDate, color) setTextColor(mTodayChose, color) mDate.text = getDateString(Date(), "- yyyy/MM/dd -") } doOnEnd { doInnerEyeAnimator() } }.start() } private fun setTextColor(textView: TextView, color: Int) { textView.visibility = View.VISIBLE textView.setTextColor(color) } /** * 执行内部眼睛动画 */ private fun doInnerEyeAnimator() { ObjectAnimator.ofFloat(mHeadInner, "rotation", 0f, 360f).apply { doOnEnd { readyGoThenKillSelf() UserPreferences.saveShowUserAnim(true) } duration = 1000 }.start() } /** * 执行背景缩放动画 */ private fun doScaleAnimator() { AnimatorSet().apply { val scaleX = ObjectAnimator.ofFloat(mIvBackground, "scaleX", 1f, 1.08f) val scaleY = ObjectAnimator.ofFloat(mIvBackground, "scaleY", 1f, 1.08f) playTogether(scaleX, scaleY) doOnEnd { readyGoThenKillSelf() UserPreferences.saveShowUserAnim(true) } duration = 2000 }.start() } override fun getContentViewLayoutId() = R.layout.fragment_local_coomon_landing } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/splash/SloganFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.splash import android.os.Bundle import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatFragment /** * Author: andy.xwt * Date: 2018/5/15 10:51 * Description: 口号fragment 用于切换视频口号 */ class SloganFragment : BaseAppCompatFragment() { companion object { @JvmStatic fun newInstance() = SloganFragment() } override fun initView(savedInstanceState: Bundle?) { } override fun getContentViewLayoutId() = R.layout.fragment_slogan } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/splash/VideoLandingFragment.kt ================================================ package com.jennifer.andy.simpleeyes.ui.splash import android.os.Bundle import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.viewpager.widget.ViewPager import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.UserPreferences import com.jennifer.andy.simpleeyes.player.render.IRenderView.Companion.AR_ASPECT_FIT_PARENT import com.jennifer.andy.simpleeyes.ui.MainActivity import com.jennifer.andy.simpleeyes.ui.base.BaseAppCompatFragment import com.jennifer.andy.simpleeyes.ui.splash.adapter.SplashVideoFragmentAdapter import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.readyGoThenKillSelf import com.jennifer.andy.simpleeyes.widget.FullScreenVideoView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTypeWriterTextView import com.jennifer.andy.simpleeyes.widget.viewpager.InterceptVerticalViewPager import com.rd.PageIndicatorView /** * Author: andy.xwt * Date: 2018/8/3 10:08 * Description:视频闪屏页 第一次用户安装应用的时候会走 */ class VideoLandingFragment : BaseAppCompatFragment() { private val mVideoView: FullScreenVideoView by bindView(R.id.video_view) private val mViewPager: InterceptVerticalViewPager by bindView(R.id.view_pager) private val mTvSloganChinese: CustomFontTypeWriterTextView by bindView(R.id.tv_slogan_zh) private val mTvSloganEnglish: CustomFontTypeWriterTextView by bindView(R.id.tv_slogan_en) private val mIndicator: PageIndicatorView by bindView(R.id.pageIndicatorView) private var mVideoPosition = 0 private var isHasPaused = false private lateinit var mFragmentList: MutableList companion object { @JvmStatic fun newInstance(): VideoLandingFragment = VideoLandingFragment() } override fun initView(savedInstanceState: Bundle?) { initSloganText() setVideoObserver() playVideo() } /** * 这里我采用的比较暴力的方法,主要是不想写事件拦截了,想写的小伙伴,可以自己去写 */ private fun initSloganText() { //设置初始标语 mTvSloganEnglish.printText(resources.getStringArray(R.array.slogan_array_en)[0]) mTvSloganChinese.printText(resources.getStringArray(R.array.slogan_array_zh)[0]) mIndicator.count = 4 mFragmentList = List(4) { SloganFragment.newInstance() } as MutableList with(mViewPager) { verticalListener = { goMainActivity() } horizontalListener = { goMainActivity() } mDisMissIndex = mFragmentList.size - 1 adapter = SplashVideoFragmentAdapter(mFragmentList, childFragmentManager) addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { mIndicator.setSelected(position) } override fun onPageScrollStateChanged(state: Int) { } override fun onPageSelected(position: Int) { if (position in 0..3) { mTvSloganEnglish.printText(resources.getStringArray(R.array.slogan_array_en)[position]) mTvSloganChinese.printText(resources.getStringArray(R.array.slogan_array_zh)[position]) } } }) } } /** * 设置用户不是第一次登录,并跳转到主界面 */ private fun goMainActivity() { UserPreferences.saveUserIsFirstLogin(false) readyGoThenKillSelf() } private fun playVideo() { val path = R.raw.landing with(mVideoView) { setAspectRatio(AR_ASPECT_FIT_PARENT) setVideoPath("android.resource://${activity?.packageName}/$path") setOnPreparedListener { requestFocus() seekTo(0) start() } setOnCompletionListener { it.isLooping = true start() } } } private fun setVideoObserver() { lifecycle.addObserver(object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onVideoResume() { if (isHasPaused) { mVideoView.seekTo(mVideoPosition) mVideoView.resume() isHasPaused = false } } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onVideoPause() { mVideoPosition = mVideoView.currentPosition mVideoView.pause() isHasPaused = true } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun onVideoStop() { mVideoView.stopPlayback() } }) } override fun getContentViewLayoutId() = R.layout.fragment_video_landing } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/splash/adapter/SplashVideoFragmentAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.splash.adapter import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter import com.jennifer.andy.simpleeyes.ui.splash.SloganFragment /** * Author: andy.xwt * Date: 2018/5/15 10:59 * Description: 闪屏视频适配器 */ class SplashVideoFragmentAdapter(var mFragmentList: MutableList, fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager) { override fun getItem(position: Int) = mFragmentList[position] override fun getCount() = mFragmentList.size } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/VideoDetailActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video import android.content.Context import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.widget.SeekBar import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.chad.library.adapter.base.BaseQuickAdapter import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.net.Extras import com.jennifer.andy.simpleeyes.player.IjkMediaController import com.jennifer.andy.simpleeyes.player.IjkVideoViewWrapper import com.jennifer.andy.simpleeyes.player.event.VideoProgressEvent import com.jennifer.andy.simpleeyes.player.render.IRenderView import com.jennifer.andy.simpleeyes.rx.RxBus import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.video.adapter.VideoDetailAdapter import com.jennifer.andy.simpleeyes.ui.video.presenter.VideoDetailPresenter import com.jennifer.andy.simpleeyes.ui.video.view.VideoDetailView import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.VideoDetailAuthorView import com.jennifer.andy.simpleeyes.widget.pull.head.VideoDetailHeadView import io.reactivex.functions.Consumer import tv.danmaku.ijk.media.player.IMediaPlayer import java.util.* /** * Author: andy.xwt * Date: 2017/12/18 16:51 * Description: 视频详细信息界面 */ class VideoDetailActivity : BaseActivity(), VideoDetailView { private val mBlurredImage by bindView(R.id.iv_blurred) private val ijkVideoViewWrapper by bindView(R.id.video_view_wrapper) private val mSeekBar by bindView(R.id.seek_bar) private val mRecycler by bindView(R.id.rv_video_recycler) private lateinit var mCurrentVideoInfo: ContentBean private var mCurrentIndex = 0 private lateinit var mVideoListInfo: MutableList private var mBackPressed = false private lateinit var ijkMediaController: IjkMediaController private var mChangeProgress: Int = 0 companion object { /** * 跳转到视频详细界面 */ @JvmStatic fun start(context: Context, content: ContentBean, videoListInfo: ArrayList, defaultIndex: Int = 0) { val bundle = Bundle() bundle.putSerializable(Extras.VIDEO_INFO, content) bundle.putSerializable(Extras.VIDEO_LIST_INFO, videoListInfo) bundle.putInt(Extras.VIDEO_INFO_INDEX, defaultIndex) val starter = Intent(context, VideoDetailActivity::class.java) starter.putExtras(bundle) context.startActivity(starter) } } override fun getBundleExtras(extras: Bundle) { mCurrentVideoInfo = extras.getSerializable(Extras.VIDEO_INFO) as ContentBean mVideoListInfo = extras.getSerializable(Extras.VIDEO_LIST_INFO) as ArrayList mCurrentIndex = extras.getInt(Extras.VIDEO_INFO_INDEX, 0) } override fun initView(savedInstanceState: Bundle?) { initPlaceHolder() initMediaController() initSeekBar() registerProgressEvent() playVideo() } private fun initPlaceHolder() { ijkVideoViewWrapper.setPlaceImageUrl(mCurrentVideoInfo.cover.detail) mBlurredImage.setImageURI(mCurrentVideoInfo.cover.blurred) } private fun initMediaController() { ijkMediaController = IjkMediaController(this) ijkMediaController.initData(mCurrentIndex, mVideoListInfo.size, mCurrentVideoInfo) ijkMediaController.controllerListener = object : IjkMediaController.ControllerListener { override fun onBackClick() { finish() } override fun onNextClick() { mCurrentIndex = ++mCurrentIndex ijkMediaController.currentIndex = mCurrentIndex refreshVideo(getFutureVideo()) } override fun onFullScreenClick() { ijkVideoViewWrapper.enterFullScreen() } override fun onTinyScreenClick() { ijkVideoViewWrapper.exitFullScreen() } override fun onPreClick() { mCurrentIndex = --mCurrentIndex ijkMediaController.currentIndex = mCurrentIndex refreshVideo(getFutureVideo()) } override fun onErrorViewClick() { ijkMediaController.hide() refreshVideo(getFutureVideo()) } override fun onShowController(isShowController: Boolean) { mSeekBar.thumb = if (isShowController) getDrawable(R.drawable.ic_player_progress_handle) else ColorDrawable(Color.TRANSPARENT) } } } private fun initSeekBar() { mSeekBar.thumb = ColorDrawable(Color.TRANSPARENT) mSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onStartTrackingTouch(bar: SeekBar) { //控制的时候停止更新进度条,同时禁止隐藏 ijkVideoViewWrapper.setDragging(true) ijkVideoViewWrapper.showControllerAllTheTime() } override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) { if (fromUser) { //更新当前播放时间 mChangeProgress = progress } } override fun onStopTrackingTouch(bar: SeekBar) { //定位都拖动位置 val newPosition = ijkVideoViewWrapper.getDuration() * mChangeProgress / 1000L ijkVideoViewWrapper.seekTo(newPosition.toInt()) ijkVideoViewWrapper.showController() ijkVideoViewWrapper.setDragging(false) } }) } /** * 重置视频信息 */ private fun refreshVideo(videoInfo: Content?) { videoInfo?.let { mCurrentVideoInfo = videoInfo.data mRecycler.visibility = View.INVISIBLE ijkVideoViewWrapper.togglePlaceImage(true) mSeekBar.secondaryProgress = 0 mSeekBar.progress = 0 initPlaceHolder() //重置视频播放信息 ijkVideoViewWrapper.setVideoPath(mCurrentVideoInfo.playUrl) ijkVideoViewWrapper.start() //获取关联视频信息 mPresenter.getRelatedVideoInfo(mCurrentVideoInfo.id) } } /** * 获取即将展示的视频 */ private fun getFutureVideo(): Content { return if (mVideoListInfo[mCurrentIndex].data.content != null) { mVideoListInfo[mCurrentIndex].data.content!! } else { mVideoListInfo[mCurrentIndex] } } private fun registerProgressEvent() { //注册进度条监听 RxBus.register(this, VideoProgressEvent::class.java, Consumer { //如果正在拖动的话,不更新进度条 if (!ijkVideoViewWrapper.isDragging()) { mSeekBar.progress = it.progress } mSeekBar.secondaryProgress = it.secondaryProgress }) } /** * 播放视频 */ private fun playVideo() { with(ijkVideoViewWrapper) { //设置视频准备完成监听 setOnPreparedListener(IMediaPlayer.OnPreparedListener { resetType() hidePlaceImage() }) //设置视频播放失败监听 setOnErrorListener(IMediaPlayer.OnErrorListener { _, _, _ -> showErrorView() true }) //设置视频播放完成监听 setOnCompletionListener(IMediaPlayer.OnCompletionListener { mSeekBar.thumb = null }) //设置视频地址,并开始播放 setVideoPath(mCurrentVideoInfo.playUrl) toggleAspectRatio(IRenderView.AR_MATCH_PARENT) setMediaController(ijkMediaController) start() } //获取相关视频信息 mPresenter.getRelatedVideoInfo(mCurrentVideoInfo.id) } /** * 添加标题 */ private fun getVideoDetailView(): View { return VideoDetailHeadView(this).apply { setTitle(mCurrentVideoInfo.title) setCategoryAndTime(mCurrentVideoInfo.category, mCurrentVideoInfo.duration) setFavoriteCount(mCurrentVideoInfo.consumption.collectionCount) setShareCount(mCurrentVideoInfo.consumption.replyCount) setReplayCount(mCurrentVideoInfo.consumption.replyCount) setDescription(mCurrentVideoInfo.description) startScrollAnimation() } } /** * 设置相关视频信息 */ private fun getRelationVideoInfo(): View { return VideoDetailAuthorView(this).apply { setVideoAuthorInfo(mCurrentVideoInfo.author) } } override fun getRelatedVideoInfoSuccess(itemList: MutableList) { with(mRecycler) { visibility = View.VISIBLE layoutManager = LinearLayoutManager(this@VideoDetailActivity) adapter = VideoDetailAdapter(itemList).apply { addHeaderView(getVideoDetailView()) addHeaderView(getRelationVideoInfo()) openLoadAnimation(BaseQuickAdapter.ALPHAIN) setFooterView(LayoutInflater.from(this@VideoDetailActivity).inflate(R.layout.item_the_end, null)) setOnItemClickListener { _, _, position -> if (getItemViewType(position) != BaseQuickAdapter.HEADER_VIEW) { refreshVideo(getItem(position)) } } } } } override fun getRelatedVideoFail() { } override fun onBackPressedSupport() { super.onBackPressedSupport() mBackPressed = true } override fun onResume() { super.onResume() //当前界面可见的时候重新注册进度条监听 registerProgressEvent() } override fun onPause() { super.onPause() if (ijkVideoViewWrapper.isPlaying()) { ijkVideoViewWrapper.pause() } } override fun onStop() { super.onStop() if (mBackPressed) { ijkVideoViewWrapper.stopPlayback() ijkVideoViewWrapper.release(true) } RxBus.unRegister(this) } override fun toggleOverridePendingTransition() = true override fun getOverridePendingTransition(): TransitionMode = TransitionMode.BOTTOM override fun getContentViewLayoutId() = R.layout.activity_video_detail } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/VideoInfoByIdActivity.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video import android.os.Bundle import com.alibaba.android.arouter.facade.Postcard import com.alibaba.android.arouter.facade.annotation.Autowired import com.alibaba.android.arouter.facade.annotation.Route import com.alibaba.android.arouter.facade.callback.NavCallback import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.net.Extras import com.jennifer.andy.simpleeyes.ui.base.BaseActivity import com.jennifer.andy.simpleeyes.ui.video.presenter.VideoInfoByIdPresenter import com.jennifer.andy.simpleeyes.ui.video.view.VideoInfoByIdView /** * Author: andy.xwt * Date: 2018/7/23 14:25 * Description: 根据视频id获取视频信息 */ @Route(path = "/AndyJennifer/detail") class VideoInfoByIdActivity : BaseActivity(), VideoInfoByIdView { @Autowired @JvmField var id: String? = null override fun initView(savedInstanceState: Bundle?) { ARouter.getInstance().inject(this) mPresenter.getVideoInfoById(id!!) } override fun getVideoInfoSuccess(contentBean: ContentBean) { val bundle = Bundle().apply { putSerializable(Extras.VIDEO_INFO, contentBean) putSerializable(Extras.VIDEO_LIST_INFO, arrayListOf()) } ARouter.getInstance() .build("/pgc/detail") .with(bundle) .navigation(this, object : NavCallback() { override fun onArrival(postcard: Postcard?) { finish() } }) } override fun getContentViewLayoutId() = R.layout.activity_videoinfo_by_id } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/adapter/VideoDetailAdapter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video.adapter import com.chad.library.adapter.base.BaseQuickAdapter import com.chad.library.adapter.base.BaseViewHolder import com.chad.library.adapter.base.util.MultiTypeDelegate import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.utils.getElapseTimeForShow /** * Author: andy.xwt * Date: 2018/2/11 14:28 * Description: */ class VideoDetailAdapter(data: MutableList) : BaseQuickAdapter(data) { companion object { const val TEXT_CARD = "textCard" const val VIDEO_SMALL_CARD = "videoSmallCard" const val TEXT_CARD_TYPE = 0 const val VIDEO_SMALL_CARD_TYPE = 1 } init { multiTypeDelegate = object : MultiTypeDelegate() { override fun getItemType(andyInfoItem: Content): Int { when (andyInfoItem.type) { TEXT_CARD -> return TEXT_CARD_TYPE VIDEO_SMALL_CARD -> return VIDEO_SMALL_CARD_TYPE } return -1 } } with(multiTypeDelegate) { registerItemType(TEXT_CARD_TYPE, R.layout.item_video_text_card) registerItemType(VIDEO_SMALL_CARD_TYPE, R.layout.item_video_samll_card) } } override fun convert(helper: BaseViewHolder?, item: Content) { when (helper?.itemViewType) { TEXT_CARD_TYPE -> setTextCardInfo(helper, item) VIDEO_SMALL_CARD_TYPE -> setVideoSmallCardInfo(helper, item) } } /** * 设置卡片信息 */ private fun setTextCardInfo(helper: BaseViewHolder, item: Content) { helper.setText(R.id.tv_text, item.data.text) } /** * 设置视频信息 */ private fun setVideoSmallCardInfo(helper: BaseViewHolder, item: Content) { val imageView = helper.getView(R.id.iv_image) imageView.setImageURI(item.data.cover.feed) helper.setText(R.id.tv_title, item.data.title) val description = "#${item.data.category} / ${getElapseTimeForShow(item.data.duration)}" helper.setText(R.id.tv_time, description) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/model/VideoDetailModel.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video.model import com.jennifer.andy.simpleeyes.entity.AndyInfo import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.net.Api import com.jennifer.andy.simpleeyes.rx.RxThreadHelper import com.jennifer.andy.simpleeyes.rx.error.globalHandleError import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel import io.reactivex.Observable /** * Author: andy.xwt * Date: 2018/1/5 15:29 * Description: */ class VideoDetailModel : BaseModel { /** * 获取相关视频信息 */ fun getRelatedVideoInfo(id: String): Observable = Api.getDefault() .getRelatedVideo(id) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) /** * 根据视频id获取视频信息 */ fun getVideoInfoById(id: String): Observable = Api.getDefault() .getVideoInfoById(id) .compose(globalHandleError()) .compose(RxThreadHelper.switchObservableThread()) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/presenter/VideoDetailPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video.presenter import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.video.model.VideoDetailModel import com.jennifer.andy.simpleeyes.ui.video.view.VideoDetailView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2017/12/18 16:55 * Description: */ class VideoDetailPresenter : BasePresenter() { private var mVideoModel: VideoDetailModel = VideoDetailModel() /** * 获取相关视频信息 */ fun getRelatedVideoInfo(id: String) { mVideoModel.getRelatedVideoInfo(id).autoDispose(mScopeProvider).subscribe({ mView?.getRelatedVideoInfoSuccess(it.itemList) }, { mView?.getRelatedVideoFail() }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/presenter/VideoInfoByIdPresenter.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video.presenter import android.view.View import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import com.jennifer.andy.simpleeyes.ui.video.model.VideoDetailModel import com.jennifer.andy.simpleeyes.ui.video.view.VideoInfoByIdView import com.uber.autodispose.autoDispose /** * Author: andy.xwt * Date: 2018/7/23 14:32 * Description: */ class VideoInfoByIdPresenter : BasePresenter() { private val mVideoModel = VideoDetailModel() fun getVideoInfoById(id: String) { mVideoModel.getVideoInfoById(id).autoDispose(mScopeProvider).subscribe({ mView?.getVideoInfoSuccess(it) }, { mView?.showNetError(View.OnClickListener { getVideoInfoById(id) }) }) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/view/VideoDetailView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video.view import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2017/12/18 16:56 * Description: */ interface VideoDetailView : BaseView { /** * 获取相关视频成功 */ fun getRelatedVideoInfoSuccess(itemList: MutableList) /** * 获取相关视屏失败 */ fun getRelatedVideoFail() } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/ui/video/view/VideoInfoByIdView.kt ================================================ package com.jennifer.andy.simpleeyes.ui.video.view import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.ui.base.BaseView /** * Author: andy.xwt * Date: 2018/7/23 14:31 * Description: */ interface VideoInfoByIdView : BaseView { fun getVideoInfoSuccess(contentBean: ContentBean) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/AppUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.content.Context /** * 获取App版本号 * * @return App版本号 */ fun Context.getAppVersionName(): String { val pm = packageManager val packageInfo = pm.getPackageInfo(packageName, 0) return packageInfo.versionName } /** * 获取App版本码 * * @return App版本码 */ fun Context.getAppVersionCode(): Int { val pm = packageManager val packageInfo = pm.getPackageInfo(packageName, 0) return packageInfo.versionCode } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/ButterKnife.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.app.Activity import android.app.Dialog import android.app.DialogFragment import android.app.Fragment import android.view.View import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty import androidx.fragment.app.DialogFragment as SupportDialogFragment import androidx.fragment.app.Fragment as SupportFragment public fun View.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun Activity.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun Dialog.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun DialogFragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun SupportDialogFragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun Fragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun SupportFragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun ViewHolder.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) public fun View.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun Activity.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun Dialog.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun DialogFragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun SupportDialogFragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun Fragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun SupportFragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun ViewHolder.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) public fun View.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun Activity.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun Dialog.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun DialogFragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun SupportDialogFragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun Fragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun SupportFragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun ViewHolder.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) public fun View.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) public fun Activity.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) public fun Dialog.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) public fun DialogFragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) public fun SupportDialogFragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) public fun Fragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) public fun SupportFragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) public fun ViewHolder.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) private val View.viewFinder: View.(Int) -> View? get() = { findViewById(it) } private val Activity.viewFinder: Activity.(Int) -> View? get() = { findViewById(it) } private val Dialog.viewFinder: Dialog.(Int) -> View? get() = { findViewById(it) } private val DialogFragment.viewFinder: DialogFragment.(Int) -> View? get() = { dialog?.findViewById(it) ?: view?.findViewById(it) } private val SupportDialogFragment.viewFinder: SupportDialogFragment.(Int) -> View? get() = { dialog?.findViewById(it) ?: view?.findViewById(it) } private val Fragment.viewFinder: Fragment.(Int) -> View? get() = { view.findViewById(it) } private val SupportFragment.viewFinder: SupportFragment.(Int) -> View? get() = { view!!.findViewById(it) } private val ViewHolder.viewFinder: ViewHolder.(Int) -> View? get() = { itemView.findViewById(it) } private fun viewNotFound(id:Int, desc: KProperty<*>): Nothing = throw IllegalStateException("View ID $id for '${desc.name}' not found.") @Suppress("UNCHECKED_CAST") private fun required(id: Int, finder: T.(Int) -> View?) = Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) } @Suppress("UNCHECKED_CAST") private fun optional(id: Int, finder: T.(Int) -> View?) = Lazy { t: T, desc -> t.finder(id) as V? } @Suppress("UNCHECKED_CAST") private fun required(ids: IntArray, finder: T.(Int) -> View?) = Lazy { t: T, desc -> ids.map { t.finder(it) as V? ?: viewNotFound(it, desc) } } @Suppress("UNCHECKED_CAST") private fun optional(ids: IntArray, finder: T.(Int) -> View?) = Lazy { t: T, desc -> ids.map { t.finder(it) as V? }.filterNotNull() } // Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it private class Lazy(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty { private object EMPTY private var value: Any? = EMPTY override fun getValue(thisRef: T, property: KProperty<*>): V { if (value == EMPTY) { value = initializer(thisRef, property) } @Suppress("UNCHECKED_CAST") return value as V } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/DensityUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.content.Context /** * 获取设备尺寸密度 */ fun Context.getDensity() = resources.displayMetrics.density /** * 获取设备收缩密度 */ fun Context.getScaleDensity() = resources.displayMetrics.scaledDensity /** * dp转px */ fun Context.dip2px(dipValue: Float) = (dipValue * getDensity() + 0.5f).toInt() /** * px转dp */ fun Context.px2dip(pxValue: Float) = ((pxValue / getDensity()) + 0.5f).toInt() /** * sp转px */ fun Context.sp2px(spValue: Float) = ((spValue * getScaleDensity()) + 0.5f).toInt() /** * px转sp */ fun Context.px2sp(pxValue: Float) = (pxValue / getScaleDensity() + 0.5f).toInt() ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/IntentRouterUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.app.Activity import androidx.fragment.app.Fragment import com.jennifer.andy.simpleeyes.utils.ext.intentFor /** * 跳转到相应的activity 并携带bundle数据 */ inline fun Activity.readyGo(extras: Map = emptyMap()) { val intent = intentFor(extras) startActivity(intent) } /** * 跳转到相应的activity,并携带bundle数据,接收返回码 */ inline fun Activity.readyGoForResult(extras: Map = emptyMap(), requestCode: Int) { val intent = intentFor(extras) startActivityForResult(intent, requestCode) } /** * 跳转到相应的activity并携带bundle数据,然后干掉当前Activity * */ inline fun Activity.readyGoThenKillSelf(extras: Map = emptyMap()) { val intent = intentFor(extras) startActivity(intent) finish() } /** * 跳转到相应的activity 并携带bundle数据 */ inline fun Fragment.readyGo(extras: Map = emptyMap()) { val intent = intentFor(extras) startActivity(intent) } /** * 跳转到相应的activity,并携带bundle数据,接收返回码 */ inline fun Fragment.readyGoForResult(extras: Map = emptyMap(), requestCode: Int) { val intent = intentFor(extras) startActivityForResult(intent, requestCode) } /** * 跳转到相应的activity并携带bundle数据,然后干掉当前Fragment所属Activity * */ inline fun Fragment.readyGoThenKillSelf(extras: Map = emptyMap()) { val intent = intentFor(extras) startActivity(intent) requireActivity().finish() } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/KeyboardUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.app.Activity import android.content.Context import android.view.inputmethod.InputMethodManager /** * 是否显示键盘 */ fun Activity.showKeyboard(isShow: Boolean) { val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager if (isShow) { if (currentFocus == null) { imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) } else { imm.showSoftInput(currentFocus, 0) } } else { if (currentFocus != null) { imm.hideSoftInputFromWindow(currentFocus!!.windowToken, 0) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/NetWorkUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.content.Context import android.net.ConnectivityManager /** * 判断当前网络时候连接 */ fun Context.isNetWorkConnected(): Boolean { val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkInfo = connectivityManager.activeNetworkInfo//返回当前网络的详细信息 return networkInfo.isAvailable } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/ProcessUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.app.ActivityManager import android.content.Context /** * 获取当前进程名称 */ fun Context.getProcessName(): String? { val pid = android.os.Process.myPid() val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager activityManager.runningAppProcesses.forEach { if (it.pid == pid) { return it.processName } } return null } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/ScreenUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.content.Context import android.util.DisplayMetrics import android.view.WindowManager /** * 获取屏幕的宽度 */ fun Context.getScreenWidth(): Int { val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager val dm = DisplayMetrics() wm.defaultDisplay.getMetrics(dm) return dm.widthPixels } /** * 获取屏幕的高度 */ fun Context.getScreenHeight(): Int { val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager val dm = DisplayMetrics() wm.defaultDisplay.getMetrics(dm) return dm.heightPixels } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/SystemUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import com.jennifer.andy.simpleeyes.ui.base.model.BaseModel import com.jennifer.andy.simpleeyes.ui.base.presenter.BasePresenter import java.lang.reflect.ParameterizedType /** * 获取当前类上的泛型参数,并实例化 * * @param any 当前类 * @param index 类泛型参数角标 * @param 泛型实例化对象 * @return 泛型实例化对象 */ fun getGenericInstance(any: Any, index: Int): T { try { val type = any.javaClass.genericSuperclass as ParameterizedType//获取当前类的父类泛型参数 val clazz = type.actualTypeArguments[index] as Class//获取泛型class val instance = clazz.newInstance() return if (instance is BasePresenter<*> || instance is BaseModel) { instance } else { throw IllegalStateException("if you use mvp user must support generic!!!") } } catch (e: Exception) { e.printStackTrace() throw IllegalStateException("translate fail!!") } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/TimeUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import java.text.SimpleDateFormat import java.util.* /** * 将秒数转换为 00 00' 00''格式 * @seconds 秒 */ fun getElapseTimeForShow(seconds: Int): String { val sb = StringBuilder() val hour = seconds / (60 * 60) if (hour != 0) { sb.append(hour).append("") } val minute = (seconds - 60 * 60 * hour) / 60 if (minute != 0 && minute > 9) { sb.append(minute).append("' ") } else { sb.append("0").append(minute).append("' ") } val second = seconds - 60 * 60 * hour - 60 * minute if (second != 0 && second > 9) { sb.append(second).append("\"") } else { sb.append("0").append(second).append("\"") } return sb.toString() } /** * 将日期转换为指定格式的日期字符串 * * @param date 日期 * @param format 格式化字符串 */ fun getDateString(date: Date, format: String): String { val formatter = SimpleDateFormat(format, Locale.getDefault()) return formatter.format(date) } /** * 判断是否是今天 * */ fun isCurrentDay(time: Long): Boolean { return isSameDay(Date(), Date(time)) } /** * 是否是同一天 */ fun isSameDay(date1: Date, date2: Date): Boolean { val cal1 = Calendar.getInstance() val cal2 = Calendar.getInstance() cal1.time = date1 cal2.time = date2 return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) } /** * 格式化时间 * 如果是当天就格式化时间为 HH:mm * 如果是一周前发布的就显示几天前 * 如果超过5天显示一周前 * @currentDate 当前日期 */ fun getTimeStr(currentDate: Date): String { val todayStart = Calendar.getInstance() todayStart.set(Calendar.HOUR_OF_DAY, 0) todayStart.set(Calendar.MINUTE, 0) todayStart.set(Calendar.SECOND, 0) todayStart.set(Calendar.MILLISECOND, 0) val numberBeforeCurrentDay = getNumberBeforeCurrentDay(todayStart.time, currentDate) return when { numberBeforeCurrentDay == 0 -> {//当天发布 val timeFormatter24 = SimpleDateFormat("HH:mm", Locale.getDefault()) timeFormatter24.format(currentDate) } numberBeforeCurrentDay <= 5 -> {//小于一周前 "$numberBeforeCurrentDay 天前" } else -> {//一周前 "1 周前" } } } private fun getNumberBeforeCurrentDay(todayBegin: Date, currentDate: Date): Int { var index = 7 while (index >= 1) { val date = Date(todayBegin.time - 3600 * 24 * 1000 * index) if (currentDate.before(date)) break else index-- } return index } /** * 格式化时间 */ fun stringForTime(timeMs: Int): String { val sb = StringBuilder() val totalSeconds = timeMs / 1000 val seconds = totalSeconds % 60 val minutes = totalSeconds / 60 % 60 val hours = totalSeconds / 3600 sb.setLength(0) val mFormatter = Formatter(sb, Locale.getDefault()) return if (hours > 0) { mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() } else { mFormatter.format("%02d:%02d", minutes, seconds).toString() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/ToastUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.content.Context import android.os.Handler import android.widget.Toast import com.jennifer.andy.simpleeyes.AndyApplication /** * Author: andy.xwt * Date: 2019-11-17 22:43 * Description: */ fun toast(value: () -> String) = AndyApplication.INSTANCE.toast(value) fun toast(value: String) = toast { value } inline fun Context.toast(crossinline value: () -> String, duration: Int = Toast.LENGTH_SHORT) { Handler(mainLooper).post { Toast.makeText(this, value(), duration).show() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/UDIDUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import java.util.* /** * 获取随机uuid */ fun getRandomUUID() = UUID.randomUUID().toString().replace("-", "") ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/VideoPlayerUtils.kt ================================================ package com.jennifer.andy.simpleeyes.utils import android.app.Activity import android.content.Context import android.content.ContextWrapper import android.view.Window import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ContextThemeWrapper /** * 隐藏ActionBar并使当前界面全屏 */ fun hideActionBar(context: Context?) { val supportActionBar = getAppCompatActivity(context)?.supportActionBar supportActionBar?.let { supportActionBar.setShowHideAnimationEnabled(false) supportActionBar.hide() } getActivity(context)?.window?.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) } /** * 显示ActionBar并退出全屏 */ fun showActionBar(context: Context?) { val supportActionBar = getAppCompatActivity(context)?.supportActionBar supportActionBar?.let { supportActionBar.setShowHideAnimationEnabled(false) supportActionBar.show() } getActivity(context)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) } /** * 获取AppCompatActivity */ private fun getAppCompatActivity(context: Context?): AppCompatActivity? { if (context == null) { return null } if (context is AppCompatActivity) { return context } else if (context is ContextThemeWrapper) { return getAppCompatActivity(context.baseContext) } return null } /** * 获取Activity */ fun getActivity(context: Context?): Activity? { if (context == null) { return null } if (context is Activity) { return context } else if (context is ContextWrapper) { return getActivity(context.baseContext) } return null } /** * 获取Window */ fun getWindow(context: Context?): Window? { return if (getAppCompatActivity(context) != null) { getAppCompatActivity(context)?.window } else { getActivity(context)?.window } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/utils/ext/IntentEx.kt ================================================ package com.jennifer.andy.simpleeyes.utils.ext import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.Fragment import java.io.Serializable /** * Author: andy.xwt * Date: 2019-12-08 16:52 * Description:根据传入的泛型创建Intent比填充相应的参数 */ inline fun Context.intentFor(params: Map): Intent { return createIntent(this, T::class.java, params) } inline fun Fragment.intentFor(params: Map): Intent { return createIntent(requireContext(), T::class.java, params) } fun createIntent(ctx: Context, clazz: Class, params: Map): Intent { val intent = Intent(ctx, clazz) if (params.isNotEmpty()) fillIntentArguments(intent, params) return intent } private fun fillIntentArguments(intent: Intent, params: Map) { params.keys.forEach { key -> when (val value = params[key]) { null -> intent.putExtra(key, null as Serializable?) is Int -> intent.putExtra(key, value) is Long -> intent.putExtra(key, value) is CharSequence -> intent.putExtra(key, value) is String -> intent.putExtra(key, value) is Float -> intent.putExtra(key, value) is Double -> intent.putExtra(key, value) is Char -> intent.putExtra(key, value) is Short -> intent.putExtra(key, value) is Boolean -> intent.putExtra(key, value) is Serializable -> intent.putExtra(key, value) is Bundle -> intent.putExtra(key, value) is Parcelable -> intent.putExtra(key, value) is Array<*> -> when { value.isArrayOf() -> intent.putExtra(key, value) value.isArrayOf() -> intent.putExtra(key, value) value.isArrayOf() -> intent.putExtra(key, value) else -> error("Intent extra $key has wrong type ${value.javaClass.name}") } is IntArray -> intent.putExtra(key, value) is LongArray -> intent.putExtra(key, value) is FloatArray -> intent.putExtra(key, value) is DoubleArray -> intent.putExtra(key, value) is CharArray -> intent.putExtra(key, value) is ShortArray -> intent.putExtra(key, value) is BooleanArray -> intent.putExtra(key, value) else -> throw error("Intent extra $key has wrong type ${value.javaClass.name}") } return@forEach } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/BottomBar.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.graphics.Color import android.os.Parcel import android.os.Parcelable import android.util.AttributeSet import android.view.ViewGroup import android.widget.LinearLayout /** * Author: andy.xwt * Date: 2017/9/27 22:00 * Description: 底部导航栏 */ class BottomBar : LinearLayout { private lateinit var mTabLayout: LinearLayout private lateinit var mTabParams: LayoutParams private var mCurrentPosition = 0//当前默认位置 private var mBottomItemLayouts = mutableListOf() private var mListener: TabSelectedListener? = null constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { orientation = HORIZONTAL mTabLayout = LinearLayout(context) mTabLayout.setBackgroundColor(Color.WHITE) mTabLayout.orientation = HORIZONTAL mTabParams = LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT) mTabParams.weight = 1f addView(mTabLayout, mTabParams) } fun addItem(bottomItem: BottomItem) { val bottomItemLayout = BottomItemLayout(context) bottomItemLayout.setTabPosition(mBottomItemLayouts.size) bottomItemLayout.setBottomItem(bottomItem) mTabLayout.addView(bottomItemLayout, mTabParams) mBottomItemLayouts.add(bottomItemLayout) bottomItemLayout.setOnClickListener { val position = bottomItemLayout.getTabPosition() if (position == mCurrentPosition) { mListener?.onTabReselected(position) } else { mListener?.onTabSelected(position, mCurrentPosition) bottomItemLayout.isSelected = true mListener?.onTabUnselected(mCurrentPosition) mBottomItemLayouts[mCurrentPosition].isSelected = false mCurrentPosition = position } } } fun setOnTabSelectedListener(TabSelectedListener: TabSelectedListener) { mListener = TabSelectedListener } /** * 选项卡选择监听 */ interface TabSelectedListener { /** * 当选项卡被选择 */ fun onTabSelected(position: Int, prePosition: Int) /** * 当前选项卡没有被选择 */ fun onTabUnselected(position: Int) /** * 当选项卡被重复选择 */ fun onTabReselected(position: Int) } /** * 初始化 */ fun initialise() { mBottomItemLayouts[mCurrentPosition].isSelected = true } internal class SavedState : BaseSavedState { var selectPosition: Int = 0 companion object { @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { override fun createFromParcel(`in`: Parcel): SavedState { return SavedState(`in`) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } } constructor(superState: Parcelable?) : super(superState) private constructor(`in`: Parcel) : super(`in`) { selectPosition = `in`.readInt() } override fun writeToParcel(out: Parcel, flags: Int) { super.writeToParcel(out, flags) out.writeValue(selectPosition) } override fun toString(): String { return ("CompoundButton.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " checked=" + selectPosition + "}") } } override fun onSaveInstanceState(): Parcelable? { return SavedState(super.onSaveInstanceState()).apply { selectPosition = mCurrentPosition } } override fun onRestoreInstanceState(state: Parcelable?) { val ss = state as SavedState super.onRestoreInstanceState(ss.superState) mCurrentPosition = ss.selectPosition initialise() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/BottomItem.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.graphics.drawable.Drawable /** * Author: andy.xwt * Date: 2017/10/7 22:52 * Description: */ class BottomItem{ var mSelectDrawable: Drawable? = null var mSelectResource = -1 var mTitleResource = -1 var mTitle: String? = null var mUnSelectedDrawable: Drawable? = null var mUnSelectedResource = -1 constructor(selectDrawable: Drawable, title: String) { this.mSelectDrawable = selectDrawable this.mTitle = title } constructor(selectResource: Int, title: String) { this.mSelectResource = selectResource this.mTitle = title } constructor(mSelectDrawable: Drawable, mTitleResource: Int) { this.mSelectDrawable = mSelectDrawable this.mTitleResource = mTitleResource } constructor(mSelectResource: Int, mTitleResource: Int) { this.mSelectResource = mSelectResource this.mTitleResource = mTitleResource } fun setUnSelectedDrawable(unSelectedDrawable: Drawable) { this.mUnSelectedDrawable = unSelectedDrawable } fun setUnSelectedDrawable(unSelectedResource: Int) { this.mUnSelectedResource = unSelectedResource } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/BottomItemLayout.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.bindView /** * Author: andy.xwt * Date: 2017/9/27 22:01 * Description: */ class BottomItemLayout : LinearLayout { private var mSelectedTextColor = resources.getColor(R.color.colorPrimaryDark) private var mUnselectedTextColor = resources.getColor(R.color.SecondaryText) private val mIcon by bindView(R.id.iv_image) private val mIconTitle by bindView(R.id.tv_title) private var mSelectedDrawable: Drawable? = null private var mUnSelectedDrawable: Drawable? = null private var mTitle: String? = null private var mTabPosition = -1 constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.layout_bottom_item, this, true) gravity = Gravity.CENTER } override fun setSelected(selected: Boolean) { super.setSelected(selected) if (selected) { if (mSelectedDrawable != null) { mIcon.setImageDrawable(mSelectedDrawable) mIconTitle.setTextColor(mSelectedTextColor) } } else { if (mUnSelectedDrawable != null) { mIcon.setImageDrawable(mUnSelectedDrawable) mIconTitle.setTextColor(mUnselectedTextColor) } } } fun setTabPosition(position: Int) { mTabPosition = position if (position == 0) { isSelected = true } } fun getTabPosition() = mTabPosition fun setBottomItem(bottomItem: BottomItem) { //设置选中图片 if (bottomItem.mSelectResource > -1) { mSelectedDrawable = resources.getDrawable(bottomItem.mSelectResource) } else if (bottomItem.mSelectDrawable != null) { mSelectedDrawable = bottomItem.mSelectDrawable!! } //设置未被选中团片 if (bottomItem.mUnSelectedResource > -1) { mUnSelectedDrawable = resources.getDrawable(bottomItem.mUnSelectedResource) } else if (bottomItem.mUnSelectedDrawable != null) { mUnSelectedDrawable = bottomItem.mUnSelectedDrawable!! } //设置标题 if (bottomItem.mTitleResource > -1) { mTitle = resources.getString(bottomItem.mTitleResource) } else if (bottomItem.mTitle != null) { mTitle = bottomItem.mTitle!! } //初始化 mIcon.setImageDrawable(mUnSelectedDrawable) mIconTitle.text = mTitle } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/CollectionOfHorizontalScrollCardView.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.utils.getElapseTimeForShow import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView /** * Author: andy.xwt * Date: 2018/7/9 15:31 * Description: */ class CollectionOfHorizontalScrollCardView : FrameLayout { private val mEliteImageView by bindView(R.id.scroll_elite_view) private val mTvTitle by bindView(R.id.scroll_tv_title) private val mTvDesc by bindView(R.id.scroll_tv_desc) constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.layout_collection_of_horizontal_scroll_card, this, true) } fun setData(content: Content) { setImageUrl(content.data.cover.feed) val description = "#${content.data.category} / ${getElapseTimeForShow(content.data.duration)}" setTitleAndDesc(content.data.title, description) setDailyVisible(content.data.library == "DAILY") } /** * 设置图片显示 * @param url 图片地址 */ private fun setImageUrl(url: String) { mEliteImageView.setImageUrl(url) } /** * 是指标题与描述 */ private fun setTitleAndDesc(title: String, desc: String) { mTvTitle.text = title mTvDesc.text = desc } /** * 设置精选是否可见 */ private fun setDailyVisible(visible: Boolean) { mEliteImageView.setDailyVisible(visible) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/CustomLoadMoreView.kt ================================================ package com.jennifer.andy.simpleeyes.widget import com.chad.library.adapter.base.loadmore.LoadMoreView import com.jennifer.andy.simpleeyes.R /** * Author: andy.xwt * Date: 2017/12/18 15:03 * Description:自定义加载更多 */ class CustomLoadMoreView : LoadMoreView() { override fun getLayoutId() = R.layout.layout_load_more_view override fun getLoadingViewId() = R.id.ll_load_more_loading_view override fun getLoadEndViewId() = R.id.rl_load_end_view override fun getLoadFailViewId() = R.id.fl_load_more_load_fail_view } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/EliteImageView.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.text.TextUtils import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.ImageView import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView /** * Author: andy.xwt * Date: 2017/11/3 17:44 * Description:精选imageView 包含翻译图片 翻译 */ class EliteImageView : FrameLayout { private val ivImageView by bindView(R.id.iv_elite_image) private val tvTranslate by bindView(R.id.tv_translate) private val ivArrow by bindView(R.id.iv_arrow) private val ivDaily by bindView(R.id.iv_daily) constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.layout_choiceness, this, true) } /** * 设置箭头是否可见 * * @param visible 是否可见 */ fun setArrowVisible(visible: Boolean) { ivArrow.visibility = if (visible) View.VISIBLE else View.GONE } /** * 设置精选是否可见 */ fun setDailyVisible(visible: Boolean) { ivDaily.visibility = if (visible) View.VISIBLE else View.GONE } /** * 设置翻译文字 */ fun setTranslateText(text: String) { if (!TextUtils.isEmpty(text)) { tvTranslate.visibility = View.VISIBLE tvTranslate.text = text } else { tvTranslate.visibility = View.GONE } } /** * 设置图片显示 * @param url 图片地址 */ fun setImageUrl(url: String) { ivImageView.setImageURI(url) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/FullScreenVideoView.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.util.AttributeSet import android.widget.VideoView import com.jennifer.andy.simpleeyes.player.MeasureHelper /** * Author: andy.xwt * Date: 2019-08-27 10:43 * Description:全屏VideoView */ class FullScreenVideoView : VideoView { private var mMeasureHelper: MeasureHelper = MeasureHelper(this) constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { mMeasureHelper.doMeasure(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension(mMeasureHelper.measuredWidth, mMeasureHelper.measuredHeight) } /** * 设置视频比例 */ fun setAspectRatio(aspectRatio: Int) { mMeasureHelper.setAspectRatio(aspectRatio) requestLayout() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/GridItemDecoration.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView /** * Author: andy.xwt * Date: 2018/4/8 11:15 * Description: */ class GridItemDecoration constructor( val spanCount: Int, val spacing: Int, val isIncludeEdge: Boolean) : RecyclerView.ItemDecoration() { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val position = parent.getChildAdapterPosition(view) // 获取当前view的位置 val column = position % spanCount // 判断当前view在第几列 if (isIncludeEdge) {//判断是否包括左右两边 // spacing - column * ((1f / spanCount) * spacing) outRect.left = spacing - column * spacing / spanCount // (column + 1) * ((1f / spanCount) * spacing) outRect.right = (column + 1) * spacing / spanCount if (position < spanCount) { // top edge outRect.top = spacing } outRect.bottom = spacing // item bottom } else { // column * ((1f / spanCount) * spacing) outRect.left = column * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing) outRect.right = spacing - (column + 1) * spacing / spanCount if (position >= spanCount) { outRect.top = spacing // item top } } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/ItemHeaderView.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.net.Uri import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import com.alibaba.android.arouter.launcher.ARouter import com.facebook.drawee.generic.RoundingParams import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.ContentBean import com.jennifer.andy.simpleeyes.entity.Header import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.getTimeStr import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.font.FontType import java.util.* /** * Author: andy.xwt * Date: 2019-07-14 15:57 * Description:列表中item对应的header */ class ItemHeaderView : FrameLayout { private val tvTitle by bindView(R.id.tv_title) private val tvSubTitle by bindView(R.id.tv_sub_title) private val tvDesc by bindView(R.id.tv_desc) private val tvFocus by bindView(R.id.tv_focus) private val ivMore by bindView(R.id.iv_more) private val imageView by bindView(R.id.iv_source) private val mTvDate by bindView(R.id.tv_date) constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.layout_common_text, this, true) } fun setHeader(header: Header, type: String?) { with(header) { //设置标题 title?.let { //分类就居中 if (type == "videoCollectionOfHorizontalScrollCard") { tvTitle.gravity = Gravity.CENTER imageView.visibility = View.GONE ivMore.visibility = View.VISIBLE } else { tvTitle.gravity = getGravity(header.textAlign) } tvTitle.visibility = View.VISIBLE tvTitle.text = it tvTitle.setFontType(font)//默认标题加粗 } //副标题 subTitle?.let { it -> //分类就居中 if (type == "videoCollectionOfHorizontalScrollCard") { tvSubTitle.gravity = Gravity.CENTER imageView.visibility = View.GONE } else { tvSubTitle.gravity = getGravity(header.textAlign) } tvSubTitle.visibility = View.VISIBLE tvSubTitle.text = it tvSubTitle.setFontType(subTitleFont) } //设置图片 icon?.let { with(imageView) { imageView.visibility = View.VISIBLE //判断图片类型 when (iconType) { //圆形图片 "round" -> imageView.hierarchy.roundingParams = RoundingParams.asCircle() ////正方形音乐带播放按钮类型 "squareWithPlayButton" -> hierarchy.setOverlayImage(context.getDrawable(R.drawable.icon_cover_play_button)) else -> hierarchy.roundingParams?.roundAsCircle = false } setImageURI(icon) } } //设置发布时间 time?.let { mTvDate.visibility = View.VISIBLE mTvDate.text = getTimeStr(Date(time!!)) } //点击关注 follow?.let { tvFocus.visibility = View.VISIBLE } //设置描述 description?.let { tvDesc.visibility = View.VISIBLE tvDesc.text = description } //跳转到指定界面 actionUrl?.let { setOnClickListener { ARouter.getInstance().build(Uri.parse(header.actionUrl)).navigation() } } } } fun setAuthorHeader(content: ContentBean) { with(content) { //设置标题 title?.let { tvTitle.visibility = View.VISIBLE tvTitle.text = it tvTitle.setFontType(FontType.BOLD)//默认标题加粗 } //设置图片 icon?.let { with(imageView) { imageView.visibility = View.VISIBLE //判断图片类型 when (iconType) { //圆形图片 "round" -> imageView.hierarchy.roundingParams = RoundingParams.asCircle() ////正方形音乐带播放按钮类型 "squareWithPlayButton" -> hierarchy.setOverlayImage(context.getDrawable(R.drawable.icon_cover_play_button)) else -> hierarchy.roundingParams?.roundAsCircle = false } setImageURI(icon) } } //点击关注 follow?.let { tvFocus.visibility = View.VISIBLE } //设置描述 description?.let { tvDesc.visibility = View.VISIBLE tvDesc.text = description } //跳转到指定界面 actionUrl?.let { setOnClickListener { ARouter.getInstance().build(Uri.parse(content.actionUrl)).navigation() } } } } private fun getGravity(textAlign: String?): Int { if (textAlign !== null) { when (textAlign) { "right" -> return Gravity.END "left" -> return Gravity.START "middle" -> return Gravity.CENTER } } return Gravity.START } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/SearchHotRemindView.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.TextView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.bindView /** * Author: andy.xwt * Date: 2018/4/9 14:40 * Description:热门搜索词提示界面 */ class SearchHotRemindView : FrameLayout { private val mTitle: TextView by bindView(R.id.tv_title) private val mResult: TextView by bindView(R.id.tv_result) constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { LayoutInflater.from(context).inflate(R.layout.layout_search_hot_remind_view, this) } /** * 设置搜索结果 * @param queryWord 搜索内容 * @param count 搜索个数 */ fun setSearchResult(queryWord: CharSequence, count: Int) { mTitle.visibility = View.GONE if (count > 0) { mResult.text = "- [ $queryWord ] 搜索结果共${count}个 -" mResult.visibility = View.VISIBLE } else { mResult.visibility = View.GONE } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/StickyNavLayout.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.util.AttributeSet import android.util.Log import android.view.View import android.widget.LinearLayout import androidx.core.view.NestedScrollingParent2 import androidx.core.view.NestedScrollingParentHelper import androidx.viewpager.widget.ViewPager import com.jennifer.andy.simpleeyes.R import kotlinx.android.synthetic.main.layout_blank_card.view.* /** * Author: andy.xwt * Date: 2019-06-24 12:16 * Description: 粘性头部+导航栏+viewPager父控件 */ class StickyNavLayout : LinearLayout, NestedScrollingParent2 { private var mScrollingParentHelper: NestedScrollingParentHelper = NestedScrollingParentHelper(this) private lateinit var mTopView: View//头部view private lateinit var mNavView: View//导航view private lateinit var mViewPager: View//viewpager private var mCanScrollDistance: Float = 0f private var mListener: ScrollChangeListener? = null constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { orientation = VERTICAL } /** * 有嵌套滑动到来了,判断父view是否接受嵌套滑动 * * @param child 嵌套滑动对应的父类的子类(因为嵌套滑动对于的父View不一定是一级就能找到的,可能挑了两级父View的父View,child的辈分>=target) * @param target 具体嵌套滑动的那个子类 * @param axes 支持嵌套滚动轴。水平方向,垂直方向,或者不指定 * @param type 滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动 */ override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int) = true /** * 当父view接受嵌套滑动,当onStartNestedScroll方法返回true该方法会调用 * @param type 滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动 */ override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) { mScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type) } /** * 在嵌套滑动的子View未滑动之前,判断父view是否优先与子view处理(也就是父view可以先消耗,然后给子view消耗) * * @param target 具体嵌套滑动的那个子类 * @param dx 水平方向嵌套滑动的子View想要变化的距离 * @param dy 垂直方向嵌套滑动的子View想要变化的距离 dy<0向下滑动 dy>0 向上滑动 * @param consumed 这个参数要我们在实现这个函数的时候指定,回头告诉子View当前父View消耗的距离 * consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离 好让子view做出相应的调整 * @param type 滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动 */ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { //这里不管是手势滑动还是fling都需要父控件处理,因为子控件可能会出现手势滑动->手势滑动->fling, //如果这里只处理手势滚动,那么就会出现,父控件滚动一定距离,子控件再fling的问题。 //如果子view欲向上滑动,则先交给父view滑动 val hideTop = dy > 0 && scrollY < mCanScrollDistance //如果子view预向下滑动,必须要子view不能向下滑动后,才能交给父view滑动 val showTop = dy < 0 && scrollY >= 0 && !target.canScrollVertically(-1) if (hideTop || showTop) { scrollBy(0, dy) consumed[1] = dy } } /** * 嵌套滑动的子View在滑动之后,判断父view是否继续处理(也就是父消耗一定距离后,子再消耗,最后判断父消耗不) * * @param target 具体嵌套滑动的那个子类 * @param dxConsumed 水平方向嵌套滑动的子View滑动的距离(消耗的距离) * @param dyConsumed 垂直方向嵌套滑动的子View滑动的距离(消耗的距离) * @param dxUnconsumed 水平方向嵌套滑动的子View未滑动的距离(未消耗的距离) * @param dyUnconsumed 垂直方向嵌套滑动的子View未滑动的距离(未消耗的距离) * @param type 滑动类型,ViewCompat.TYPE_NON_TOUCH fling 效果ViewCompat.TYPE_TOUCH 手势滑动 */ override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) { } /** * 嵌套滑动时,如果父View处理了fling,那子view就没有办法处理fling了,所以这里要返回为false */ override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float) = false override fun onFinishInflate() { super.onFinishInflate() mTopView = findViewById(R.id.id_sticky_nav_layout_top_view) mNavView = findViewById(R.id.id_sticky_nav_layout_nav_view) mViewPager = findViewById(R.id.id_sticky_nav_layout_viewpager) if (mViewPager !is ViewPager) { throw RuntimeException("id_sticky_nav_layout_viewpager should be viewpager!") } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) //ViewPager修改后的高度= 总高度-导航栏高度 val layoutParams = mViewPager.layoutParams layoutParams.height = measuredHeight - mNavView.measuredHeight mViewPager.layoutParams = layoutParams super.onMeasure(widthMeasureSpec, heightMeasureSpec) } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) //可滑动高度为,topView的高度- 标题栏的高度 mCanScrollDistance = (mTopView.measuredHeight - resources.getDimension(R.dimen.normal_title_height).toInt()).toFloat() } override fun scrollTo(x: Int, y: Int) { //控制父view滑动范围为0-mCanScrollDistance.之间 var adjustY = y if (y < 0) adjustY = 0 if (y > mCanScrollDistance) adjustY = mCanScrollDistance.toInt() //将移动比例传出去 Log.d("wtf", "adJustY--> $adjustY $mCanScrollDistance --->${adjustY / mCanScrollDistance}") mListener?.onScroll(adjustY / mCanScrollDistance) if (adjustY != scrollY) super.scrollTo(x, adjustY) } override fun onStopNestedScroll(target: View, type: Int) { mScrollingParentHelper.onStopNestedScroll(view, type) } override fun getNestedScrollAxes(): Int { return mScrollingParentHelper.nestedScrollAxes } interface ScrollChangeListener { /** * 移动监听 * @param moveRatio 移动比例 */ fun onScroll(moveRatio: Float) } fun setScrollChangeListener(scrollChangeListener: ScrollChangeListener) { mListener = scrollChangeListener } override fun onDetachedFromWindow() { super.onDetachedFromWindow() mListener = null } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/VerticalProgressBar.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.graphics.Canvas import android.util.AttributeSet import android.widget.ProgressBar /** * Author: andy.xwt * Date: 2018/2/27 18:14 * Description:竖直progressBar */ class VerticalProgressBar : ProgressBar { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) override fun onDraw(canvas: Canvas) { canvas.rotate(-90f)//反转90度,将水平ProgressBar竖起来 canvas.translate(-height.toFloat(), 0f)//将经过旋转后得到的VerticalProgressBar移到正确的位置,注意经旋转后宽高值互换 super.onDraw(canvas) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension(heightMeasureSpec, widthMeasureSpec)//互换宽高值 } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(h, w, oldw, oldh)//互换宽高值 } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/VideoDetailAuthorView.kt ================================================ package com.jennifer.andy.simpleeyes.widget import android.content.Context import android.os.Bundle import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import com.alibaba.android.arouter.launcher.ARouter import com.facebook.drawee.view.SimpleDraweeView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.AuthorBean import com.jennifer.andy.simpleeyes.net.Extras import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView /** * Author: andy.xwt * Date: 2018/2/12 15:19 * Description: */ class VideoDetailAuthorView : FrameLayout, View.OnClickListener { private val mIvImage by bindView(R.id.iv_image) private val mTvTitle by bindView(R.id.tv_title) private val mDescription by bindView(R.id.tv_desc) private val mAddFollow by bindView(R.id.tv_follow) private lateinit var mTitle: String private lateinit var mId: String constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.layout_video_author_head, this, true) setOnClickListener(this) } override fun onClick(v: View?) { val bundle = Bundle().apply { putString(Extras.TAB_INDEX, "0") putString(Extras.TITLE, mTitle) putString(Extras.ID, mId) } ARouter.getInstance() .build("/pgc/detail") .with(bundle) .navigation() } /** * 设置视频作者信息 */ fun setVideoAuthorInfo(author: AuthorBean) { mIvImage.setImageURI(author.icon) mTvTitle.text = author.name mDescription.text = author.description mTitle = author.name mId = author.id } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/font/CustomFontTextView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.font import android.content.Context import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView /** * Author: andy.xwt * Date: 2017/10/30 16:45 * Description:自定义字体textView */ open class CustomFontTextView : AppCompatTextView { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { if (!isInEditMode) { TypefaceManager.setTextTypeFace(context, attrs, this) } } /** * 根据字体类型设置字体 * * @param fontType */ fun setFontType(fontType: FontType) { if (!isInEditMode) { TypefaceManager.setTextTypeFace(this, fontType) } } /** 根据字体名称设置,如果没有找到相应的字体,则使用默认的字体 * @param fontName 字体名称 * @param defaultFontType 默认字体 */ fun setFontType(fontName: String?, defaultFontType: FontType = FontType.BOLD) { if (!isInEditMode) { val fontType = TypefaceManager.getFontTypeByName(fontName) TypefaceManager.setTextTypeFace(this, fontType ?: defaultFontType) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/font/CustomFontTypeWriterTextView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.font import android.content.Context import android.text.TextUtils import android.util.AttributeSet /** * Author: andy.xwt * Date: 2017/12/12 11:10 * Description:打印文字TextView */ class CustomFontTypeWriterTextView : CustomFontTextView { private lateinit var mSloganSpanGroup: PrintSpanGroup constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) /** * 打印文字 */ fun printText(text: String?, printTime: Long = 0) { if (!TextUtils.isEmpty(text)) { mSloganSpanGroup = PrintSpanGroup(text!!, printTime) mSloganSpanGroup.startPrint(this) } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/font/FontType.kt ================================================ package com.jennifer.andy.simpleeyes.widget.font /** * Author: andy.xwt * Date: 2017/11/2 14:41 * Description:字体常量 包括 方正兰亭细黑简体,方正兰亭中粗黑简体,拉丁,龙虾字体 */ enum class FontType(var index: Int, var fontName: String, val path: String) { NORMAL(0, "Normal", "fonts/FZLanTingHeiS-L-GB-Regular.TTF"),//方正兰亭细黑简体 BOLD(1, "Bold", "fonts/FZLanTingHeiS-DB1-GB-Regular.TTF"),//方正兰亭中粗黑简体 FUTURE(2, "Future", "fonts/Futura-CondensedMedium.ttf"),//拉丁 LOBSTER(3, "Lobster", "fonts/Lobster-1.4.otf");//龙虾字体 } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/font/PrintSpan.kt ================================================ package com.jennifer.andy.simpleeyes.widget.font import android.text.TextPaint import android.text.style.MetricAffectingSpan /** * Author: andy.xwt * Date: 2017/11/27 15:17 * Description: */ class PrintSpan(var printAlpha: Int) : MetricAffectingSpan() { override fun updateMeasureState(tp: TextPaint) { tp.alpha = printAlpha } override fun updateDrawState(tp: TextPaint) { tp.alpha = printAlpha } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/font/PrintSpanGroup.kt ================================================ package com.jennifer.andy.simpleeyes.widget.font import android.animation.ObjectAnimator import android.graphics.Color import android.text.SpannableString import android.text.Spanned import android.util.Property import android.widget.TextView /** * Author: andy.xwt * Date: 2017/11/27 15:29 * Description: */ class PrintSpanGroup constructor(printText: CharSequence, var printTime: Long = 0) { private var mSpans: MutableList = mutableListOf() private var spannableString: SpannableString = SpannableString(printText) private var mAlpha: Float = 255f companion object { const val DEFAULT_PRINT_TIME = 500L } private val TYPE_WRITER_GROUP_ALPHA_PROPERTY = object : Property(Float::class.java, "type_writer_group_alpha_property") { override fun set(printGroup: PrintSpanGroup, alpha: Float) { setAlpha(alpha) } override fun get(printGroup: PrintSpanGroup): Float? { return mAlpha } } init { buildPrintSpanGroup(0, printText.length - 1) printTime = if (printTime > 0) printTime else DEFAULT_PRINT_TIME } /** * 将text拆分成单个span */ private fun buildPrintSpanGroup(start: Int, end: Int) { for (i in start..end) { val printSpan = PrintSpan(Color.TRANSPARENT) spannableString.setSpan(printSpan, i, i + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) mSpans.add(printSpan) } } /** * 设置单个span的颜色显示 */ private fun setAlpha(alpha: Float) { val size = mSpans.size var total = size * 1f * alpha//计算范围,哪个范围显示,哪个范围不显示 for (i in 0 until size) { val printSpan = mSpans[i] if (i <= total) { printSpan.printAlpha = 255 } else { printSpan.printAlpha = 0 } } } /** * 执行打印请求 */ fun startPrint(textView: TextView) { val objectAnimator = ObjectAnimator.ofFloat(this, TYPE_WRITER_GROUP_ALPHA_PROPERTY, 0f, 1f) objectAnimator.duration = printTime objectAnimator.start() objectAnimator.addUpdateListener { textView.text = spannableString } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/font/TypefaceManager.kt ================================================ package com.jennifer.andy.simpleeyes.widget.font import android.content.Context import android.graphics.Typeface import android.util.AttributeSet import android.widget.TextView import com.jennifer.andy.simpleeyes.AndyApplication import com.jennifer.andy.simpleeyes.R /** * Author: andy.xwt * Date: 2017/10/30 17:19 * Description:字体管理工具类 */ object TypefaceManager { private val mTypeFaceMap: MutableMap = mutableMapOf() private var mTypeFaceIndex: Int = FontType.NORMAL.index /** * 设置textView字体,如果参数中有字体,就采用本身的,如果没有就根据设置的值设置字体 * *@param context 上下文 *@param attributes 参数 *@param textView textView */ fun setTextTypeFace(context: Context, attributes: AttributeSet?, textView: TextView) { if (textView.typeface != null && textView.typeface.style != 0) { return } val typeArray = context.obtainStyledAttributes(attributes, R.styleable.CustomFontTextView) mTypeFaceIndex = typeArray.getInteger(R.styleable.CustomFontTextView_font_name, mTypeFaceIndex) if (mTypeFaceIndex in 0..FontType.values().size) { textView.typeface = getTypeFace(FontType.values()[mTypeFaceIndex]) } typeArray.recycle() } /** * 根据字体设置textVie显示的字体 */ fun setTextTypeFace(textView: TextView, fontType: FontType?) { val localTypeFace = getTypeFace(fontType) textView.typeface = localTypeFace } /** * 根据名称获取字体类型 * @return 字体类型 */ fun getFontTypeByName(fontName: String?): FontType? { return FontType.values().firstOrNull { it.fontName == fontName } } /** * 根据字体类型获取字体 * @param fontType 字体类型 */ fun getTypeFace(fontType: FontType?): Typeface? { return fontType?.let { var typeFace = mTypeFaceMap[fontType] if (typeFace == null) { typeFace = Typeface.createFromAsset(AndyApplication.INSTANCE.assets, fontType.path) mTypeFaceMap[fontType] = typeFace } return typeFace } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/image/CenterAlignImageSpan.kt ================================================ package com.jennifer.andy.simpleeyes.widget.image import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint import android.graphics.drawable.Drawable import android.text.style.ImageSpan /** * Author: andy.xwt * Date: 2018/4/4 15:56 * Description:文字图片居中显示 */ class CenterAlignImageSpan : ImageSpan { constructor(drawable: Drawable) : super(drawable) constructor(bitmap: Bitmap) : super(bitmap) override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { val drawable = drawable val fm = paint.fontMetrics val transY = (y + fm.descent + y + fm.ascent) / 2 - drawable.bounds.bottom / 2//计算y方向的位移 canvas.save() canvas.translate(x, transY)//绘制图片位移一段距离 drawable.draw(canvas) canvas.restore() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/image/imageloader/FrescoImageLoader.kt ================================================ package com.jennifer.andy.simpleeyes.widget.image.imageloader import android.content.Context import android.net.Uri import android.widget.ImageView import com.facebook.drawee.view.SimpleDraweeView import com.youth.banner.loader.ImageLoader /** * Author: andy.xwt * Date: 2017/11/3 11:58 * Description:banner中的图片加载器 */ class FrescoImageLoader : ImageLoader() { override fun displayImage(context: Context?, path: Any?, imageView: ImageView?) { imageView?.setImageURI(Uri.parse(path as String)) } override fun createImageView(context: Context?): ImageView { return SimpleDraweeView(context) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/head/EliteHeaderView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.head import android.animation.ArgbEvaluator import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.ImageView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.pull.refresh.PullRefreshView import kotlin.math.abs /** * Author: andy.xwt * Date: 2018/6/25 13:16 * Description: 每日精选刷新头布局,包括内部眼睛的旋转,和文字的变色 */ class EliteHeaderView : PullRefreshView { private val mHeadInner: ImageView by bindView(R.id.iv_head_inner) private val mHeadOuter: ImageView by bindView(R.id.iv_head_outer) private val mLoadingMessage: CustomFontTextView by bindView(R.id.tv_loading_msg) private var mRotationAnimator: ValueAnimator? = null private var mYDistance = 0f companion object { private const val ROTATION_DAMP = 2f//阻尼系数 } constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.refresh_daily_elite_header, this, true) } /** * 处理额外的下拉 dy */ override fun handleExtraPullEvent(dy: Float) { //处理眼睛旋转 mHeadInner.rotation = mHeadInner.rotation + (dy.toInt() / ROTATION_DAMP) } /** * 处理有效的下拉 dy */ override fun handleValidPullEvent(dy: Float) { //处理文字透明度 mYDistance += dy if (mYDistance >= height - mLoadingMessage.bottom && mYDistance <= height - mLoadingMessage.top) { val argbEvaluator = ArgbEvaluator() val fraction = abs(mLoadingMessage.bottom - height + mYDistance) / (mLoadingMessage.height) val textColor = argbEvaluator.evaluate(fraction, 0, 0xff444444.toInt()) as Int mLoadingMessage.setTextColor(textColor) } } override fun doRefresh() { doInnerEyeAnimator() } override fun getDoRefreshHeight() = mHeadOuter.height /** * 执行内部眼睛动画,执行之前,先停止之前的 */ private fun doInnerEyeAnimator() { reset() mRotationAnimator = ValueAnimator.ofFloat(0f, 360f) mRotationAnimator?.addUpdateListener { mHeadInner.rotation += 10 } mRotationAnimator?.duration = 200 mRotationAnimator?.repeatCount = -1 mRotationAnimator?.start() } override fun reset() { if (mRotationAnimator != null) { mRotationAnimator?.cancel() } } override fun onDetachedFromWindow() { super.onDetachedFromWindow() reset() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/head/HeaderRefreshView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.head import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.animation.Animation import android.view.animation.LinearInterpolator import android.view.animation.RotateAnimation import android.widget.FrameLayout import android.widget.ImageView import android.widget.RelativeLayout import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.dip2px /** * Author: andy.xwt * Date: 2017/12/13 16:50 * Description: */ class HeaderRefreshView : FrameLayout { private val mRefreshContainer by bindView(R.id.rl_refresh_container) private val mIvRefresh by bindView(R.id.iv_refresh) private var mRotateAnimation: RotateAnimation? = null /** * 执行刷新阀值 50dp */ companion object { private const val REFRESH_THRESHOLD_VALUE = 50f } constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.refresh_category_header, this, true) mRefreshContainer.background.alpha = 0 mIvRefresh.imageAlpha = 0 } /** * 显示刷新遮罩 */ fun showRefreshCover(scrollValue: Int) { if (scrollValue in 1..getRefreshThresholdValue()) { val percent = (scrollValue.toFloat() / getRefreshThresholdValue()) mRefreshContainer.background.alpha = (percent * 255).toInt() mIvRefresh.imageAlpha = (percent * 255).toInt() mIvRefresh.scaleX = percent mIvRefresh.scaleY = percent } else { mRefreshContainer.background.alpha = 255 mIvRefresh.imageAlpha = 255 startRefreshAnimation() } } /** * 执行刷新动画 */ private fun startRefreshAnimation() { if (mRotateAnimation == null) { mRotateAnimation = RotateAnimation(0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) .apply { interpolator = LinearInterpolator() repeatCount = -1 duration = 1000 } mIvRefresh.startAnimation(mRotateAnimation) } } /** * 关闭刷新遮罩 */ fun hideRefreshCover() { mIvRefresh.clearAnimation() mRotateAnimation = null ValueAnimator.ofFloat(1f, 0f).apply { duration = 500 addUpdateListener { val animatedValue = (it.animatedValue) as Float mRefreshContainer.background.alpha = (animatedValue * 255).toInt() mIvRefresh.imageAlpha = animatedValue.toInt() } }.start() } /** * 获取刷新阀值 */ fun getRefreshThresholdValue() = context.dip2px(REFRESH_THRESHOLD_VALUE) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/head/HomePageHeaderView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.head import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout import android.widget.ImageView import android.widget.RelativeLayout import androidx.viewpager.widget.ViewPager import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.entity.TopIssue import com.jennifer.andy.simpleeyes.ui.base.BaseFragment import com.jennifer.andy.simpleeyes.ui.home.DailyEliteActivity import com.jennifer.andy.simpleeyes.ui.search.SearchHotActivity import com.jennifer.andy.simpleeyes.ui.video.VideoDetailActivity import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.readyGo import com.jennifer.andy.simpleeyes.widget.font.CustomFontTypeWriterTextView import com.jennifer.andy.simpleeyes.widget.image.imageloader.FrescoImageLoader import com.youth.banner.Banner import com.youth.banner.BannerConfig import java.util.* /** * Author: andy.xwt * Date: 2017/11/27 13:55 * Description:主界面头部布局 */ class HomePageHeaderView : FrameLayout { private val mBanner: Banner by bindView(R.id.head_banner) private val mTitle: CustomFontTypeWriterTextView by bindView(R.id.tv_title) private val mText: CustomFontTypeWriterTextView by bindView(R.id.tv_text) private val mHeadRefreshView: HeaderRefreshView by bindView(R.id.head_refresh) private val mIvSearch: ImageView by bindView(R.id.iv_search) private val mMoreContainer: RelativeLayout by bindView(R.id.rl_more_container) private lateinit var mTopIssue: TopIssue private lateinit var mBaseFragment: BaseFragment<*, *> private var mScrollValue = 0 private var currentPosition = -1 constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { LayoutInflater.from(context).inflate(R.layout.layout_category_head_view, this, true) init() } /** * 初始化 */ private fun init() { //设置banner滑动监听 mBanner.setOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) { } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { } override fun onPageSelected(position: Int) { //播放动画,并设置打印文字 if (currentPosition != position) {//处理banner的position问题 mTitle.printText(mTopIssue.data.itemList[position].data.title) mText.printText(mTopIssue.data.itemList[position].data.slogan) currentPosition = position } } }) //跳转到搜索界面 mIvSearch.setOnClickListener { mBaseFragment.readyGo() } //跳转到每日精选 mMoreContainer.setOnClickListener { mBaseFragment.readyGo() } } /** * 设置头部信息 */ fun setHeaderInfo(topIssue: TopIssue, videoListInfo: MutableList, baseFragment: BaseFragment<*, *>) { mTopIssue = topIssue mBaseFragment = baseFragment mBanner.setImageLoader(FrescoImageLoader()) mBanner.setImages(getTopIssueCardUrl(topIssue.data.itemList)) mBanner.setBannerStyle(BannerConfig.CIRCLE_INDICATOR) mBanner.setIndicatorGravity(BannerConfig.CENTER) mBanner.isAutoPlay(true) mBanner.start() mBanner.setDelayTime(6000) mBanner.setOnBannerListener { val item = mTopIssue.data.itemList[it] VideoDetailActivity.start(context, item.data, videoListInfo as ArrayList, 0) } } /** * 显示刷新遮罩 */ fun showRefreshCover(scrollValue: Int) { mScrollValue = scrollValue mHeadRefreshView.showRefreshCover(scrollValue) mBanner.stopAutoPlay() } /** * 关闭刷新遮罩 */ fun hideRefreshCover() { mHeadRefreshView.hideRefreshCover() mBanner.startAutoPlay() } /** * 获取顶部图片地址集合 */ private fun getTopIssueCardUrl(itemList: MutableList) = itemList.map { it.data.cover.feed } /** * 判断是否达到刷新阀值, */ fun judgeCanRefresh() = mScrollValue >= mHeadRefreshView.getRefreshThresholdValue() } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/head/VideoDetailHeadView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.head import android.animation.ObjectAnimator import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import com.alibaba.android.arouter.launcher.ARouter import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.UserPreferences import com.jennifer.andy.simpleeyes.utils.getElapseTimeForShow import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTextView import com.jennifer.andy.simpleeyes.widget.font.CustomFontTypeWriterTextView /** * Author: andy.xwt * Date: 2018/2/11 18:02 * Description:包含视频的介绍视频信息,收藏、分享、回复等操作 */ class VideoDetailHeadView : FrameLayout, View.OnClickListener { private val mTitle by bindView(R.id.tv_title) private val mTvTime by bindView(R.id.tv_time) private val mDescription by bindView(R.id.tv_desc) private val mFavorite by bindView(R.id.tv_favorite) private val mShare by bindView(R.id.tv_share) private val mReply by bindView(R.id.tv_reply) constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context) } private fun init(context: Context) { LayoutInflater.from(context).inflate(R.layout.layout_video_detail_head, this, true) mFavorite.setOnClickListener(this) mShare.setOnClickListener(this) mReply.setOnClickListener(this) mFavorite.setOnClickListener(this) } /** * 开启动画 */ fun startScrollAnimation() { val widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) measure(widthSpec, heightSpec) ObjectAnimator.ofFloat(this, "translationY", -measuredHeight.toFloat(), 0f).apply { duration = 300 start() } } override fun onClick(v: View) { val userIsLogin = UserPreferences.getUserIsLogin() if (!userIsLogin) //如果用户没登录,直接跳转到登录界面 ARouter.getInstance().build("/github/Login").navigation() else { when (v.id) { R.id.tv_favorite -> addFavorite() R.id.tv_share -> showShare() R.id.tv_reply -> addReply() R.id.tv_download -> downloadVideo() } } } /** * 添加收藏 */ private fun addFavorite() { } /** * 显示分享 */ private fun showShare() { } /** * 添加回复 */ private fun addReply() { } /** * 下载视频 */ private fun downloadVideo() { } /** * 设置收藏次数 */ fun setFavoriteCount(count: String) { mFavorite.text = count } /** * 设置分享次数 */ fun setShareCount(count: String) { mShare.text = count } /** * 设置评论个数 */ fun setReplayCount(count: String) { mReply.text = count } /** * 设置标题 */ fun setTitle(title: String) { mTitle.printText(title) } /** * 设置描述 */ fun setDescription(description: String) { mDescription.printText(description) } /** * 设置种类与时间 */ fun setCategoryAndTime(category: String, duration: Int) { val description = "#$category / ${getElapseTimeForShow(duration)}" mTvTime.text = description } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/refresh/LinearLayoutManagerWithSmoothScroller.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.refresh import android.content.Context import android.graphics.PointF import android.util.DisplayMetrics import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView /** * Author: andy.xwt * Date: 2018/6/19 22:24 * Description:SmoothScrollerToTop 平滑滚动式 滚动位置的View与RecyclerView顶部对齐 */ class LinearLayoutManagerWithSmoothScroller : LinearLayoutManager { constructor(context: Context) : super(context) constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout) override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State?, position: Int) { val smoothScroller = TopSnappedSmoothScroller(recyclerView.context) smoothScroller.targetPosition = position startSmoothScroll(smoothScroller) } inner class TopSnappedSmoothScroller(context: Context) : LinearSmoothScroller(context) { override fun computeScrollVectorForPosition(targetPosition: Int): PointF? { return this@LinearLayoutManagerWithSmoothScroller.computeScrollVectorForPosition(targetPosition) } /** * 默认顶部与RecyclerView对齐 */ override fun getVerticalSnapPreference(): Int { return SNAP_TO_START } /** * 滚动一英寸默认需要时间这里我改小了,意味着滚动速度变快了 */ override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics) = 15f / displayMetrics.densityDpi } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/refresh/PullRefreshView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.refresh import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout /** * Author: andy.xwt * Date: 2018/6/25 17:29 * Description: */ abstract class PullRefreshView : FrameLayout { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) /** * 处理额外下拉事件 */ abstract fun handleExtraPullEvent(dy: Float) /** * 处理有效下拉事件 */ abstract fun handleValidPullEvent(dy: Float) /** * 执行刷新操作 */ abstract fun doRefresh() /** * 执行刷新的高度 */ open fun getDoRefreshHeight() = height /** * 重置操作 */ abstract fun reset() } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/refresh/PullToRefresh.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.refresh /** * Author: andy.xwt * Date: 2018/6/15 10:12 * Description: */ interface PullToRefresh { /** * 设置刷新View */ fun initRefreshView(): PullRefreshView? /** * 是否正在刷新 */ fun isRefreshing(): Boolean /** * 刷新完毕 */ fun refreshComplete() /** * 下拉刷新是否可用 */ fun isPullToRefreshEnabled(): Boolean /** * 获取根布局 */ fun getRootView(): T } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/refresh/PullToRefreshBase.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.refresh import android.content.Context import android.util.AttributeSet import android.view.* import android.widget.LinearLayout import android.widget.Scroller import androidx.customview.widget.ViewDragHelper.INVALID_POINTER import kotlin.math.abs /** * Author: andy.xwt * Date: 2018/6/15 10:16 * Description: 基础刷新父布局。处理竖直方向上的事件分发。处理了多点触摸下的刷新。 */ abstract class PullToRefreshBase : LinearLayout, PullToRefresh { protected lateinit var mRootView: T protected var mRefreshView: PullRefreshView? = null private lateinit var mSmoothScroller: Scroller private var mTouchSlop: Int = 0 lateinit var refreshListener: () -> Unit /** * 用来记录y轴上的速度 */ private lateinit var mVelocityTracker: VelocityTracker /** * 刷新View实际高度 */ private var mRefreshHeight = 0 /** * 拖动的时候有效的手指id */ private var mActivePointerId = INVALID_POINTER /** * 当前刷新状态 */ var mScrollState = SCROLL_STATE_IDLE companion object { /** * 当前刷新是空闲的 */ const val SCROLL_STATE_IDLE = 0 /** * 当前正在拖动 */ const val SCROLL_STATE_DRAGGING = 1 /** * 本身的滑动 */ const val SCROLL_STATE_SETTLING = 2 /** * 滑动最小接近距离,单位dp */ const val SCROLL_CLOSE_ENOUGH = 2 private const val ROTATION_DAMP = 1.5f//阻尼系数 } /** * 最后点击的位置 */ private var mLastMotionX = 0f private var mLastMotionY = 0f private var mInitialMotionX = 0f private var mInitialMotionY = 0f private var isRefreshEnable = true //是否能下拉刷新 private var isBeingDragged = false//是否正在进行拖拽 private var isUnableToDrag = false//当前不能拖拽 用于判断x y轴移动距离的 private var isScrollStarted = false//内容是否开始滚动 private var isRefreshIng = false//是否正在刷新 private val mEndScrollRunnable = Runnable { mScrollState = SCROLL_STATE_IDLE } constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context, attrs) } private fun init(context: Context, attrs: AttributeSet?) { orientation = VERTICAL mRootView = createRootView(context) mTouchSlop = ViewConfiguration.get(getContext()).scaledTouchSlop mSmoothScroller = Scroller(getContext()) mRefreshView = initRefreshView() addView(mRefreshView, 0) addView(mRootView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) if (mRefreshView != null) { //获取刷新view的高度 measureChild(mRefreshView, widthMeasureSpec, heightMeasureSpec) mRefreshHeight = mRefreshView!!.measuredHeight val layoutParams = mRefreshView?.layoutParams as LayoutParams layoutParams.topMargin = -mRefreshHeight mRefreshView?.layoutParams = layoutParams } else { throw RuntimeException("you not set refreshView!!") } } override fun onInterceptTouchEvent(event: MotionEvent): Boolean { if (isPullToRefreshEnabled()) { val action = event.action if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { //重置 isBeingDragged = false return false } if (action != MotionEvent.ACTION_DOWN) { if (isBeingDragged) {//如果正在进行拖拽,就拦截 return true } if (isUnableToDrag) {//如果不能被拖拽,就不拦截 return false } if (!mSmoothScroller.computeScrollOffset()) { mSmoothScroller.abortAnimation() } } when (action and MotionEvent.ACTION_MASK) { MotionEvent.ACTION_DOWN -> { if (isReadyForPullStart()) { mLastMotionX = event.x mInitialMotionX = event.x mLastMotionY = event.y mInitialMotionY = event.y //获取有效手指 第一个有效手指的角标总是0 mActivePointerId = event.getPointerId(0) isUnableToDrag = false //在点击的时候,判断当前刷新界面是否下拉显示,或者在回退的路上 if (mScrollState == SCROLL_STATE_SETTLING && getScrollDiff() > SCROLL_CLOSE_ENOUGH) { isBeingDragged = true mScrollState = SCROLL_STATE_DRAGGING } else { isBeingDragged = false } } } MotionEvent.ACTION_MOVE -> { //获取有效的手指的距离 val actionPointerId = mActivePointerId if (mActivePointerId != INVALID_POINTER) { val pointerIndex = event.findPointerIndex(actionPointerId) val x = event.getX(pointerIndex) val xAbs = abs(x - mInitialMotionX) val y = event.getY(pointerIndex) val dy = y - mLastMotionY val yAbs = abs(dy) //如果竖直移动距离大于水平移动距离且为下拉事件,设置当前为拖动状态 if (yAbs > mTouchSlop && yAbs * 0.5 > xAbs && dy > 0) { isBeingDragged = true mScrollState = SCROLL_STATE_DRAGGING mLastMotionX = x mLastMotionY = if (dy > 0) mInitialMotionY + mTouchSlop else mInitialMotionY - mTouchSlop } else if (xAbs > mTouchSlop) { isUnableToDrag = true } } } MotionEvent.ACTION_POINTER_UP -> { onSecondaryPointerUp(event) } } } return isBeingDragged } /** * 重新赋值有效的手指,并记录抬起的手指的最后的y轴距离 */ private fun onSecondaryPointerUp(event: MotionEvent) { val pointerIndex = event.actionIndex val pointerId = event.getPointerId(pointerIndex) if (pointerId == mActivePointerId) { val newPointerIndex = if (pointerIndex == 0) 1 else 0 mLastMotionY = event.getY(newPointerIndex) mActivePointerId = event.getPointerId(newPointerIndex) } } override fun onTouchEvent(event: MotionEvent): Boolean { if (event.action == MotionEvent.ACTION_DOWN && event.edgeFlags != 0) return false if (mRefreshView == null) { return false } val action = event.action when (action and MotionEvent.ACTION_MASK) { MotionEvent.ACTION_CANCEL -> { if (isBeingDragged) { //滚动回去 isRefreshIng = false smoothScrollTo(-scrollX, -scrollY) isBeingDragged = false } } MotionEvent.ACTION_DOWN -> { //这个时候要停止刷新的滚动 mLastMotionX = event.x mInitialMotionX = event.x mLastMotionY = event.y mInitialMotionY = event.y //获取有效手指 第一个有效手指的角标总是0 mActivePointerId = event.getPointerId(0) mSmoothScroller.abortAnimation() } MotionEvent.ACTION_MOVE -> { if (isBeingDragged) { //获取有效的手指的距离 val actionPointerId = mActivePointerId if (mActivePointerId != INVALID_POINTER) { val pointerIndex = event.findPointerIndex(actionPointerId) val x = event.getX(pointerIndex) val xAbs = abs(x - mLastMotionX) val y = event.getY(pointerIndex) val dy = y - mLastMotionY val yAbs = abs(dy) //如果竖直移动距离大于水平移动距离,设置当前为拖动状态 if (yAbs > xAbs && scrollY <= 0) { isBeingDragged = true mScrollState = SCROLL_STATE_DRAGGING mLastMotionX = x mLastMotionY = y performDrag(dy / ROTATION_DAMP) dispatchExtraPullEvent(dy)//将竖直移动距离分发出去 } } } } MotionEvent.ACTION_UP -> { if (isBeingDragged) { isBeingDragged = false if (abs(scrollY) > mRefreshHeight / 2) {//如果超过一半就执行请求 //执行刷新请求 isRefreshIng = true smoothScrollTo(0, -(mRefreshView!!.getDoRefreshHeight() + scrollY), 500) } else { isRefreshIng = false //滚动回去 smoothScrollTo(-scrollX, -scrollY, 500) } } } MotionEvent.ACTION_POINTER_DOWN -> { val index = event.actionIndex val y = event.getY(index) val x = event.getX(index) mLastMotionY = y mLastMotionX = x mActivePointerId = event.getPointerId(index) } MotionEvent.ACTION_POINTER_UP -> { onSecondaryPointerUp(event) } } return true } /** * 开始滚动 */ private fun performDrag(dy: Float) { if (scrollY - dy <= 0) {//控制滚动范围为0到headView的显示高度 if (abs(scrollY - dy) in 0..mRefreshHeight) { scrollBy(0, -dy.toInt()) dispatchValidPullEvent(dy) } } } override fun computeScroll() { isScrollStarted = true //处理自己滚动效果,滚动到顶部或者到滚动到RefreshView的高度位置 if (mScrollState == SCROLL_STATE_SETTLING) { if (mSmoothScroller.computeScrollOffset()) { val x = mSmoothScroller.currX val y = mSmoothScroller.currY scrollTo(x, y) postInvalidate() return } else { if (isRefreshIng) { mRefreshView?.doRefresh() refreshListener() } else mRefreshView?.reset() mScrollState = SCROLL_STATE_IDLE } } } override fun refreshComplete() { if (isRefreshIng) { isRefreshIng = false smoothScrollTo(-scrollX, -scrollY, 1500)//这里增加了点时间,让视图不那么快的滚回去 } } override fun isRefreshing() = isRefreshIng /** 弹性滑动 * @param x 水平滑动距离 * @param y 竖直方向距离 */ private fun smoothScrollTo(x: Int, y: Int, duration: Int = 0) { mScrollState = SCROLL_STATE_SETTLING if (duration > 0) { mSmoothScroller.startScroll(scrollX, scrollY, x, y, duration) } else { mSmoothScroller.startScroll(scrollX, scrollY, x, y) } postInvalidate() } /** * 获取当前滑动的差值 */ private fun getScrollDiff(): Int { return abs(mSmoothScroller.finalY - mSmoothScroller.currY) } /** * 分发额外的移动距离 */ open fun dispatchExtraPullEvent(dy: Float) { } /** * 处理有效的移动距离 */ open fun dispatchValidPullEvent(dy: Float) { } /** * 是否允许下拉刷新 */ override fun isPullToRefreshEnabled() = isRefreshEnable /** * 创建根布局 */ abstract fun createRootView(context: Context): T /** * 是否准备好下拉 */ abstract fun isReadyForPullStart(): Boolean /** * 获取当前RecyclerView */ override fun getRootView() = mRootView override fun onDetachedFromWindow() { super.onDetachedFromWindow() if (!mSmoothScroller.isFinished) { mSmoothScroller.abortAnimation() } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/refresh/PullToRefreshRecyclerView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.refresh import android.content.Context import android.util.AttributeSet import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.chad.library.adapter.base.BaseQuickAdapter import com.jennifer.andy.simpleeyes.widget.pull.head.EliteHeaderView /** * Author: andy.xwt * Date: 2018/6/19 14:37 * Description:下拉刷新RecyclerView */ class PullToRefreshRecyclerView : PullToRefreshBase { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) override fun createRootView(context: Context): RecyclerView = RecyclerView(context) /** * 判断当前recyclerView是否滑动到顶部,如果是在顶部就可以进行下拉 */ override fun isReadyForPullStart(): Boolean { val adapter = mRootView.adapter return if (adapter == null || adapter.itemCount == 0) true else !mRootView.canScrollVertically(-1) } /** * 设置适配器与布局管理器 */ fun setAdapterAndLayoutManager(adapter: BaseQuickAdapter<*, *>, layoutManager: LinearLayoutManager) { mRootView.adapter = adapter mRootView.layoutManager = layoutManager mRootView.itemAnimator = DefaultItemAnimator() } /** * 滚动到相应位置 */ fun smoothScrollToPosition(position: Int) { mRootView.smoothScrollToPosition(position) } /** * 添加滑动监听 */ fun addOnScrollListener(onScrollListener: RecyclerView.OnScrollListener) { mRootView.addOnScrollListener(onScrollListener) } override fun initRefreshView() = EliteHeaderView(context) override fun dispatchExtraPullEvent(dy: Float) { mRefreshView?.handleExtraPullEvent(dy) } override fun dispatchValidPullEvent(dy: Float) { mRefreshView?.handleValidPullEvent(dy) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/zoom/PullToZoom.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.zoom import android.content.res.TypedArray import android.view.View /** * Author: andy.xwt * Date: 2017/11/11 16:14 * Description:下拉变焦 T 下拉变焦根布局 */ interface PullToZoom { /** * 获取变焦的view */ fun getZoomView(): View? /** * 获取头布局 */ fun getHeaderView(): View? /** * 获取下拉变焦根布局 */ fun getPullRootView(): T /** * 下拉变焦是否可用 */ fun isPullToZoomEnabled(): Boolean /** * 是否正在变焦 */ fun isZooming(): Boolean /** * 是否存在视差 */ fun isParallax(): Boolean /** * 是否隐藏头部 */ fun isHideHeader(): Boolean /** * 处理配置的zoomView与headerView */ fun handleStyledAttributes(typedArray: TypedArray) } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/zoom/PullToZoomBase.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.zoom import android.content.Context import android.util.AttributeSet import android.view.* import android.widget.LinearLayout import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.getScreenHeight import kotlin.math.abs import kotlin.math.min import kotlin.math.roundToInt /** * Author: andy.xwt * Date: 2017/11/11 16:22 * Description:下拉变焦基础类 */ abstract class PullToZoomBase : LinearLayout, PullToZoom { private var mTouchSlop = 0 private var mScreenHeight = 0 private var mScreenWidth = 0 protected lateinit var mRootView: T protected var mZoomView: View? = null protected var mHeadView: View? = null private var isParallax = true private var isZoomEnable = true private var isZooming = false private var isHideHeader = false private var mIsBeingDragged = false//是否被拖拽 private var mInterceptPressedX = 0f private var mInterceptPressedY = 0f private var mTouchPressedX = 0f private var mTouchPressedY = 0f private val DAMPING = 3f//阻尼系数 protected var mPullZoomListener: OnPullZoomListener? = null constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(context, attrs) } private fun init(context: Context, attrs: AttributeSet?) { gravity = Gravity.CENTER mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop mScreenHeight = context.getScreenHeight() mScreenWidth = context.getScreenHeight() mRootView = createRootView(context) attrs?.let { val layoutInflater = LayoutInflater.from(context) val typeArray = context.obtainStyledAttributes(attrs, R.styleable.PullToZoomBase) val zoomViewResId = typeArray.getResourceId(R.styleable.PullToZoomBase_zoomView, 0) val headViewResId = typeArray.getResourceId(R.styleable.PullToZoomBase_headView, 0) isParallax = typeArray.getBoolean(R.styleable.PullToZoomBase_isHeaderParallax, true) if (zoomViewResId > 0) { mZoomView = layoutInflater.inflate(zoomViewResId, null, false) } if (headViewResId > 0) { mHeadView = layoutInflater.inflate(headViewResId, null, false) } handleStyledAttributes(typeArray) typeArray.recycle() } addView(mRootView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) } /** * 处理竖直拖动滑动监听,并记录当前拖动的状态 */ override fun onInterceptTouchEvent(event: MotionEvent): Boolean { if (isPullToZoomEnabled() && !isHideHeader) { val action = event.action //如果是取消或者手指抬起不拦截 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mIsBeingDragged = false return false } //如果正在拖拽,拦截 if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) { return true } when (action) { MotionEvent.ACTION_DOWN -> { if (isReadyForPullStart()) { mInterceptPressedX = event.x mInterceptPressedY = event.y mIsBeingDragged = false mTouchPressedY = event.y mTouchPressedX = event.x } } MotionEvent.ACTION_MOVE -> { val dy: Float val dx: Float val currentX = event.x val currentY = event.y dy = currentY - mInterceptPressedY dx = currentX - mInterceptPressedX if (dy > mTouchSlop && (abs(dy) > abs(dx)) && isReadyForPullStart()) { mInterceptPressedX = currentX mInterceptPressedY = currentY mIsBeingDragged = true } } } } return mIsBeingDragged } override fun onTouchEvent(event: MotionEvent): Boolean { if (isPullToZoomEnabled() && !isHideHeader()) { //如果是在边缘也不拦截 if (event.action == MotionEvent.ACTION_DOWN && event.edgeFlags != 0) { return false } when (event.action) { MotionEvent.ACTION_MOVE -> { if (mIsBeingDragged) { mInterceptPressedY = event.y mInterceptPressedX = event.x dispatchPullEvent() isZooming = true return true } } MotionEvent.ACTION_DOWN -> { if (isReadyForPullStart()) { mTouchPressedX = event.x mTouchPressedY = event.y return true } } MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { if (mIsBeingDragged) { mIsBeingDragged = false if (isZooming()) { smoothScrollToTop() isZooming = false } } return true } } } return false } /** * 创建根布局 */ abstract fun createRootView(context: Context): T /** * 是否准备好下拉 */ abstract fun isReadyForPullStart(): Boolean /** * 下拉变焦头布局 */ abstract fun pullHeadToZoom(scrollValue: Int) /** * 平滑滑动到顶部 */ abstract fun smoothScrollToTop() /** * 设置头view */ abstract fun setHeaderView(headerView: View?) /** * 设置头布局的高度 */ abstract fun setHeaderViewLayoutParams(layoutParams: LayoutParams) /** * 设置变焦view */ abstract fun setZoomView(zoomView: View?) /** * 处理下拉事件 */ private fun dispatchPullEvent() { val scrollValue = (min(mTouchPressedY - mInterceptPressedY, 0f) / DAMPING).roundToInt() pullHeadToZoom(scrollValue) mPullZoomListener?.onPullZooming(abs(scrollValue)) } override fun getZoomView() = mZoomView override fun getHeaderView() = mHeadView override fun getPullRootView(): T = mRootView override fun isPullToZoomEnabled() = isZoomEnable override fun isZooming() = isZooming override fun isParallax() = isParallax override fun isHideHeader() = isHideHeader fun setHideHeader(isHideHeader: Boolean) { this.isHideHeader = isHideHeader } fun setIsParallax(isParallax: Boolean) { this.isParallax = isParallax } fun setIsZoomEnable(isZoomEnable: Boolean) { this.isZoomEnable = isZoomEnable } fun setOnPullZoomListener(OnPullZoomListener: OnPullZoomListener) { mPullZoomListener = OnPullZoomListener } /** * 下拉变焦监听 */ interface OnPullZoomListener { /** * 正在变焦 * @param scrollValue 滑动距离 */ fun onPullZooming(scrollValue: Int) /** * 变焦结束 */ fun onPullZoomEnd() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/pull/zoom/PullToZoomRecyclerView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.pull.zoom import android.animation.ValueAnimator import android.content.Context import android.content.res.TypedArray import android.util.AttributeSet import android.view.View import android.view.animation.AccelerateInterpolator import android.widget.FrameLayout import android.widget.LinearLayout import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.chad.library.adapter.base.BaseQuickAdapter import kotlin.math.abs /** * Author: andy.xwt * Date: 2017/11/24 14:00 * Description:下拉放大recyclerView,只支持竖直拖动 */ class PullToZoomRecyclerView : PullToZoomBase { private lateinit var mHeaderContainer: FrameLayout private var mHeaderHeight: Int = 0 private var mValueAnimator: ValueAnimator? = null constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) /** * 添加头布局在recyclerView中,头布局包括 zoomView与headView */ override fun handleStyledAttributes(typedArray: TypedArray) { mHeaderContainer = FrameLayout(context) mZoomView?.let { mHeaderContainer.addView(mZoomView) } mHeadView?.let { mHeaderContainer.addView(mHeadView) } mRootView.adapter?.let { val adapter = mRootView.adapter as BaseQuickAdapter<*, *> adapter.addHeaderView(mHeaderContainer) } } override fun createRootView(context: Context): RecyclerView = RecyclerView(context) /** * 判断当前recyclerView是否滑动到顶部,如果是在顶部就可以进行下拉 */ override fun isReadyForPullStart(): Boolean { val adapter = mRootView.adapter return if (adapter == null || adapter.itemCount == 0) true else !mRootView.canScrollVertically(-1) } /** * 改变头部布局高度 */ override fun pullHeadToZoom(scrollValue: Int) { mValueAnimator?.let { if (mValueAnimator!!.isStarted) { mValueAnimator?.cancel() } } val lp = mHeaderContainer.layoutParams lp.height = abs(scrollValue) + mHeaderHeight mHeaderContainer.layoutParams = lp } /** * 滑动到顶部 */ override fun smoothScrollToTop() { mValueAnimator = ValueAnimator.ofInt(mHeaderContainer.bottom, mHeaderHeight) mValueAnimator?.duration = 350 mValueAnimator?.interpolator = AccelerateInterpolator() mValueAnimator?.addUpdateListener { val lp = mHeaderContainer.layoutParams lp.height = it.animatedValue as Int mHeaderContainer.layoutParams = lp if (lp.height == mHeaderHeight) { mPullZoomListener?.onPullZoomEnd() } } mValueAnimator?.start() } /** * 设置头布局 */ override fun setHeaderView(headerView: View?) { headerView?.let { mHeadView = headerView updateZoomAndHeaderView() } } /** * 设置头布局的高度,与宽度,该方法必须要调用 */ override fun setHeaderViewLayoutParams(layoutParams: LinearLayout.LayoutParams) { mHeaderContainer.let { mHeaderContainer.layoutParams = layoutParams mHeaderHeight = layoutParams.height } } /** * 设置放大布局 */ override fun setZoomView(zoomView: View?) { zoomView?.let { mZoomView = zoomView updateZoomAndHeaderView() } } /** * 更新放大布局与头布局 */ private fun updateZoomAndHeaderView() { mHeaderContainer?.let { mZoomView?.let { mHeaderContainer.addView(mZoomView) } mHeadView?.let { mHeaderContainer.addView(mHeadView) } } } /** * 设置适配器与布局管理器 */ fun setAdapterAndLayoutManager(adapter: BaseQuickAdapter<*, *>, layoutManager: LinearLayoutManager) { mRootView.adapter = adapter mRootView.layoutManager = layoutManager mRootView.itemAnimator = DefaultItemAnimator() updateView() } /** * 更新RecyclerView中的布局,并获取头布局的高度 */ private fun updateView() { mHeaderContainer?.let { val adapter = mRootView.adapter as BaseQuickAdapter<*, *> adapter.removeHeaderView(mHeaderContainer) mHeaderContainer.removeAllViews() updateZoomAndHeaderView() //获取头布局的高度 mHeaderHeight = mHeaderContainer.layoutParams.height adapter.addHeaderView(mHeaderContainer) } } fun scrollToTop() { mRootView.smoothScrollToPosition(0) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/state/MultipleStateView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.state import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.View.OnClickListener import android.view.ViewGroup import android.widget.ImageView import android.widget.RelativeLayout import android.widget.TextView import com.jennifer.andy.simpleeyes.R /** * Author: andy.xwt * Date: 2017/8/31 22:40 * Description:多状态布局。包含加载界面,错误界面,网络异常界面,内容界面,空界面 */ class MultipleStateView : RelativeLayout { private var mEmptyView: View? = null private var mNetErrorView: View? = null private var mLoadingView: View? = null private var mContentViews: MutableList = mutableListOf() enum class State { EMPTY, NET_ERROR, LOADING, CONTENT } constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) override fun addView(child: View?, params: ViewGroup.LayoutParams?) { super.addView(child, params) child?.let { if (child.tag != State.LOADING && child.tag != State.EMPTY && child.tag != State.NET_ERROR) { mContentViews.add(child) } } } fun showLoading() { switchContent(State.LOADING) } fun showEmpty(onClickListener: OnClickListener) { switchContent(State.EMPTY, onClickListener) } fun showNetError(onClickListener: OnClickListener) { switchContent(State.NET_ERROR, onClickListener) } fun showContent() { switchContent(State.CONTENT) } /** * 设置空数据view */ private fun setEmptyView(onClickListener: OnClickListener) { if (mEmptyView == null) { mEmptyView = LayoutInflater.from(context).inflate(R.layout.layout_loading_message, null) mEmptyView?.tag = State.EMPTY val imageView = mEmptyView?.findViewById(R.id.iv_image) val errorText = mEmptyView?.findViewById(R.id.tv_message_info) imageView?.setImageResource(R.drawable.ic_eye_black_error) errorText?.setText(R.string.empty_message) mEmptyView?.setOnClickListener(onClickListener) addStateView(mEmptyView) } else { mEmptyView?.visibility = View.VISIBLE } } /** * 显示空数据View */ private fun showEmptyView(onClickListener: OnClickListener) { setEmptyView(onClickListener) hideLoadingView() hideNetErrorView() setContentViewVisible(false) } /** * 隐藏空数据view */ private fun hideEmptyView() { mEmptyView?.let { it.visibility = View.GONE } } /** * 设置网络异常view */ private fun setNetErrorView(onClickListener: OnClickListener) { if (mNetErrorView == null) { mNetErrorView = LayoutInflater.from(context).inflate(R.layout.layout_loading_message, null) mNetErrorView?.tag = State.NET_ERROR val imageView = mNetErrorView?.findViewById(R.id.iv_image) val errorText = mNetErrorView?.findViewById(R.id.tv_message_info) imageView?.setImageResource(R.drawable.ic_eye_black_error) errorText?.setText(R.string.net_error_message) mNetErrorView?.setOnClickListener(onClickListener) addStateView(mNetErrorView) } else { mNetErrorView?.visibility = View.VISIBLE } } /** * 显示网络异常view */ private fun showNetErrorView(onClickListener: OnClickListener) { setNetErrorView(onClickListener) hideLoadingView() hideEmptyView() setContentViewVisible(false) } /** * 隐藏网络异常view */ private fun hideNetErrorView() { mNetErrorView?.let { it.visibility = View.GONE } } /** * 设置加载中view */ private fun setLoadingView(onClickListener: OnClickListener) { if (mLoadingView == null) { mLoadingView = NetLoadingView(context) mLoadingView?.tag = State.LOADING mLoadingView?.setOnClickListener(onClickListener) addStateView(mLoadingView) } else { mLoadingView?.visibility = View.VISIBLE } } /** * 显示加载view */ private fun showLoadingView(onClickListener: OnClickListener) { setLoadingView(onClickListener) hideEmptyView() hideNetErrorView() setContentViewVisible(false) } /** * 隐藏加载view */ private fun hideLoadingView() { mLoadingView?.let { it.visibility = View.GONE } } /** * 设置内容界面是否显示 */ private fun setContentViewVisible(isVisible: Boolean) { for (mContentView in mContentViews) { mContentView.visibility = if (isVisible) View.VISIBLE else View.GONE } } /** * 显示内容视图 */ private fun showContentView() { hideEmptyView() hideNetErrorView() hideLoadingView() setContentViewVisible(true) } /** * 添加状态布局 */ private fun addStateView(view: View?) { val layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) layoutParams.addRule(CENTER_IN_PARENT) addView(view, layoutParams) } /** * 切换布局 */ private fun switchContent(state: State, onClickListener: OnClickListener = OnClickListener { }) { when (state) { State.EMPTY -> showEmptyView(onClickListener) State.LOADING -> showLoadingView(onClickListener) State.NET_ERROR -> showNetErrorView(onClickListener) State.CONTENT -> showContentView() } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/state/NetLoadingView.kt ================================================ package com.jennifer.andy.simpleeyes.widget.state import android.animation.ObjectAnimator import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout import android.widget.ImageView import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.utils.bindView /** * Author: andy.xwt * Date: 2018/2/11 17:44 * Description:网络加载view */ class NetLoadingView : FrameLayout { private val mHeadInner by bindView(R.id.iv_head_inner) private lateinit var mRotationAnimator: ObjectAnimator constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { LayoutInflater.from(getContext()).inflate(R.layout.layout_loading_view, this) doInnerEyeAnimator() } /** * 执行内部眼睛动画 */ private fun doInnerEyeAnimator() { mRotationAnimator = ObjectAnimator.ofFloat(mHeadInner, "rotation", 0f, 360f) mRotationAnimator.duration = 1000 mRotationAnimator.repeatCount = -1 mRotationAnimator.start() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() mRotationAnimator.cancel() } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/tab/AnimationUtils.java ================================================ package com.jennifer.andy.simpleeyes.widget.tab; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import androidx.interpolator.view.animation.FastOutLinearInInterpolator; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; /** * Author: andy.xwt * Date: 2018/7/4 13:47 * Description: */ public class AnimationUtils { static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator(); static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator(); static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); /** * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */ static float lerp(float startValue, float endValue, float fraction) { return startValue + (fraction * (endValue - startValue)); } static int lerp(int startValue, int endValue, float fraction) { return startValue + Math.round(fraction * (endValue - startValue)); } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/tab/ShortTabLayout.java ================================================ package com.jennifer.andy.simpleeyes.widget.tab; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Layout; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.google.android.material.tabs.TabLayout; import com.jennifer.andy.simpleeyes.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.TooltipCompat; import androidx.core.util.Pools; import androidx.core.view.GravityCompat; import androidx.core.view.PointerIconCompat; import androidx.core.view.ViewCompat; import androidx.core.widget.TextViewCompat; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING; import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE; import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING; /** * TabLayout provides a horizontal layout to display tabs. *

*

Population of the tabs to display is * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)} * respectively. To display the tab, you need to add it to the layout via one of the * {@link #addTab(Tab)} methods. For example: *

 * TabLayout tabLayout = ...;
 * tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
 * tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
 * tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
 * 
* You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be * notified when any tab's selection state has been changed. *

*

You can also add items to TabLayout in your layout through the use of {@link TabItem}. * An example usage is like so:

*

*

 * <TabLayout
 *         android:layout_height="wrap_content"
 *         android:layout_width="match_parent">
 *
 *     <android.support.design.widget.TabItem
 *             android:text="@string/tab_text"/>
 *
 *     <android.support.design.widget.TabItem
 *             android:icon="@drawable/ic_android"/>
 *
 * </TabLayout>
 * 
*

*

ViewPager integration

*

* If you're using a {@link ViewPager} together * with this layout, you can call {@link #setupWithViewPager(ViewPager)} to link the two together. * This layout will be automatically populated from the {@link PagerAdapter}'s page titles.

*

*

* This view also supports being used as part of a ViewPager's decor, and can be added * directly to the ViewPager in a layout resource file like so:

*

*

 * <android.support.v4.view.ViewPager
 *     android:layout_width="match_parent"
 *     android:layout_height="match_parent">
 *
 *     <TabLayout
 *         android:layout_width="match_parent"
 *         android:layout_height="wrap_content"
 *         android:layout_gravity="top" />
 *
 * </android.support.v4.view.ViewPager>
 * 
* * @attr ref android.support.design.R.styleable#TabLayout_tabPadding * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingStart * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingTop * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingEnd * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingBottom * @attr ref android.support.design.R.styleable#TabLayout_tabContentStart * @attr ref android.support.design.R.styleable#TabLayout_tabBackground * @attr ref android.support.design.R.styleable#TabLayout_tabMinWidth * @attr ref android.support.design.R.styleable#TabLayout_tabMaxWidth * @attr ref android.support.design.R.styleable#TabLayout_tabTextAppearance * @see Tabs */ @ViewPager.DecorView public class ShortTabLayout extends HorizontalScrollView { private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps static final int DEFAULT_GAP_TEXT_ICON = 8; // dps private static final int INVALID_WIDTH = -1; private static final int DEFAULT_HEIGHT = 48; // dps private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps static final int FIXED_WRAP_GUTTER_MIN = 16; //dps static final int MOTION_NON_ADJACENT_OFFSET = 24; private static final int ANIMATION_DURATION = 300; private static final Pools.Pool sTabPool = new Pools.SynchronizedPool<>(16); /** * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab * labels and a larger number of tabs. They are best used for browsing contexts in touch * interfaces when users don’t need to directly compare the tab labels. * * @see #setTabMode(int) * @see #getTabMode() */ public static final int MODE_SCROLLABLE = 0; /** * Fixed tabs display all tabs concurrently and are best used with content that benefits from * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. * Fixed tabs have equal width, based on the widest tab label. * * @see #setTabMode(int) * @see #getTabMode() */ public static final int MODE_FIXED = 1; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } /** * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect * when used with {@link #MODE_FIXED}. * * @see #setTabGravity(int) * @see #getTabGravity() */ public static final int GRAVITY_FILL = 0; /** * Gravity used to lay out the tabs in the center of the {@link TabLayout}. * * @see #setTabGravity(int) * @see #getTabGravity() */ public static final int GRAVITY_CENTER = 1; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) @Retention(RetentionPolicy.SOURCE) public @interface TabGravity { } /** * Callback interface invoked when a tab's selection state changes. */ public interface OnTabSelectedListener { /** * Called when a tab enters the selected state. * * @param tab The tab that was selected */ public void onTabSelected(Tab tab); /** * Called when a tab exits the selected state. * * @param tab The tab that was unselected */ public void onTabUnselected(Tab tab); /** * Called when a tab that is already selected is chosen again by the user. Some applications * may use this action to return to the top level of a category. * * @param tab The tab that was reselected. */ public void onTabReselected(Tab tab); } private final ArrayList mTabs = new ArrayList<>(); private Tab mSelectedTab; private final SlidingTabStrip mTabStrip; int mTabPaddingStart; int mTabPaddingTop; int mTabPaddingEnd; int mTabPaddingBottom; int mTabTextAppearance; ColorStateList mTabTextColors; float mTabTextSize; float mTabTextMultiLineSize; final int mTabBackgroundResId; int mTabMaxWidth = Integer.MAX_VALUE; private final int mRequestedTabMinWidth; private final int mRequestedTabMaxWidth; private final int mScrollableTabMinWidth; private int mContentInsetStart; int mTabGravity; int mMode; private OnTabSelectedListener mSelectedListener; private final ArrayList mSelectedListeners = new ArrayList<>(); private OnTabSelectedListener mCurrentVpSelectedListener; private ValueAnimator mScrollAnimator; ViewPager mViewPager; private PagerAdapter mPagerAdapter; private DataSetObserver mPagerAdapterObserver; private TabLayoutOnPageChangeListener mPageChangeListener; private AdapterChangeListener mAdapterChangeListener; private boolean mSetupViewPagerImplicitly; // Pool we use as a simple RecyclerBin private final Pools.Pool mTabViewPool = new Pools.SimplePool<>(12); public ShortTabLayout(Context context) { this(context, null); } public ShortTabLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ShortTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); checkAppCompatTheme(context); // Disable the Scroll Bar setHorizontalScrollBarEnabled(false); // Add the TabStrip mTabStrip = new SlidingTabStrip(context); super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, defStyleAttr, R.style.Widget_Design_TabLayout); mTabStrip.setSelectedIndicatorHeight( a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)); mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)); mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, mTabPaddingStart); mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, mTabPaddingTop); mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, mTabPaddingEnd); mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, mTabPaddingBottom); mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, R.style.TextAppearance_Design_Tab); // Text colors/sizes come from the text appearance first final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, R.styleable.TextAppearance); try { mTabTextSize = ta.getDimensionPixelSize( R.styleable.TextAppearance_android_textSize, 0); mTabTextColors = ta.getColorStateList( R.styleable.TextAppearance_android_textColor); } finally { ta.recycle(); } if (a.hasValue(R.styleable.TabLayout_tabTextColor)) { // If we have an explicit text color set, use it instead mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor); } if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) { // We have an explicit selected text color set, so we need to make merge it with the // current colors. This is exposed so that developers can use theme attributes to set // this (theme attrs in ColorStateLists are Lollipop+) final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0); mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); } mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, INVALID_WIDTH); mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, INVALID_WIDTH); mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0); mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0); mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED); mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL); a.recycle(); // TODO add attr for these final Resources res = getResources(); mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width); // Now apply the tab mode and gravity applyModeAndGravity(); } private static final int[] APPCOMPAT_CHECK_ATTRS = { R.attr.colorPrimary }; private void checkAppCompatTheme(Context context) { TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS); final boolean failed = !a.hasValue(0); a.recycle(); if (failed) { throw new IllegalArgumentException("You need to use a Theme.AppCompat theme " + "(or descendant) with the design library."); } } /** * Sets the tab indicator's color for the currently selected tab. * * @param color color to use for the indicator * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorColor */ public void setSelectedTabIndicatorColor(@ColorInt int color) { mTabStrip.setSelectedIndicatorColor(color); } /** * Sets the tab indicator's height for the currently selected tab. * * @param height height to use for the indicator in pixels * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorHeight */ public void setSelectedTabIndicatorHeight(int height) { mTabStrip.setSelectedIndicatorHeight(height); } /** * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as * part of a scrolling container such as {@link ViewPager}. *

* Calling this method does not update the selected tab, it is only used for drawing purposes. * * @param position current scroll position * @param positionOffset Value from [0, 1) indicating the offset from {@code position}. * @param updateSelectedText Whether to update the text's selected state. */ public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { setScrollPosition(position, positionOffset, updateSelectedText, true); } void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, boolean updateIndicatorPosition) { final int roundedPosition = Math.round(position + positionOffset); if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) { return; } // Set the indicator position, if enabled if (updateIndicatorPosition) { mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); } // Now update the scroll position, canceling any running animation if (mScrollAnimator != null && mScrollAnimator.isRunning()) { mScrollAnimator.cancel(); } scrollTo(calculateScrollXForTab(position, positionOffset), 0); // Update the 'selected state' view as we scroll, if enabled if (updateSelectedText) { setSelectedTabView(roundedPosition); } } private float getScrollPosition() { return mTabStrip.getIndicatorPosition(); } /** * Add a tab to this layout. The tab will be added at the end of the list. * If this is the first tab to be added it will become the selected tab. * * @param tab Tab to add */ public void addTab(@NonNull Tab tab) { addTab(tab, mTabs.isEmpty()); } /** * Add a tab to this layout. The tab will be inserted at position. * If this is the first tab to be added it will become the selected tab. * * @param tab The tab to add * @param position The new position of the tab */ public void addTab(@NonNull Tab tab, int position) { addTab(tab, position, mTabs.isEmpty()); } /** * Add a tab to this layout. The tab will be added at the end of the list. * * @param tab Tab to add * @param setSelected True if the added tab should become the selected tab. */ public void addTab(@NonNull Tab tab, boolean setSelected) { addTab(tab, mTabs.size(), setSelected); } /** * Add a tab to this layout. The tab will be inserted at position. * * @param tab The tab to add * @param position The new position of the tab * @param setSelected True if the added tab should become the selected tab. */ public void addTab(@NonNull Tab tab, int position, boolean setSelected) { if (tab.mParent != this) { throw new IllegalArgumentException("Tab belongs to a different TabLayout."); } configureTab(tab, position); addTabView(tab); if (setSelected) { tab.select(); } } private void addTabFromItemView(@NonNull TabItem item) { final Tab tab = newTab(); if (item.mText != null) { tab.setText(item.mText); } if (item.mIcon != null) { tab.setIcon(item.mIcon); } if (item.mCustomLayout != 0) { tab.setCustomView(item.mCustomLayout); } if (!TextUtils.isEmpty(item.getContentDescription())) { tab.setContentDescription(item.getContentDescription()); } addTab(tab); } /** * @deprecated Use {@link #addOnTabSelectedListener(OnTabSelectedListener)} and * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}. */ @Deprecated public void setOnTabSelectedListener(@Nullable OnTabSelectedListener listener) { // The logic in this method emulates what we had before support for multiple // registered listeners. if (mSelectedListener != null) { removeOnTabSelectedListener(mSelectedListener); } // Update the deprecated field so that we can remove the passed listener the next // time we're called mSelectedListener = listener; if (listener != null) { addOnTabSelectedListener(listener); } } /** * Add a {@link OnTabSelectedListener} that will be invoked when tab selection * changes. *

*

Components that add a listener should take care to remove it when finished via * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.

* * @param listener listener to add */ public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { if (!mSelectedListeners.contains(listener)) { mSelectedListeners.add(listener); } } /** * Remove the given {@link OnTabSelectedListener} that was previously added via * {@link #addOnTabSelectedListener(OnTabSelectedListener)}. * * @param listener listener to remove */ public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { mSelectedListeners.remove(listener); } /** * Remove all previously added {@link OnTabSelectedListener}s. */ public void clearOnTabSelectedListeners() { mSelectedListeners.clear(); } /** * Create and return a new {@link Tab}. You need to manually add this using * {@link #addTab(Tab)} or a related method. * * @return Demo new Tab * @see #addTab(Tab) */ @NonNull public Tab newTab() { Tab tab = sTabPool.acquire(); if (tab == null) { tab = new Tab(); } tab.mParent = this; tab.mView = createTabView(tab); return tab; } /** * Returns the number of tabs currently registered with the action bar. * * @return Tab count */ public int getTabCount() { return mTabs.size(); } /** * Returns the tab at the specified index. */ @Nullable public Tab getTabAt(int index) { return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index); } /** * Returns the position of the current selected tab. * * @return selected tab position, or {@code -1} if there isn't a selected tab. */ public int getSelectedTabPosition() { return mSelectedTab != null ? mSelectedTab.getPosition() : -1; } /** * Remove a tab from the layout. If the removed tab was selected it will be deselected * and another tab will be selected if present. * * @param tab The tab to remove */ public void removeTab(Tab tab) { if (tab.mParent != this) { throw new IllegalArgumentException("Tab does not belong to this TabLayout."); } removeTabAt(tab.getPosition()); } /** * Remove a tab from the layout. If the removed tab was selected it will be deselected * and another tab will be selected if present. * * @param position Position of the tab to remove */ public void removeTabAt(int position) { final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0; removeTabViewAt(position); final Tab removedTab = mTabs.remove(position); if (removedTab != null) { removedTab.reset(); sTabPool.release(removedTab); } final int newTabCount = mTabs.size(); for (int i = position; i < newTabCount; i++) { mTabs.get(i).setPosition(i); } if (selectedTabPosition == position) { selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); } } /** * Remove all tabs from the action bar and deselect the current tab. */ public void removeAllTabs() { // Remove all the views for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) { removeTabViewAt(i); } for (final Iterator i = mTabs.iterator(); i.hasNext(); ) { final Tab tab = i.next(); i.remove(); tab.reset(); sTabPool.release(tab); } mSelectedTab = null; } /** * Set the behavior mode for the Tabs in this layout. The valid input options are: *
    *
  • {@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used * with content that benefits from quick pivots between tabs.
  • *
  • {@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment, * and can contain longer tab labels and a larger number of tabs. They are best used for * browsing contexts in touch interfaces when users don’t need to directly compare the tab * labels. This mode is commonly used with a {@link ViewPager}.
  • *
* * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}. * @attr ref android.support.design.R.styleable#TabLayout_tabMode */ public void setTabMode(@Mode int mode) { if (mode != mMode) { mMode = mode; applyModeAndGravity(); } } /** * Returns the current mode used by this {@link TabLayout}. * * @see #setTabMode(int) */ @Mode public int getTabMode() { return mMode; } /** * Set the gravity to use when laying out the tabs. * * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. * @attr ref android.support.design.R.styleable#TabLayout_tabGravity */ public void setTabGravity(@TabGravity int gravity) { if (mTabGravity != gravity) { mTabGravity = gravity; applyModeAndGravity(); } } /** * The current gravity used for laying out tabs. * * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. */ @TabGravity public int getTabGravity() { return mTabGravity; } /** * Sets the text colors for the different states (normal, selected) used for the tabs. * * @see #getTabTextColors() */ public void setTabTextColors(@Nullable ColorStateList textColor) { if (mTabTextColors != textColor) { mTabTextColors = textColor; updateAllTabs(); } } /** * Gets the text colors for the different states (normal, selected) used for the tabs. */ @Nullable public ColorStateList getTabTextColors() { return mTabTextColors; } /** * Sets the text colors for the different states (normal, selected) used for the tabs. * * @attr ref android.support.design.R.styleable#TabLayout_tabTextColor * @attr ref android.support.design.R.styleable#TabLayout_tabSelectedTextColor */ public void setTabTextColors(int normalColor, int selectedColor) { setTabTextColors(createColorStateList(normalColor, selectedColor)); } /** * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}. *

*

This is the same as calling {@link #setupWithViewPager(ViewPager, boolean)} with * auto-refresh enabled.

* * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link */ public void setupWithViewPager(@Nullable ViewPager viewPager) { setupWithViewPager(viewPager, true); } /** * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}. *

*

This method will link the given ViewPager and this TabLayout together so that * changes in one are automatically reflected in the other. This includes scroll state changes * and clicks. The tabs displayed in this layout will be populated * from the ViewPager adapter's page titles.

*

*

If {@code autoRefresh} is {@code true}, any changes in the {@link PagerAdapter} will * trigger this layout to re-populate itself from the adapter's titles.

*

*

If the given ViewPager is non-null, it needs to already have a * {@link PagerAdapter} set.

* * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link * @param autoRefresh whether this layout should refresh its contents if the given ViewPager's * content changes */ public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh) { setupWithViewPager(viewPager, autoRefresh, false); } private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh, boolean implicitSetup) { if (mViewPager != null) { // If we've already been setup with a ViewPager, remove us from it if (mPageChangeListener != null) { mViewPager.removeOnPageChangeListener(mPageChangeListener); } if (mAdapterChangeListener != null) { mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener); } } if (mCurrentVpSelectedListener != null) { // If we already have a tab selected listener for the ViewPager, remove it removeOnTabSelectedListener(mCurrentVpSelectedListener); mCurrentVpSelectedListener = null; } if (viewPager != null) { mViewPager = viewPager; // Add our custom OnPageChangeListener to the ViewPager if (mPageChangeListener == null) { mPageChangeListener = new TabLayoutOnPageChangeListener(this); } mPageChangeListener.reset(); viewPager.addOnPageChangeListener(mPageChangeListener); // Now we'll add a tab selected listener to set ViewPager's current item mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager); addOnTabSelectedListener(mCurrentVpSelectedListener); final PagerAdapter adapter = viewPager.getAdapter(); if (adapter != null) { // Now we'll populate ourselves from the pager adapter, adding an observer if // autoRefresh is enabled setPagerAdapter(adapter, autoRefresh); } // Add a listener so that we're notified of any adapter changes if (mAdapterChangeListener == null) { mAdapterChangeListener = new AdapterChangeListener(); } mAdapterChangeListener.setAutoRefresh(autoRefresh); viewPager.addOnAdapterChangeListener(mAdapterChangeListener); // Now update the scroll position to match the ViewPager's current item setScrollPosition(viewPager.getCurrentItem(), 0f, true); } else { // We've been given a null ViewPager so we need to clear out the internal state, // listeners and observers mViewPager = null; setPagerAdapter(null, false); } mSetupViewPagerImplicitly = implicitSetup; } /** * @deprecated Use {@link #setupWithViewPager(ViewPager)} to link a TabLayout with a ViewPager * together. When that method is used, the TabLayout will be automatically updated * when the {@link PagerAdapter} is changed. */ @Deprecated public void setTabsFromPagerAdapter(@Nullable final PagerAdapter adapter) { setPagerAdapter(adapter, false); } @Override public boolean shouldDelayChildPressedState() { // Only delay the pressed state if the tabs can scroll return getTabScrollRange() > 0; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mViewPager == null) { // If we don't have a ViewPager already, check if our parent is a ViewPager to // setup with it automatically final ViewParent vp = getParent(); if (vp instanceof ViewPager) { // If we have a ViewPager parent and we've been added as part of its decor, let's // assume that we should automatically setup to display any titles setupWithViewPager((ViewPager) vp, true, true); } } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mSetupViewPagerImplicitly) { // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc setupWithViewPager(null); mSetupViewPagerImplicitly = false; } } private int getTabScrollRange() { return Math.max(0, mTabStrip.getWidth() - getWidth() - getPaddingLeft() - getPaddingRight()); } void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) { if (mPagerAdapter != null && mPagerAdapterObserver != null) { // If we already have a PagerAdapter, unregister our observer mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); } mPagerAdapter = adapter; if (addObserver && adapter != null) { // Register our observer on the new adapter if (mPagerAdapterObserver == null) { mPagerAdapterObserver = new PagerAdapterObserver(); } adapter.registerDataSetObserver(mPagerAdapterObserver); } // Finally make sure we reflect the new adapter populateFromPagerAdapter(); } void populateFromPagerAdapter() { removeAllTabs(); if (mPagerAdapter != null) { final int adapterCount = mPagerAdapter.getCount(); for (int i = 0; i < adapterCount; i++) { addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false); } // Make sure we reflect the currently set ViewPager item if (mViewPager != null && adapterCount > 0) { final int curItem = mViewPager.getCurrentItem(); if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { selectTab(getTabAt(curItem)); } } } } private void updateAllTabs() { for (int i = 0, z = mTabs.size(); i < z; i++) { mTabs.get(i).updateView(); } } private TabView createTabView(@NonNull final Tab tab) { TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null; if (tabView == null) { tabView = new TabView(getContext()); } tabView.setTab(tab); tabView.setFocusable(true); tabView.setMinimumWidth(getTabMinWidth()); return tabView; } private void configureTab(Tab tab, int position) { tab.setPosition(position); mTabs.add(position, tab); final int count = mTabs.size(); for (int i = position + 1; i < count; i++) { mTabs.get(i).setPosition(i); } } private void addTabView(Tab tab) { final TabView tabView = tab.mView; mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs()); } @Override public void addView(View child) { addViewInternal(child); } @Override public void addView(View child, int index) { addViewInternal(child); } @Override public void addView(View child, ViewGroup.LayoutParams params) { addViewInternal(child); } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { addViewInternal(child); } private void addViewInternal(final View child) { if (child instanceof TabItem) { addTabFromItemView((TabItem) child); } else { throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout"); } } private LinearLayout.LayoutParams createLayoutParamsForTabs() { final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); updateTabViewLayoutParams(lp); return lp; } private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { lp.width = 0; lp.weight = 1; } else { lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; lp.weight = 0; } } int dpToPx(int dps) { return Math.round(getResources().getDisplayMetrics().density * dps); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // If we have a MeasureSpec which allows us to decide our height, try and use the default // height final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom(); switch (MeasureSpec.getMode(heightMeasureSpec)) { case MeasureSpec.AT_MOST: heightMeasureSpec = MeasureSpec.makeMeasureSpec( Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), MeasureSpec.EXACTLY); break; case MeasureSpec.UNSPECIFIED: heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); break; } final int specWidth = MeasureSpec.getSize(widthMeasureSpec); if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { // If we don't have an unspecified width spec, use the given size to calculate // the max tab width mTabMaxWidth = mRequestedTabMaxWidth > 0 ? mRequestedTabMaxWidth : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); } // Now super measure itself using the (possibly) modified height spec super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() == 1) { // If we're in fixed mode then we need to make the tab strip is the same width as us // so we don't scroll final View child = getChildAt(0); boolean remeasure = false; switch (mMode) { case MODE_SCROLLABLE: // We only need to resize the child if it's smaller than us. This is similar // to fillViewport remeasure = child.getMeasuredWidth() < getMeasuredWidth(); break; case MODE_FIXED: // Resize the child so that it doesn't scroll remeasure = child.getMeasuredWidth() != getMeasuredWidth(); break; } if (remeasure) { // Re-measure the child with a widthSpec set to be exactly our measure width int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), child.getLayoutParams().height); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( getMeasuredWidth(), MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } private void removeTabViewAt(int position) { final TabView view = (TabView) mTabStrip.getChildAt(position); mTabStrip.removeViewAt(position); if (view != null) { view.reset(); mTabViewPool.release(view); } requestLayout(); } private void animateToTab(int newPosition) { if (newPosition == Tab.INVALID_POSITION) { return; } if (getWindowToken() == null || !ViewCompat.isLaidOut(this) || mTabStrip.childrenNeedLayout()) { // If we don't have a window token, or we haven't been laid out yet just draw the new // position now setScrollPosition(newPosition, 0f, true); return; } final int startScrollX = getScrollX(); final int targetScrollX = calculateScrollXForTab(newPosition, 0); if (startScrollX != targetScrollX) { ensureScrollAnimator(); mScrollAnimator.setIntValues(startScrollX, targetScrollX); mScrollAnimator.start(); } // Now animate the indicator mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); } private void ensureScrollAnimator() { if (mScrollAnimator == null) { mScrollAnimator = new ValueAnimator(); mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); mScrollAnimator.setDuration(ANIMATION_DURATION); mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { scrollTo((int) animator.getAnimatedValue(), 0); } }); } } void setScrollAnimatorListener(Animator.AnimatorListener listener) { ensureScrollAnimator(); mScrollAnimator.addListener(listener); } private void setSelectedTabView(int position) { final int tabCount = mTabStrip.getChildCount(); if (position < tabCount) { for (int i = 0; i < tabCount; i++) { final View child = mTabStrip.getChildAt(i); child.setSelected(i == position); } } } void selectTab(Tab tab) { selectTab(tab, true); } void selectTab(final Tab tab, boolean updateIndicator) { final Tab currentTab = mSelectedTab; if (currentTab == tab) { if (currentTab != null) { dispatchTabReselected(tab); animateToTab(tab.getPosition()); } } else { final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION; if (updateIndicator) { if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION) && newPosition != Tab.INVALID_POSITION) { // If we don't currently have a tab, just draw the indicator setScrollPosition(newPosition, 0f, true); } else { animateToTab(newPosition); } if (newPosition != Tab.INVALID_POSITION) { setSelectedTabView(newPosition); } } if (currentTab != null) { dispatchTabUnselected(currentTab); } mSelectedTab = tab; if (tab != null) { dispatchTabSelected(tab); } } } private void dispatchTabSelected(@NonNull final Tab tab) { for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { mSelectedListeners.get(i).onTabSelected(tab); } } private void dispatchTabUnselected(@NonNull final Tab tab) { for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { mSelectedListeners.get(i).onTabUnselected(tab); } } private void dispatchTabReselected(@NonNull final Tab tab) { for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { mSelectedListeners.get(i).onTabReselected(tab); } } private int calculateScrollXForTab(int position, float positionOffset) { if (mMode == MODE_SCROLLABLE) { final View selectedChild = mTabStrip.getChildAt(position); final View nextChild = position + 1 < mTabStrip.getChildCount() ? mTabStrip.getChildAt(position + 1) : null; final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; // base scroll amount: places center of tab in center of parent int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2); // offset amount: fraction of the distance between centers of tabs int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset); return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) ? scrollBase + scrollOffset : scrollBase - scrollOffset; } return 0; } private void applyModeAndGravity() { int paddingStart = 0; if (mMode == MODE_SCROLLABLE) { // If we're scrollable, or fixed at start, inset using padding paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); } ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); switch (mMode) { case MODE_FIXED: mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL); break; case MODE_SCROLLABLE: mTabStrip.setGravity(GravityCompat.START); break; } updateTabViews(true); } void updateTabViews(final boolean requestLayout) { for (int i = 0; i < mTabStrip.getChildCount(); i++) { View child = mTabStrip.getChildAt(i); child.setMinimumWidth(getTabMinWidth()); updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); if (requestLayout) { child.requestLayout(); } } } /** * Demo tab in this layout. Instances can be created via {@link #newTab()}. */ public static final class Tab { /** * An invalid position for a tab. * * @see #getPosition() */ public static final int INVALID_POSITION = -1; private Object mTag; private Drawable mIcon; private CharSequence mText; private CharSequence mContentDesc; private int mPosition = INVALID_POSITION; private View mCustomView; ShortTabLayout mParent; TabView mView; Tab() { // Private constructor } /** * @return This Tab's tag object. */ @Nullable public Object getTag() { return mTag; } /** * Give this Tab an arbitrary object to hold for later use. * * @param tag Object to store * @return The current instance for call chaining */ @NonNull public Tab setTag(@Nullable Object tag) { mTag = tag; return this; } /** * Returns the custom view used for this tab. * * @see #setCustomView(View) * @see #setCustomView(int) */ @Nullable public View getCustomView() { return mCustomView; } /** * Set a custom view to be used for this tab. *

* If the provided view contains a {@link TextView} with an ID of * {@link android.R.id#text1} then that will be updated with the value given * to {@link #setText(CharSequence)}. Similarly, if this layout contains an * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with * the value given to {@link #setIcon(Drawable)}. *

* * @param view Custom view to be used as a tab. * @return The current instance for call chaining */ @NonNull public Tab setCustomView(@Nullable View view) { mCustomView = view; updateView(); return this; } /** * Set a custom view to be used for this tab. *

* If the inflated layout contains a {@link TextView} with an ID of * {@link android.R.id#text1} then that will be updated with the value given * to {@link #setText(CharSequence)}. Similarly, if this layout contains an * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with * the value given to {@link #setIcon(Drawable)}. *

* * @param resId Demo layout resource to inflate and use as a custom tab view * @return The current instance for call chaining */ @NonNull public Tab setCustomView(@LayoutRes int resId) { final LayoutInflater inflater = LayoutInflater.from(mView.getContext()); return setCustomView(inflater.inflate(resId, mView, false)); } /** * Return the icon associated with this tab. * * @return The tab's icon */ @Nullable public Drawable getIcon() { return mIcon; } /** * Return the current position of this tab in the action bar. * * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in * the action bar. */ public int getPosition() { return mPosition; } void setPosition(int position) { mPosition = position; } /** * Return the text of this tab. * * @return The tab's text */ @Nullable public CharSequence getText() { return mText; } /** * Set the icon displayed on this tab. * * @param icon The drawable to use as an icon * @return The current instance for call chaining */ @NonNull public Tab setIcon(@Nullable Drawable icon) { mIcon = icon; updateView(); return this; } /** * Set the icon displayed on this tab. * * @param resId Demo resource ID referring to the icon that should be displayed * @return The current instance for call chaining */ @NonNull public Tab setIcon(@DrawableRes int resId) { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId)); } /** * Set the text displayed on this tab. Text may be truncated if there is not room to display * the entire string. * * @param text The text to display * @return The current instance for call chaining */ @NonNull public Tab setText(@Nullable CharSequence text) { mText = text; updateView(); return this; } /** * Set the text displayed on this tab. Text may be truncated if there is not room to display * the entire string. * * @param resId Demo resource ID referring to the text that should be displayed * @return The current instance for call chaining */ @NonNull public Tab setText(@StringRes int resId) { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return setText(mParent.getResources().getText(resId)); } /** * Select this tab. Only valid if the tab has been added to the action bar. */ public void select() { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } mParent.selectTab(this); } /** * Returns true if this tab is currently selected. */ public boolean isSelected() { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return mParent.getSelectedTabPosition() == mPosition; } /** * Set a description of this tab's content for use in accessibility support. If no content * description is provided the title will be used. * * @param resId Demo resource ID referring to the description text * @return The current instance for call chaining * @see #setContentDescription(CharSequence) * @see #getContentDescription() */ @NonNull public Tab setContentDescription(@StringRes int resId) { if (mParent == null) { throw new IllegalArgumentException("Tab not attached to a TabLayout"); } return setContentDescription(mParent.getResources().getText(resId)); } /** * Set a description of this tab's content for use in accessibility support. If no content * description is provided the title will be used. * * @param contentDesc Description of this tab's content * @return The current instance for call chaining * @see #setContentDescription(int) * @see #getContentDescription() */ @NonNull public Tab setContentDescription(@Nullable CharSequence contentDesc) { mContentDesc = contentDesc; updateView(); return this; } /** * Gets a brief description of this tab's content for use in accessibility support. * * @return Description of this tab's content * @see #setContentDescription(CharSequence) * @see #setContentDescription(int) */ @Nullable public CharSequence getContentDescription() { return mContentDesc; } void updateView() { if (mView != null) { mView.update(); } } void reset() { mParent = null; mView = null; mTag = null; mIcon = null; mText = null; mContentDesc = null; mPosition = INVALID_POSITION; mCustomView = null; } } class TabView extends LinearLayout { private Tab mTab; private TextView mTextView; private ImageView mIconView; private View mCustomView; private TextView mCustomTextView; private ImageView mCustomIconView; private int mDefaultMaxLines = 2; public TabView(Context context) { super(context); if (mTabBackgroundResId != 0) { ViewCompat.setBackground( this, AppCompatResources.getDrawable(context, mTabBackgroundResId)); } ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, mTabPaddingEnd, mTabPaddingBottom); setGravity(Gravity.CENTER); setOrientation(VERTICAL); setClickable(true); ViewCompat.setPointerIcon(this, PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND)); } @Override public boolean performClick() { final boolean handled = super.performClick(); if (mTab != null) { if (!handled) { playSoundEffect(SoundEffectConstants.CLICK); } mTab.select(); return true; } else { return handled; } } @Override public void setSelected(final boolean selected) { final boolean changed = isSelected() != selected; super.setSelected(selected); if (changed && selected && Build.VERSION.SDK_INT < 16) { // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } // Always dispatch this to the child views, regardless of whether the value has // changed if (mTextView != null) { mTextView.setSelected(selected); } if (mIconView != null) { mIconView.setSelected(selected); } if (mCustomView != null) { mCustomView.setSelected(selected); } } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); // This view masquerades as an action bar tab. event.setClassName(ActionBar.Tab.class.getName()); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); // This view masquerades as an action bar tab. info.setClassName(ActionBar.Tab.class.getName()); } @Override public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); final int maxWidth = getTabMaxWidth(); final int widthMeasureSpec; final int heightMeasureSpec = origHeightMeasureSpec; if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED || specWidthSize > maxWidth)) { // If we have a max width and a given spec which is either unspecified or // larger than the max width, update the width spec using the same mode widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST); } else { // Else, use the original width spec widthMeasureSpec = origWidthMeasureSpec; } // Now lets measure super.onMeasure(widthMeasureSpec, heightMeasureSpec); // We need to switch the text size based on whether the text is spanning 2 lines or not if (mTextView != null) { final Resources res = getResources(); float textSize = mTabTextSize; int maxLines = mDefaultMaxLines; if (mIconView != null && mIconView.getVisibility() == VISIBLE) { // If the icon view is being displayed, we limit the text to 1 line maxLines = 1; } else if (mTextView != null && mTextView.getLineCount() > 1) { // Otherwise when we have text which wraps we reduce the text size textSize = mTabTextMultiLineSize; } final float curTextSize = mTextView.getTextSize(); final int curLineCount = mTextView.getLineCount(); final int curMaxLines = TextViewCompat.getMaxLines(mTextView); if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { // We've got a new text size and/or max lines... boolean updateTextView = true; if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { // If we're in fixed mode, going up in text size and currently have 1 line // then it's very easy to get into an infinite recursion. // To combat that we check to see if the change in text size // will cause a line count change. If so, abort the size change and stick // to the smaller size. final Layout layout = mTextView.getLayout(); if (layout == null || approximateLineWidth(layout, 0, textSize) > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) { updateTextView = false; } } if (updateTextView) { mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); mTextView.setMaxLines(maxLines); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } } } void setTab(@Nullable final Tab tab) { if (tab != mTab) { mTab = tab; update(); } } void reset() { setTab(null); setSelected(false); } final void update() { final Tab tab = mTab; final View custom = tab != null ? tab.getCustomView() : null; if (custom != null) { final ViewParent customParent = custom.getParent(); if (customParent != this) { if (customParent != null) { ((ViewGroup) customParent).removeView(custom); } addView(custom); } mCustomView = custom; if (mTextView != null) { mTextView.setVisibility(GONE); } if (mIconView != null) { mIconView.setVisibility(GONE); mIconView.setImageDrawable(null); } mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); if (mCustomTextView != null) { mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); } mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); } else { // We do not have a custom view. Remove one if it already exists if (mCustomView != null) { removeView(mCustomView); mCustomView = null; } mCustomTextView = null; mCustomIconView = null; } if (mCustomView == null) { // If there isn't a custom view, we'll us our own in-built layouts if (mIconView == null) { ImageView iconView = (ImageView) LayoutInflater.from(getContext()) .inflate(R.layout.design_layout_tab_icon, this, false); addView(iconView, 0); mIconView = iconView; } if (mTextView == null) { TextView textView = (TextView) LayoutInflater.from(getContext()) .inflate(R.layout.design_layout_tab_text, this, false); addView(textView); mTextView = textView; mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); } TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance); if (mTabTextColors != null) { mTextView.setTextColor(mTabTextColors); } updateTextAndIcon(mTextView, mIconView); } else { // Else, we'll see if there is a TextView or ImageView present and update them if (mCustomTextView != null || mCustomIconView != null) { updateTextAndIcon(mCustomTextView, mCustomIconView); } } // Finally update our selected state setSelected(tab != null && tab.isSelected()); } private void updateTextAndIcon(@Nullable final TextView textView, @Nullable final ImageView iconView) { final Drawable icon = mTab != null ? mTab.getIcon() : null; final CharSequence text = mTab != null ? mTab.getText() : null; final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null; if (iconView != null) { if (icon != null) { iconView.setImageDrawable(icon); iconView.setVisibility(VISIBLE); setVisibility(VISIBLE); } else { iconView.setVisibility(GONE); iconView.setImageDrawable(null); } iconView.setContentDescription(contentDesc); } final boolean hasText = !TextUtils.isEmpty(text); if (textView != null) { if (hasText) { textView.setText(text); textView.setVisibility(VISIBLE); setVisibility(VISIBLE); } else { textView.setVisibility(GONE); textView.setText(null); } textView.setContentDescription(contentDesc); } if (iconView != null) { MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); int bottomMargin = 0; if (hasText && iconView.getVisibility() == VISIBLE) { // If we're showing both text and icon, add some margin bottom to the icon bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); } if (bottomMargin != lp.bottomMargin) { lp.bottomMargin = bottomMargin; iconView.requestLayout(); } } TooltipCompat.setTooltipText(this, hasText ? null : contentDesc); } public Tab getTab() { return mTab; } /** * Approximates a given lines width with the new provided text size. */ private float approximateLineWidth(Layout layout, int line, float textSize) { return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); } } private class SlidingTabStrip extends LinearLayout { private int mSelectedIndicatorHeight; private final Paint mSelectedIndicatorPaint; int mSelectedPosition = -1; float mSelectionOffset; private int mLayoutDirection = -1; private int mIndicatorLeft = -1; private int mIndicatorRight = -1; private ValueAnimator mIndicatorAnimator; SlidingTabStrip(Context context) { super(context); setWillNotDraw(false); mSelectedIndicatorPaint = new Paint(); } void setSelectedIndicatorColor(int color) { if (mSelectedIndicatorPaint.getColor() != color) { mSelectedIndicatorPaint.setColor(color); ViewCompat.postInvalidateOnAnimation(this); } } void setSelectedIndicatorHeight(int height) { if (mSelectedIndicatorHeight != height) { mSelectedIndicatorHeight = height; ViewCompat.postInvalidateOnAnimation(this); } } boolean childrenNeedLayout() { for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); if (child.getWidth() <= 0) { return true; } } return false; } void setIndicatorPositionFromTabPosition(int position, float positionOffset) { if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } mSelectedPosition = position; mSelectionOffset = positionOffset; updateIndicatorPosition(); } float getIndicatorPosition() { return mSelectedPosition + mSelectionOffset; } @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); // Workaround for a bug before Android M where LinearLayout did not relayout itself when // layout direction changed. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { //noinspection WrongConstant if (mLayoutDirection != layoutDirection) { requestLayout(); mLayoutDirection = layoutDirection; } } } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { // HorizontalScrollView will first measure use with UNSPECIFIED, and then with // EXACTLY. Ignore the first call since anything we do will be overwritten anyway return; } if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { final int count = getChildCount(); // First we'll find the widest tab int largestTabWidth = 0; for (int i = 0, z = count; i < z; i++) { View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); } } if (largestTabWidth <= 0) { // If we don't have a largest child yet, skip until the next measure pass return; } final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); boolean remeasure = false; if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { // If the tabs fit within our width minus gutters, we will set all tabs to have // the same width for (int i = 0; i < count; i++) { final LinearLayout.LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); if (lp.width != largestTabWidth || lp.weight != 0) { lp.width = largestTabWidth; lp.weight = 0; remeasure = true; } } } else { // If the tabs will wrap to be larger than the width minus gutters, we need // to switch to GRAVITY_FILL mTabGravity = GRAVITY_FILL; updateTabViews(false); remeasure = true; } if (remeasure) { // Now re-measure after our changes 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); if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { // If we're currently running an animation, lets cancel it and start a // new animation with the remaining duration mIndicatorAnimator.cancel(); final long duration = mIndicatorAnimator.getDuration(); animateIndicatorToPosition(mSelectedPosition, Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration)); } else { // If we've been layed out, update the indicator position updateIndicatorPosition(); } } private void updateIndicatorPosition() { final View selectedTitle = getChildAt(mSelectedPosition); int left, right; if (selectedTitle != null && selectedTitle.getWidth() > 0) { left = selectedTitle.getLeft(); right = selectedTitle.getRight(); if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { // Draw the selection partway between the tabs View nextTitle = getChildAt(mSelectedPosition + 1); left = (int) (mSelectionOffset * nextTitle.getLeft() + (1.0f - mSelectionOffset) * left); right = (int) (mSelectionOffset * nextTitle.getRight() + (1.0f - mSelectionOffset) * right); } } else { left = right = -1; } setIndicatorPosition(left, right); } void setIndicatorPosition(int left, int right) { if (left != mIndicatorLeft || right != mIndicatorRight) { // If the indicator's left/right has changed, invalidate mIndicatorLeft = left; mIndicatorRight = right; ViewCompat.postInvalidateOnAnimation(this); } } void animateIndicatorToPosition(final int position, int duration) { if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { mIndicatorAnimator.cancel(); } final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; final View targetView = getChildAt(position); if (targetView == null) { // If we don't have a view, just update the position now and return updateIndicatorPosition(); return; } final int targetLeft = targetView.getLeft(); final int targetRight = targetView.getRight(); final int startLeft; final int startRight; if (Math.abs(position - mSelectedPosition) <= 1) { // If the views are adjacent, we'll animate from edge-to-edge startLeft = mIndicatorLeft; startRight = mIndicatorRight; } else { // Else, we'll just grow from the nearest edge final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); if (position < mSelectedPosition) { // We're going end-to-start if (isRtl) { startLeft = startRight = targetLeft - offset; } else { startLeft = startRight = targetRight + offset; } } else { // We're going start-to-end if (isRtl) { startLeft = startRight = targetRight + offset; } else { startLeft = startRight = targetLeft - offset; } } } if (startLeft != targetLeft || startRight != targetRight) { ValueAnimator animator = mIndicatorAnimator = new ValueAnimator(); animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); animator.setDuration(duration); animator.setFloatValues(0, 1); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { final float fraction = animator.getAnimatedFraction(); setIndicatorPosition( AnimationUtils.lerp(startLeft, targetLeft, fraction), AnimationUtils.lerp(startRight, targetRight, fraction)); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { mSelectedPosition = position; mSelectionOffset = 0f; } }); animator.start(); } } @Override public void draw(Canvas canvas) { super.draw(canvas); // 选中的位置添加下划线 下划线宽度为其总宽度的8分之一 int width = mIndicatorRight - mIndicatorLeft; int indicatorWidth = width / 8; int indicatorLeft = mIndicatorLeft + (width - indicatorWidth) / 2; int indicatorRight = mIndicatorRight - (width / 2) + indicatorWidth / 2; if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) { canvas.drawRect(indicatorLeft, getHeight() - mSelectedIndicatorHeight, indicatorRight, getHeight(), mSelectedIndicatorPaint); } } } private static ColorStateList createColorStateList(int defaultColor, int selectedColor) { final int[][] states = new int[2][]; final int[] colors = new int[2]; int i = 0; states[i] = SELECTED_STATE_SET; colors[i] = selectedColor; i++; // Default enabled state states[i] = EMPTY_STATE_SET; colors[i] = defaultColor; i++; return new ColorStateList(states, colors); } private int getDefaultHeight() { boolean hasIconAndText = false; for (int i = 0, count = mTabs.size(); i < count; i++) { Tab tab = mTabs.get(i); if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { hasIconAndText = true; break; } } return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; } private int getTabMinWidth() { if (mRequestedTabMinWidth != INVALID_WIDTH) { // If we have been given a min width, use it return mRequestedTabMinWidth; } // Else, we'll use the default value return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { // We don't care about the layout params of any views added to us, since we don't actually // add them. The only view we add is the SlidingTabStrip, which is done manually. // We return the default layout params so that we don't blow up if we're given a TabItem // without android:layout_* values. return generateDefaultLayoutParams(); } int getTabMaxWidth() { return mTabMaxWidth; } /** * Demo {@link ViewPager.OnPageChangeListener} class which contains the * necessary calls back to the provided {@link TabLayout} so that the tab position is * kept in sync. *

*

This class stores the provided TabLayout weakly, meaning that you can use * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and * not cause a leak. */ public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { private final WeakReference mTabLayoutRef; private int mPreviousScrollState; private int mScrollState; public TabLayoutOnPageChangeListener(ShortTabLayout tabLayout) { mTabLayoutRef = new WeakReference<>(tabLayout); } @Override public void onPageScrollStateChanged(final int state) { mPreviousScrollState = mScrollState; mScrollState = state; } @Override public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { final ShortTabLayout tabLayout = mTabLayoutRef.get(); if (tabLayout != null) { // Only update the text selection if we're not settling, or we are settling after // being dragged final boolean updateText = mScrollState != SCROLL_STATE_SETTLING || mPreviousScrollState == SCROLL_STATE_DRAGGING; // Update the indicator if we're not settling after being idle. This is caused // from a setCurrentItem() call and will be handled by an animation from // onPageSelected() instead. final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE); tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator); } } @Override public void onPageSelected(final int position) { final ShortTabLayout tabLayout = mTabLayoutRef.get(); if (tabLayout != null && tabLayout.getSelectedTabPosition() != position && position < tabLayout.getTabCount()) { // Select the tab, only updating the indicator if we're not being dragged/settled // (since onPageScrolled will handle that). final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE || (mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE); tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator); } } void reset() { mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; } } /** * Demo {@link OnTabSelectedListener} class which contains the necessary calls back * to the provided {@link ViewPager} so that the tab position is kept in sync. */ public static class ViewPagerOnTabSelectedListener implements OnTabSelectedListener { private final ViewPager mViewPager; public ViewPagerOnTabSelectedListener(ViewPager viewPager) { mViewPager = viewPager; } @Override public void onTabSelected(Tab tab) { mViewPager.setCurrentItem(tab.getPosition()); } @Override public void onTabUnselected(Tab tab) { // No-op } @Override public void onTabReselected(Tab tab) { // No-op } } private class PagerAdapterObserver extends DataSetObserver { PagerAdapterObserver() { } @Override public void onChanged() { populateFromPagerAdapter(); } @Override public void onInvalidated() { populateFromPagerAdapter(); } } private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener { private boolean mAutoRefresh; AdapterChangeListener() { } @Override public void onAdapterChanged(@NonNull ViewPager viewPager, @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) { if (mViewPager == viewPager) { setPagerAdapter(newAdapter, mAutoRefresh); } } void setAutoRefresh(boolean autoRefresh) { mAutoRefresh = autoRefresh; } } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/tab/TabItem.java ================================================ package com.jennifer.andy.simpleeyes.widget.tab; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.appcompat.widget.TintTypedArray; import android.util.AttributeSet; import android.view.View; import com.jennifer.andy.simpleeyes.R; /** * Author: andy.xwt * Date: 2018/7/4 13:49 * Description: */ public final class TabItem extends View { final CharSequence mText; final Drawable mIcon; final int mCustomLayout; public TabItem(Context context) { this(context, null); } @SuppressLint("RestrictedApi") public TabItem(Context context, AttributeSet attrs) { super(context, attrs); @SuppressLint("RestrictedApi") final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.TabItem); mText = a.getText(R.styleable.TabItem_android_text); mIcon = a.getDrawable(R.styleable.TabItem_android_icon); mCustomLayout = a.getResourceId(R.styleable.TabItem_android_layout, 0); a.recycle(); } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/viewpager/InterceptVerticalViewPager.kt ================================================ package com.jennifer.andy.simpleeyes.widget.viewpager import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.view.ViewConfiguration import androidx.viewpager.widget.ViewPager import kotlin.math.abs /** * Author: andy.xwt * Date: 2018/6/14 16:03 * Description: * 该ViewPager主要拦截两个情况: * 1.拦截向上的滑动 * 2.拦截固定角标的向左滑动 * */ class InterceptVerticalViewPager : ViewPager { private var mLastMotionX: Float = 0f private var mLastMotionY: Float = 0f lateinit var verticalListener: () -> Unit lateinit var horizontalListener: (Int) -> Unit private var isDoListener: Boolean = false var mDisMissIndex = -1 private val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop constructor (context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) override fun dispatchTouchEvent(ev: MotionEvent): Boolean { val action = ev.action and MotionEvent.ACTION_MASK val y = ev.y val x = ev.x when (action) { MotionEvent.ACTION_DOWN -> { mLastMotionX = ev.x mLastMotionY = ev.y } MotionEvent.ACTION_MOVE -> { val dx = mLastMotionX - x val dy = mLastMotionY - y val absX = abs(dx) val absY = abs(dy) //向上滑动,且y轴坐标的距离大于x轴滑动距离的一半 if (dy > 0 && absY > absX * 0.5f && absY > scaledTouchSlop) { if (!isDoListener) { verticalListener() isDoListener = true return false } } //像左滑动,且x轴坐标的距离大于y轴滑动距离的一半 if (dx > 0 && absX > absY * 0.5f && absX > scaledTouchSlop) { if (!isDoListener && mDisMissIndex == currentItem) { horizontalListener(mDisMissIndex) isDoListener = true return false } } mLastMotionX = x mLastMotionY = y } } return super.dispatchTouchEvent(ev) } } ================================================ FILE: app/src/main/java/com/jennifer/andy/simpleeyes/widget/viewpager/MarginWithIndicatorViewPager.kt ================================================ package com.jennifer.andy.simpleeyes.widget.viewpager import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.viewpager.widget.PagerAdapter import androidx.viewpager.widget.ViewPager import com.jennifer.andy.simpleeyes.R import com.jennifer.andy.simpleeyes.entity.Content import com.jennifer.andy.simpleeyes.utils.bindView import com.jennifer.andy.simpleeyes.utils.dip2px import com.jennifer.andy.simpleeyes.widget.CollectionOfHorizontalScrollCardView import com.rd.PageIndicatorView /** * Author: andy.xwt * Date: 2018/7/9 17:11 * Description: 带分割(可以看见前面视图的)与指示器的ViewPager */ class MarginWithIndicatorViewPager : FrameLayout { private val mViewPager: ViewPager by bindView(R.id.vp_indicator_pager) private val mIndicator: PageIndicatorView by bindView(R.id.pageIndicatorView) private lateinit var mItemList: MutableList lateinit var pageViewClickListener: (position: Int) -> Unit constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { LayoutInflater.from(context).inflate(R.layout.layout_margin_with_indicator_pager, this, true) } /** * 设置数据 */ fun setData(itemList: MutableList) { mItemList = itemList setAdapter() } private fun setAdapter() { mViewPager.adapter = MarginWithViewPagerAdapter() mViewPager.offscreenPageLimit = 3 mViewPager.pageMargin = context.dip2px(10f) mViewPager.currentItem = 10000 * mItemList.size mIndicator.count = mItemList.size mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int) { } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { mIndicator.setSelected(position % mItemList.size) } override fun onPageSelected(position: Int) { } }) } /** * 设置无限滚动适配器 */ inner class MarginWithViewPagerAdapter : PagerAdapter() { override fun getCount() = Int.MAX_VALUE override fun instantiateItem(container: ViewGroup, position: Int): Any { val cardView = CollectionOfHorizontalScrollCardView(this@MarginWithIndicatorViewPager.context) val newPosition = position % mItemList.size cardView.setOnClickListener { pageViewClickListener(newPosition) }//点击响应事件 cardView.setData(mItemList[newPosition]) container.addView(cardView) return cardView } override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { container.removeView(`object` as View?) } override fun isViewFromObject(view: View, `object`: Any): Boolean { return view == `object` } } } ================================================ FILE: app/src/main/res/anim/bottom_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/bottom_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/fade_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/fade_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/left_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/left_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/no_anim.xml ================================================ ================================================ FILE: app/src/main/res/anim/right_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/right_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/scale_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/scale_out.xml ================================================ ================================================ FILE: app/src/main/res/anim/top_in.xml ================================================ ================================================ FILE: app/src/main/res/anim/top_out.xml ================================================ ================================================ FILE: app/src/main/res/color/selector_item_square_text.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/seek_bar_layer.xml ================================================ ================================================ FILE: app/src/main/res/drawable/selector_checkbox_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/selector_item_square_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_black_border.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_border_bottom.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_border_bottom_top.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_hot_search_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_indicator_selected.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_indicator_unselected.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_share_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_show_all_border.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_translate_border.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_video_detail_placeholder.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_webview_scrollbar.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_white_border.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_all_author.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_all_category.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_author_tag_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_category_tab.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_common_recyclerview.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_daily_elite.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_landing.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_login.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_rank.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_tag.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_topic.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_video_detail.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_videoinfo_by_id.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_webview.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_light_controller.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_volume_controller.xml ================================================ ================================================ FILE: app/src/main/res/layout/empty_search_word.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_cache.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_feed.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_follow.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_home.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_local_coomon_landing.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_profile.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_search_hot.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_slogan.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_tag_detail_info.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_video_landing.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_collection_brief.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_collection_card_cover.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_collection_of_horizontal_scroll_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_hot_search.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_profile_setting.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_square_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_square_collection.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_the_end.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_video_samll_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_video_text_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_author_tag_detail_header.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_blank_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_bottom_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_brife_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_card_banner.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_category_head_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_category_tab_toolbar.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_center_title_share_toolbar.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_choiceness.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_collection_of_horizontal_scroll_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_collection_with_brief.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_collection_with_cover.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_common_text.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_common_toolbar.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_division_line.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_follow_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_horizontal_scroll_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_ijk_wrapper.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_left_title_share_toolbar.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_load_more_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_loading_message.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_loading_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_margin_with_indicator_pager.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_media_controller_full_screen.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_media_controller_tiny.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_search_hot_remind_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_single_text.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_single_video.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_square_collection.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_video_author_head.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_video_detail_head.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_video_error.xml ================================================ ================================================ FILE: app/src/main/res/layout/layout_video_small_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/refresh_category_header.xml ================================================ ================================================ FILE: app/src/main/res/layout/refresh_daily_elite_header.xml ================================================ ================================================ FILE: app/src/main/res/layout-v21/layout_square_collection.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/attrs.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #FEFFFE #000100 #61EECB #757575 #FFFFFF #FEFFFE #f2ffffff #e6ffffff #d9ffffff #ccffffff #bfffffff #b3ffffff #a6ffffff #99ffffff #8cffffff #80ffffff #73ffffff #66ffffff #59ffffff #4dffffff #40ffffff #33ffffff #26ffffff #0dffffff #000000 #4D000000 #33000000 #f2000000 #e6000000 #d9000000 #cc000000 #bf000000 #b3000000 #a6000000 #99000000 #8c000000 #80000000 #73000000 #66000000 #59000000 #4d000000 #40000000 #33000000 #26000000 #0d000000 #444444 #1E1F1D #66A2A2A2 #00000000 #BBBBBB #828885 #66A2A2A2 #EDEEEC #B7B9B8 #EFF0EF ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 15dp 15dp 15dp 15dp 15dp 15dp 15dp 200dp 40dp 40dp 48dp ================================================ FILE: app/src/main/res/values/ids_sticky_nav_layout.xml ================================================ ================================================ FILE: app/src/main/res/values/strings.xml ================================================ SimpleEyes 首页 发现 关注 我的 网络错误\n点击屏幕重试 没有数据\n点击屏幕重试 查看全部 全部分类 全部作者 全部排行 广告 Andy精选 更多开眼编辑精选 -The End- 正在加载中… Invalid progressive playback Unknown OK Aspect / Fit parent Aspect / Fill parent Aspect / Wrap item Free / Fill parent 16:9 / Fit parent 4:3 / Fit parent N/A 开眼 Eyepetizer >]]> for \n Today Daily appetizes for your eyes.Bon eyepetit. 每日精选视频推介,让你大开眼界。 缓存 + 关注 网络错误或无连接,点击屏幕重试 帮你找到感兴趣的视频 取消 输入标题或描述中的关键字找到更多视频 - 热门搜索词 - 可以通过所属分类\n以及标题或描述中的关键词来查找 很抱歉没有找到相匹配的内容 每日编辑精选 加载更多失败 今日日报已更新 Discover Subscription 登录后即可关注作者、\n发表评论、同步收藏视频和播放记录 点击登录即可发表评论及同步已收藏视频 评论 收藏 Version 3.8.1.2.216 登录或注册及同意开眼用户服务协议 我的缓存 编辑 每日编辑精选,一如既往 关注越多,发现越多 离线自动缓存,精彩永不下线 登录即可订阅、评论和同步已收藏视频 我的消息 我的关注 我的缓存 观看记录 意见反馈 我要投稿 Daily appetizers for your eyes,as always Subscribe more,discover a whole lot more Enjoy your daily eyepetit,even without connectivity Sign in to comment &collect videos,to subscript also ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: app/src/test/java/com/jennifer/andy/simpleeyes/ExampleUnitTest.kt ================================================ package com.jennifer.andy.simpleeyes import org.junit.Assert.assertEquals import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.3.11' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() maven { url "https://jitpack.io" } } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Mar 11 21:37:12 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto initData echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto initData echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :initData @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ include ':app'