From 8a15389635444e9502ca348aea2f955161f7c9c8 Mon Sep 17 00:00:00 2001 From: tuhui <470666774@qq.com> Date: Fri, 8 Sep 2017 14:18:13 +0800 Subject: [PATCH] Initial commit --- .gitignore | 9 + .idea/compiler.xml | 22 + .idea/copyright/profiles_settings.xml | 3 + .idea/gradle.xml | 18 + .idea/misc.xml | 46 + .idea/modules.xml | 9 + .idea/runConfigurations.xml | 12 + build.gradle | 31 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 ++ gradlew.bat | 90 + settings.gradle | 1 + widget/.gitignore | 1 + widget/build.gradle | 94 + widget/proguard-rules.pro | 17 + .../widget/ExampleInstrumentedTest.java | 26 + widget/src/main/AndroidManifest.xml | 8 + .../com/hzecool/widget/ActionSheetDialog.java | 510 ++++ .../com/hzecool/widget/BalanceEditText.java | 80 + .../com/hzecool/widget/BalanceTextView.java | 93 + .../hzecool/widget/BottomNavigationView.java | 877 +++++++ .../com/hzecool/widget/CheckImageView.java | 72 + .../com/hzecool/widget/ClearableEditText.java | 120 + .../CountDownButton/CountDownButton.java | 214 ++ .../java/com/hzecool/widget/FlowLayout.java | 200 ++ .../hzecool/widget/RecycleViewDivider.java | 148 ++ .../java/com/hzecool/widget/WaveSideBar.java | 398 +++ .../java/com/hzecool/widget/WaveView.java | 166 ++ .../hzecool/widget/badgeview/BadgeView.java | 221 ++ .../circleimageview/CircleImageView.java | 440 ++++ .../widget/colorpicker/ColorPickerView.java | 187 ++ .../imgselector/MultiImageSelector.java | 117 + .../MultiImageSelectorActivity.java | 182 ++ .../MultiImageSelectorFragment.java | 567 ++++ .../imgselector/adapter/FolderAdapter.java | 178 ++ .../imgselector/adapter/ImageGridAdapter.java | 243 ++ .../widget/imgselector/bean/Folder.java | 27 + .../widget/imgselector/bean/Image.java | 30 + .../widget/imgselector/utils/FileUtils.java | 129 + .../widget/imgselector/utils/ScreenUtils.java | 29 + .../widget/imgselector/utils/TimeUtils.java | 31 + .../imgselector/view/SquareFrameLayout.java | 24 + .../imgselector/view/SquaredImageView.java | 24 + .../widget/loadingLayout/LoadingLayout.java | 685 +++++ .../loadingLayout/LoadingLayoutUtils.java | 48 + .../materialdialog/DefaultRvAdapter.java | 210 ++ .../widget/materialdialog/DialogAction.java | 10 + .../widget/materialdialog/DialogBase.java | 71 + .../widget/materialdialog/DialogInit.java | 554 ++++ .../widget/materialdialog/GravityEnum.java | 40 + .../widget/materialdialog/MaterialDialog.java | 2270 +++++++++++++++++ .../materialdialog/StackingBehavior.java | 19 + .../hzecool/widget/materialdialog/Theme.java | 8 + .../materialdialog/internal/MDAdapter.java | 12 + .../materialdialog/internal/MDButton.java | 97 + .../materialdialog/internal/MDRootLayout.java | 631 +++++ .../materialdialog/internal/MDTintHelper.java | 194 ++ .../internal/ThemeSingleton.java | 62 + .../materialdialog/util/DialogUtils.java | 305 +++ .../materialdialog/util/RippleHelper.java | 21 + .../materialdialog/util/TypefaceHelper.java | 47 + .../ninegridview/ColorFilterImageView.java | 51 + .../widget/ninegridview/DisplayUtils.java | 260 ++ .../widget/ninegridview/MultiImageView.java | 214 ++ .../widget/popwindowlist/PopAdapter.java | 27 + .../widget/popwindowlist/RecyclerViewPop.java | 81 + .../com/hzecool/widget/puzzle/AppUtil.java | 23 + .../hzecool/widget/puzzle/Coordinates.java | 32 + .../hzecool/widget/puzzle/DensityUtil.java | 41 + .../com/hzecool/widget/puzzle/ImageBean.java | 186 ++ .../com/hzecool/widget/puzzle/ImageItem.java | 20 + .../com/hzecool/widget/puzzle/PuzzleView.java | 347 +++ .../com/hzecool/widget/puzzle/SelectItem.java | 29 + .../widget/riseedittext/RiseEditText.java | 161 ++ .../widget/riseedittext/RiseEditTextNum.java | 144 ++ .../widget/risenumber/IRiseNumber.java | 43 + .../widget/risenumber/RiseNumberTextView.java | 219 ++ .../widget/svprogresshud/SVProgressHUD.java | 348 +++ .../SVProgressHUDAnimateUtil.java | 25 + .../listener/OnDismissListener.java | 10 + .../view/SVCircleProgressBar.java | 195 ++ .../view/SVProgressDefaultView.java | 126 + .../widget/swipdelete/SwipeMenuLayout.java | 621 +++++ .../treeview/holder/SimpleViewHolder.java | 29 + .../widget/treeview/model/TreeNode.java | 284 +++ .../widget/treeview/view/AndroidTreeView.java | 486 ++++ .../treeview/view/TreeNodeWrapperView.java | 53 + .../widget/treeview/view/TwoDScrollView.java | 1107 ++++++++ .../hzecool/widget/utils/GlideSetting.java | 22 + .../com/hzecool/widget/utils/ImageUtils.java | 146 ++ .../com/hzecool/widget/utils/SizeUtils.java | 179 ++ .../com/hzecool/widget/utils/UpLoadUtils.java | 101 + .../AppBarStateChangeListener.java | 40 + .../xRecyclerView/ArrowRefreshHeader.java | 268 ++ .../xRecyclerView/BaseRefreshHeader.java | 19 + .../xRecyclerView/ItemTouchHelperAdapter.java | 39 + .../widget/xRecyclerView/JellyView.java | 108 + .../xRecyclerView/LoadingMoreFooter.java | 110 + .../widget/xRecyclerView/ProgressStyle.java | 36 + .../xRecyclerView/RecyclerViewEmptyView.java | 92 + .../SimpleItemTouchHelperCallback.java | 87 + .../xRecyclerView/SimpleViewSwitcher.java | 60 + .../widget/xRecyclerView/XRecyclerView.java | 727 ++++++ .../AVLoadingIndicatorView.java | 365 +++ .../indicator/BallBeatIndicator.java | 82 + .../indicator/BallClipRotateIndicator.java | 65 + .../BallClipRotateMultipleIndicator.java | 85 + .../BallClipRotatePulseIndicator.java | 94 + .../indicator/BallGridBeatIndicator.java | 76 + .../indicator/BallGridPulseIndicator.java | 103 + .../indicator/BallPulseIndicator.java | 68 + .../indicator/BallPulseRiseIndicator.java | 41 + .../indicator/BallPulseSyncIndicator.java | 57 + .../indicator/BallRotateIndicator.java | 71 + .../indicator/BallScaleIndicator.java | 63 + .../indicator/BallScaleMultipleIndicator.java | 70 + .../indicator/BallScaleRippleIndicator.java | 59 + .../BallScaleRippleMultipleIndicator.java | 65 + .../BallSpinFadeLoaderIndicator.java | 116 + .../indicator/BallTrianglePathIndicator.java | 82 + .../indicator/BallZigZagDeflectIndicator.java | 65 + .../indicator/BallZigZagIndicator.java | 75 + .../indicator/BaseIndicatorController.java | 100 + .../indicator/CubeTransitionIndicator.java | 110 + .../indicator/LineScaleIndicator.java | 62 + .../indicator/LineScalePartyIndicator.java | 64 + .../indicator/LineScalePulseOutIndicator.java | 37 + .../LineScalePulseOutRapidIndicator.java | 37 + .../LineSpinFadeLoaderIndicator.java | 30 + .../indicator/PacmanIndicator.java | 119 + .../indicator/SemiCircleSpinIndicator.java | 36 + .../indicator/SquareSpinIndicator.java | 37 + .../indicator/TriangleSkewSpinIndicator.java | 45 + .../res/anim/action_sheet_dialog_enter.xml | 30 + .../res/anim/action_sheet_dialog_exit.xml | 30 + widget/src/main/res/anim/decelerate_cubic.xml | 3 + .../src/main/res/anim/dialog_enter_anim.xml | 13 + widget/src/main/res/anim/dialog_exit_anim.xml | 13 + widget/src/main/res/anim/hot_pop_enter.xml | 12 + widget/src/main/res/anim/hot_pop_out.xml | 13 + widget/src/main/res/anim/popup_enter.xml | 9 + widget/src/main/res/anim/popup_exit.xml | 9 + widget/src/main/res/anim/svfade_in_center.xml | 18 + .../src/main/res/anim/svfade_out_center.xml | 18 + .../src/main/res/anim/svslide_in_bottom.xml | 11 + widget/src/main/res/anim/svslide_in_top.xml | 11 + .../src/main/res/anim/svslide_out_bottom.xml | 11 + widget/src/main/res/anim/svslide_out_top.xml | 11 + .../main/res/color/mis_default_text_color.xml | 8 + .../main/res/color/mis_folder_text_color.xml | 8 + widget/src/main/res/drawable-hdpi/pickup.png | Bin 0 -> 1922 bytes widget/src/main/res/drawable-hdpi/rgb.png | Bin 0 -> 48014 bytes .../main/res/drawable-xhdpi/divider_bg.9.png | Bin 0 -> 951 bytes widget/src/main/res/drawable-xhdpi/down.png | Bin 0 -> 9980 bytes .../res/drawable-xhdpi/md_btn_selected.xml | 17 + .../drawable-xhdpi/md_btn_selected_dark.xml | 17 + .../res/drawable-xhdpi/md_btn_selector.xml | 8 + .../drawable-xhdpi/md_btn_selector_dark.xml | 8 + .../drawable-xhdpi/md_btn_selector_ripple.xml | 8 + .../md_btn_selector_ripple_dark.xml | 8 + .../main/res/drawable-xhdpi/md_btn_shape.xml | 16 + .../res/drawable-xhdpi/md_item_selected.xml | 5 + .../drawable-xhdpi/md_item_selected_dark.xml | 5 + .../main/res/drawable-xhdpi/md_nav_back.xml | 9 + .../main/res/drawable-xhdpi/md_selector.xml | 8 + .../res/drawable-xhdpi/md_selector_dark.xml | 8 + .../res/drawable-xhdpi/md_transparent.xml | 5 + widget/src/main/res/drawable-xhdpi/up.png | Bin 0 -> 6709 bytes .../main/res/drawable/bg_overlay_gradient.xml | 10 + .../src/main/res/drawable/bg_square_voice.xml | 16 + .../res/drawable/bg_svprogresshuddefault.xml | 7 + widget/src/main/res/drawable/color_cursor.xml | 5 + .../src/main/res/drawable/mis_action_btn.xml | 32 + widget/src/main/res/drawable/progressbar.xml | 16 + .../src/main/res/drawable/progressloading.xml | 5 + widget/src/main/res/drawable/radios.xml | 32 + widget/src/main/res/drawable/rectangle.xml | 30 + .../main/res/drawable/rectangle_pop_bg.xml | 8 + .../res/drawable/selector_btn_back_gray.xml | 24 + widget/src/main/res/drawable/top_radio.xml | 35 + .../layout-ldrtl/md_listitem_multichoice.xml | 38 + .../layout-ldrtl/md_listitem_singlechoice.xml | 38 + .../main/res/layout-v14/md_stub_progress.xml | 56 + .../md_stub_progress_indeterminate.xml | 33 + ...stub_progress_indeterminate_horizontal.xml | 24 + .../layout-v14/mis_fragment_multi_image.xml | 47 + .../src/main/res/layout/edittext_layout.xml | 12 + .../res/layout/layout_action_sheet_dialog.xml | 54 + .../main/res/layout/layout_svprogresshud.xml | 8 + .../src/main/res/layout/listview_footer.xml | 21 + .../src/main/res/layout/listview_header.xml | 70 + .../src/main/res/layout/md_dialog_basic.xml | 32 + .../main/res/layout/md_dialog_basic_check.xml | 49 + .../src/main/res/layout/md_dialog_custom.xml | 16 + .../src/main/res/layout/md_dialog_input.xml | 71 + .../main/res/layout/md_dialog_input_check.xml | 81 + widget/src/main/res/layout/md_dialog_list.xml | 54 + .../main/res/layout/md_dialog_list_check.xml | 63 + .../main/res/layout/md_dialog_progress.xml | 26 + .../md_dialog_progress_indeterminate.xml | 14 + ...alog_progress_indeterminate_horizontal.xml | 26 + widget/src/main/res/layout/md_listitem.xml | 26 + .../res/layout/md_listitem_multichoice.xml | 39 + .../res/layout/md_listitem_singlechoice.xml | 39 + .../main/res/layout/md_stub_actionbuttons.xml | 23 + .../src/main/res/layout/md_stub_progress.xml | 57 + .../layout/md_stub_progress_indeterminate.xml | 32 + ...stub_progress_indeterminate_horizontal.xml | 24 + .../main/res/layout/md_stub_titleframe.xml | 29 + .../layout/md_stub_titleframe_lesspadding.xml | 29 + .../main/res/layout/mis_activity_default.xml | 40 + .../res/layout/mis_cmp_customer_actionbar.xml | 43 + .../res/layout/mis_fragment_multi_image.xml | 45 + .../main/res/layout/mis_list_item_camera.xml | 19 + .../main/res/layout/mis_list_item_folder.xml | 74 + .../main/res/layout/mis_list_item_image.xml | 30 + .../res/layout/pickerview_custom_options.xml | 80 + .../main/res/layout/pull_to_refresh_head.xml | 73 + .../src/main/res/layout/recyclerview_pop.xml | 16 + widget/src/main/res/layout/rv_pop_item.xml | 15 + .../res/layout/view_svprogressdefault.xml | 52 + .../src/main/res/layout/widget_empty_page.xml | 27 + .../src/main/res/layout/widget_error_page.xml | 39 + .../main/res/layout/widget_loading_page.xml | 16 + .../main/res/layout/widget_nonetwork_page.xml | 39 + widget/src/main/res/mipmap-xhdpi/empty.png | Bin 0 -> 4088 bytes widget/src/main/res/mipmap-xhdpi/error.png | Bin 0 -> 4021 bytes .../src/main/res/mipmap-xhdpi/ic_delete.png | Bin 0 -> 970 bytes .../res/mipmap-xhdpi/ic_loading_rotate.png | Bin 0 -> 444 bytes .../mipmap-xhdpi/ic_pulltorefresh_arrow.png | Bin 0 -> 1456 bytes .../src/main/res/mipmap-xhdpi/loading_01.png | Bin 0 -> 1335 bytes .../src/main/res/mipmap-xhdpi/loading_02.png | Bin 0 -> 1305 bytes .../src/main/res/mipmap-xhdpi/loading_03.png | Bin 0 -> 1282 bytes .../src/main/res/mipmap-xhdpi/loading_04.png | Bin 0 -> 1316 bytes .../src/main/res/mipmap-xhdpi/loading_05.png | Bin 0 -> 1327 bytes .../src/main/res/mipmap-xhdpi/loading_06.png | Bin 0 -> 1294 bytes .../src/main/res/mipmap-xhdpi/loading_07.png | Bin 0 -> 1280 bytes .../src/main/res/mipmap-xhdpi/loading_08.png | Bin 0 -> 1293 bytes .../src/main/res/mipmap-xhdpi/loading_09.png | Bin 0 -> 1296 bytes .../src/main/res/mipmap-xhdpi/loading_10.png | Bin 0 -> 1277 bytes .../src/main/res/mipmap-xhdpi/loading_11.png | Bin 0 -> 1303 bytes .../src/main/res/mipmap-xhdpi/loading_12.png | Bin 0 -> 1309 bytes .../main/res/mipmap-xhdpi/mis_btn_back.png | Bin 0 -> 603 bytes .../res/mipmap-xhdpi/mis_default_check_s.png | Bin 0 -> 642 bytes .../res/mipmap-xhdpi/mis_ic_menu_back.png | Bin 0 -> 1319 bytes .../mipmap-xhdpi/mis_selector_indicator.png | Bin 0 -> 333 bytes .../src/main/res/mipmap-xhdpi/no_network.png | Bin 0 -> 3412 bytes .../res/mipmap-xxhdpi/ic_svstatus_error.png | Bin 0 -> 2315 bytes .../res/mipmap-xxhdpi/ic_svstatus_info.png | Bin 0 -> 4385 bytes .../res/mipmap-xxhdpi/ic_svstatus_loading.png | Bin 0 -> 23247 bytes .../res/mipmap-xxhdpi/ic_svstatus_success.png | Bin 0 -> 1576 bytes widget/src/main/res/mipmap-xxhdpi/mis_asv.png | Bin 0 -> 2759 bytes widget/src/main/res/mipmap-xxhdpi/mis_asy.png | Bin 0 -> 4522 bytes .../res/mipmap-xxhdpi/mis_btn_selected.png | Bin 0 -> 1708 bytes .../res/mipmap-xxhdpi/mis_btn_unselected.png | Bin 0 -> 362 bytes .../res/mipmap-xxhdpi/mis_default_check.png | Bin 0 -> 986 bytes .../res/mipmap-xxhdpi/mis_default_error.png | Bin 0 -> 1041 bytes .../res/mipmap-xxhdpi/mis_text_indicator.png | Bin 0 -> 191 bytes widget/src/main/res/values-sw360dp/bool.xml | 4 + widget/src/main/res/values-sw360dp/dimens.xml | 7 + widget/src/main/res/values-sw480dp/bool.xml | 4 + widget/src/main/res/values-sw480dp/dimens.xml | 7 + .../main/res/values-sw720dp-land/dimens.xml | 6 + widget/src/main/res/values-sw720dp/dimens.xml | 6 + widget/src/main/res/values-v11/styles.xml | 40 + widget/src/main/res/values-v14/styles.xml | 68 + widget/src/main/res/values-v21/styles.xml | 28 + widget/src/main/res/values-zh/strings.xml | 25 + widget/src/main/res/values/attrs.xml | 214 ++ widget/src/main/res/values/bool.xml | 4 + widget/src/main/res/values/color.xml | 37 + widget/src/main/res/values/dimens.xml | 76 + widget/src/main/res/values/ids.xml | 6 + widget/src/main/res/values/integers.xml | 4 + widget/src/main/res/values/public.xml | 5 + widget/src/main/res/values/strings.xml | 40 + widget/src/main/res/values/styles.xml | 172 ++ .../com/hzecool/widget/ExampleUnitTest.java | 17 + 280 files changed, 23815 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 widget/.gitignore create mode 100644 widget/build.gradle create mode 100644 widget/proguard-rules.pro create mode 100644 widget/src/androidTest/java/com/hzecool/widget/ExampleInstrumentedTest.java create mode 100644 widget/src/main/AndroidManifest.xml create mode 100644 widget/src/main/java/com/hzecool/widget/ActionSheetDialog.java create mode 100644 widget/src/main/java/com/hzecool/widget/BalanceEditText.java create mode 100644 widget/src/main/java/com/hzecool/widget/BalanceTextView.java create mode 100644 widget/src/main/java/com/hzecool/widget/BottomNavigationView.java create mode 100644 widget/src/main/java/com/hzecool/widget/CheckImageView.java create mode 100644 widget/src/main/java/com/hzecool/widget/ClearableEditText.java create mode 100644 widget/src/main/java/com/hzecool/widget/CountDownButton/CountDownButton.java create mode 100644 widget/src/main/java/com/hzecool/widget/FlowLayout.java create mode 100644 widget/src/main/java/com/hzecool/widget/RecycleViewDivider.java create mode 100644 widget/src/main/java/com/hzecool/widget/WaveSideBar.java create mode 100644 widget/src/main/java/com/hzecool/widget/WaveView.java create mode 100644 widget/src/main/java/com/hzecool/widget/badgeview/BadgeView.java create mode 100644 widget/src/main/java/com/hzecool/widget/circleimageview/CircleImageView.java create mode 100644 widget/src/main/java/com/hzecool/widget/colorpicker/ColorPickerView.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelector.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorActivity.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorFragment.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/adapter/FolderAdapter.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/adapter/ImageGridAdapter.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/bean/Folder.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/bean/Image.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/utils/FileUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/utils/ScreenUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/utils/TimeUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/view/SquareFrameLayout.java create mode 100644 widget/src/main/java/com/hzecool/widget/imgselector/view/SquaredImageView.java create mode 100644 widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayout.java create mode 100644 widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayoutUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/DefaultRvAdapter.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/DialogAction.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/DialogBase.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/DialogInit.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/GravityEnum.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/MaterialDialog.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/StackingBehavior.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/Theme.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDAdapter.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDButton.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDRootLayout.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDTintHelper.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/internal/ThemeSingleton.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/util/DialogUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/util/RippleHelper.java create mode 100644 widget/src/main/java/com/hzecool/widget/materialdialog/util/TypefaceHelper.java create mode 100644 widget/src/main/java/com/hzecool/widget/ninegridview/ColorFilterImageView.java create mode 100644 widget/src/main/java/com/hzecool/widget/ninegridview/DisplayUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/ninegridview/MultiImageView.java create mode 100644 widget/src/main/java/com/hzecool/widget/popwindowlist/PopAdapter.java create mode 100644 widget/src/main/java/com/hzecool/widget/popwindowlist/RecyclerViewPop.java create mode 100644 widget/src/main/java/com/hzecool/widget/puzzle/AppUtil.java create mode 100644 widget/src/main/java/com/hzecool/widget/puzzle/Coordinates.java create mode 100644 widget/src/main/java/com/hzecool/widget/puzzle/DensityUtil.java create mode 100644 widget/src/main/java/com/hzecool/widget/puzzle/ImageBean.java create mode 100644 widget/src/main/java/com/hzecool/widget/puzzle/ImageItem.java create mode 100644 widget/src/main/java/com/hzecool/widget/puzzle/PuzzleView.java create mode 100644 widget/src/main/java/com/hzecool/widget/puzzle/SelectItem.java create mode 100644 widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditText.java create mode 100644 widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditTextNum.java create mode 100644 widget/src/main/java/com/hzecool/widget/risenumber/IRiseNumber.java create mode 100644 widget/src/main/java/com/hzecool/widget/risenumber/RiseNumberTextView.java create mode 100644 widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUD.java create mode 100644 widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUDAnimateUtil.java create mode 100644 widget/src/main/java/com/hzecool/widget/svprogresshud/listener/OnDismissListener.java create mode 100644 widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVCircleProgressBar.java create mode 100644 widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVProgressDefaultView.java create mode 100644 widget/src/main/java/com/hzecool/widget/swipdelete/SwipeMenuLayout.java create mode 100644 widget/src/main/java/com/hzecool/widget/treeview/holder/SimpleViewHolder.java create mode 100644 widget/src/main/java/com/hzecool/widget/treeview/model/TreeNode.java create mode 100644 widget/src/main/java/com/hzecool/widget/treeview/view/AndroidTreeView.java create mode 100644 widget/src/main/java/com/hzecool/widget/treeview/view/TreeNodeWrapperView.java create mode 100644 widget/src/main/java/com/hzecool/widget/treeview/view/TwoDScrollView.java create mode 100644 widget/src/main/java/com/hzecool/widget/utils/GlideSetting.java create mode 100644 widget/src/main/java/com/hzecool/widget/utils/ImageUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/utils/SizeUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/utils/UpLoadUtils.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/AppBarStateChangeListener.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/ArrowRefreshHeader.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/BaseRefreshHeader.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/ItemTouchHelperAdapter.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/JellyView.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/LoadingMoreFooter.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/ProgressStyle.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/RecyclerViewEmptyView.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleItemTouchHelperCallback.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleViewSwitcher.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/XRecyclerView.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/AVLoadingIndicatorView.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallBeatIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateMultipleIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotatePulseIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridBeatIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridPulseIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseRiseIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseSyncIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallRotateIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleMultipleIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleMultipleIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallSpinFadeLoaderIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallTrianglePathIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagDeflectIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BaseIndicatorController.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/CubeTransitionIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScaleIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePartyIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutRapidIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineSpinFadeLoaderIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/PacmanIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SemiCircleSpinIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SquareSpinIndicator.java create mode 100644 widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/TriangleSkewSpinIndicator.java create mode 100644 widget/src/main/res/anim/action_sheet_dialog_enter.xml create mode 100644 widget/src/main/res/anim/action_sheet_dialog_exit.xml create mode 100644 widget/src/main/res/anim/decelerate_cubic.xml create mode 100644 widget/src/main/res/anim/dialog_enter_anim.xml create mode 100644 widget/src/main/res/anim/dialog_exit_anim.xml create mode 100644 widget/src/main/res/anim/hot_pop_enter.xml create mode 100644 widget/src/main/res/anim/hot_pop_out.xml create mode 100644 widget/src/main/res/anim/popup_enter.xml create mode 100644 widget/src/main/res/anim/popup_exit.xml create mode 100644 widget/src/main/res/anim/svfade_in_center.xml create mode 100644 widget/src/main/res/anim/svfade_out_center.xml create mode 100644 widget/src/main/res/anim/svslide_in_bottom.xml create mode 100644 widget/src/main/res/anim/svslide_in_top.xml create mode 100644 widget/src/main/res/anim/svslide_out_bottom.xml create mode 100644 widget/src/main/res/anim/svslide_out_top.xml create mode 100644 widget/src/main/res/color/mis_default_text_color.xml create mode 100644 widget/src/main/res/color/mis_folder_text_color.xml create mode 100644 widget/src/main/res/drawable-hdpi/pickup.png create mode 100644 widget/src/main/res/drawable-hdpi/rgb.png create mode 100644 widget/src/main/res/drawable-xhdpi/divider_bg.9.png create mode 100644 widget/src/main/res/drawable-xhdpi/down.png create mode 100644 widget/src/main/res/drawable-xhdpi/md_btn_selected.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_btn_selected_dark.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_btn_selector.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_btn_selector_dark.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple_dark.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_btn_shape.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_item_selected.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_item_selected_dark.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_nav_back.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_selector.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_selector_dark.xml create mode 100644 widget/src/main/res/drawable-xhdpi/md_transparent.xml create mode 100644 widget/src/main/res/drawable-xhdpi/up.png create mode 100644 widget/src/main/res/drawable/bg_overlay_gradient.xml create mode 100644 widget/src/main/res/drawable/bg_square_voice.xml create mode 100644 widget/src/main/res/drawable/bg_svprogresshuddefault.xml create mode 100644 widget/src/main/res/drawable/color_cursor.xml create mode 100644 widget/src/main/res/drawable/mis_action_btn.xml create mode 100644 widget/src/main/res/drawable/progressbar.xml create mode 100644 widget/src/main/res/drawable/progressloading.xml create mode 100644 widget/src/main/res/drawable/radios.xml create mode 100644 widget/src/main/res/drawable/rectangle.xml create mode 100644 widget/src/main/res/drawable/rectangle_pop_bg.xml create mode 100644 widget/src/main/res/drawable/selector_btn_back_gray.xml create mode 100644 widget/src/main/res/drawable/top_radio.xml create mode 100644 widget/src/main/res/layout-ldrtl/md_listitem_multichoice.xml create mode 100644 widget/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml create mode 100644 widget/src/main/res/layout-v14/md_stub_progress.xml create mode 100644 widget/src/main/res/layout-v14/md_stub_progress_indeterminate.xml create mode 100644 widget/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml create mode 100644 widget/src/main/res/layout-v14/mis_fragment_multi_image.xml create mode 100644 widget/src/main/res/layout/edittext_layout.xml create mode 100644 widget/src/main/res/layout/layout_action_sheet_dialog.xml create mode 100644 widget/src/main/res/layout/layout_svprogresshud.xml create mode 100644 widget/src/main/res/layout/listview_footer.xml create mode 100644 widget/src/main/res/layout/listview_header.xml create mode 100644 widget/src/main/res/layout/md_dialog_basic.xml create mode 100644 widget/src/main/res/layout/md_dialog_basic_check.xml create mode 100644 widget/src/main/res/layout/md_dialog_custom.xml create mode 100644 widget/src/main/res/layout/md_dialog_input.xml create mode 100644 widget/src/main/res/layout/md_dialog_input_check.xml create mode 100644 widget/src/main/res/layout/md_dialog_list.xml create mode 100644 widget/src/main/res/layout/md_dialog_list_check.xml create mode 100644 widget/src/main/res/layout/md_dialog_progress.xml create mode 100644 widget/src/main/res/layout/md_dialog_progress_indeterminate.xml create mode 100644 widget/src/main/res/layout/md_dialog_progress_indeterminate_horizontal.xml create mode 100644 widget/src/main/res/layout/md_listitem.xml create mode 100644 widget/src/main/res/layout/md_listitem_multichoice.xml create mode 100644 widget/src/main/res/layout/md_listitem_singlechoice.xml create mode 100644 widget/src/main/res/layout/md_stub_actionbuttons.xml create mode 100644 widget/src/main/res/layout/md_stub_progress.xml create mode 100644 widget/src/main/res/layout/md_stub_progress_indeterminate.xml create mode 100644 widget/src/main/res/layout/md_stub_progress_indeterminate_horizontal.xml create mode 100644 widget/src/main/res/layout/md_stub_titleframe.xml create mode 100644 widget/src/main/res/layout/md_stub_titleframe_lesspadding.xml create mode 100644 widget/src/main/res/layout/mis_activity_default.xml create mode 100644 widget/src/main/res/layout/mis_cmp_customer_actionbar.xml create mode 100644 widget/src/main/res/layout/mis_fragment_multi_image.xml create mode 100644 widget/src/main/res/layout/mis_list_item_camera.xml create mode 100644 widget/src/main/res/layout/mis_list_item_folder.xml create mode 100644 widget/src/main/res/layout/mis_list_item_image.xml create mode 100644 widget/src/main/res/layout/pickerview_custom_options.xml create mode 100644 widget/src/main/res/layout/pull_to_refresh_head.xml create mode 100644 widget/src/main/res/layout/recyclerview_pop.xml create mode 100644 widget/src/main/res/layout/rv_pop_item.xml create mode 100644 widget/src/main/res/layout/view_svprogressdefault.xml create mode 100644 widget/src/main/res/layout/widget_empty_page.xml create mode 100644 widget/src/main/res/layout/widget_error_page.xml create mode 100644 widget/src/main/res/layout/widget_loading_page.xml create mode 100644 widget/src/main/res/layout/widget_nonetwork_page.xml create mode 100644 widget/src/main/res/mipmap-xhdpi/empty.png create mode 100644 widget/src/main/res/mipmap-xhdpi/error.png create mode 100644 widget/src/main/res/mipmap-xhdpi/ic_delete.png create mode 100644 widget/src/main/res/mipmap-xhdpi/ic_loading_rotate.png create mode 100644 widget/src/main/res/mipmap-xhdpi/ic_pulltorefresh_arrow.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_01.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_02.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_03.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_04.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_05.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_06.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_07.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_08.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_09.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_10.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_11.png create mode 100644 widget/src/main/res/mipmap-xhdpi/loading_12.png create mode 100644 widget/src/main/res/mipmap-xhdpi/mis_btn_back.png create mode 100644 widget/src/main/res/mipmap-xhdpi/mis_default_check_s.png create mode 100644 widget/src/main/res/mipmap-xhdpi/mis_ic_menu_back.png create mode 100644 widget/src/main/res/mipmap-xhdpi/mis_selector_indicator.png create mode 100644 widget/src/main/res/mipmap-xhdpi/no_network.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/ic_svstatus_error.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/ic_svstatus_info.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/ic_svstatus_loading.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/ic_svstatus_success.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/mis_asv.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/mis_asy.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/mis_btn_selected.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/mis_btn_unselected.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/mis_default_check.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/mis_default_error.png create mode 100644 widget/src/main/res/mipmap-xxhdpi/mis_text_indicator.png create mode 100644 widget/src/main/res/values-sw360dp/bool.xml create mode 100644 widget/src/main/res/values-sw360dp/dimens.xml create mode 100644 widget/src/main/res/values-sw480dp/bool.xml create mode 100644 widget/src/main/res/values-sw480dp/dimens.xml create mode 100644 widget/src/main/res/values-sw720dp-land/dimens.xml create mode 100644 widget/src/main/res/values-sw720dp/dimens.xml create mode 100644 widget/src/main/res/values-v11/styles.xml create mode 100644 widget/src/main/res/values-v14/styles.xml create mode 100644 widget/src/main/res/values-v21/styles.xml create mode 100644 widget/src/main/res/values-zh/strings.xml create mode 100644 widget/src/main/res/values/attrs.xml create mode 100644 widget/src/main/res/values/bool.xml create mode 100644 widget/src/main/res/values/color.xml create mode 100644 widget/src/main/res/values/dimens.xml create mode 100644 widget/src/main/res/values/ids.xml create mode 100644 widget/src/main/res/values/integers.xml create mode 100644 widget/src/main/res/values/public.xml create mode 100644 widget/src/main/res/values/strings.xml create mode 100644 widget/src/main/res/values/styles.xml create mode 100644 widget/src/test/java/com/hzecool/widget/ExampleUnitTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..4f9005a --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4c78841 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e24555f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f364207 --- /dev/null +++ b/build.gradle @@ -0,0 +1,31 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'me.tatarka:gradle-retrolambda:3.4.0' + classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1' + classpath 'com.jakewharton:butterknife-gradle-plugin:8.5.1' + } +} + +allprojects { + repositories { + jcenter() + maven { url 'https://jitpack.io' } + mavenCentral() + maven { + url "https://maven.google.com" + } + maven { + url 'https://dl.bintray.com/hpdx/maven/' + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# 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. +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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d166add --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 07 13:57:53 CST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/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 "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 init + +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 init + +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 + +:init +@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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..938b446 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':widget' diff --git a/widget/.gitignore b/widget/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/widget/.gitignore @@ -0,0 +1 @@ +/build diff --git a/widget/build.gradle b/widget/build.gradle new file mode 100644 index 0000000..275a11a --- /dev/null +++ b/widget/build.gradle @@ -0,0 +1,94 @@ +apply plugin: 'com.android.library' +apply plugin: 'me.tatarka.retrolambda' + +android { + compileSdkVersion 26 + buildToolsVersion "26.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + targetCompatibility 1.8 + sourceCompatibility 1.8 + } + + configurations { + all*.exclude module: 'PhotoView' //去除重复依赖库 + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:26.+' + testCompile 'junit:junit:4.12' + + compile 'com.android.support:recyclerview-v7:26.+' + compile 'com.android.support:design:26.+' + + compile 'com.github.Cutta:GifView:1.1' + compile 'com.makeramen:roundedimageview:2.3.0'//圆角 + compile 'com.jude:rollviewpager:1.4.6' + compile 'com.commit451:PhotoView:1.2.4' + compile 'com.davemorrissey.labs:subsampling-scale-image-view:3.6.0' + compile 'com.github.bumptech.glide:glide:4.1.1' + annotationProcessor 'com.github.bumptech.glide:compiler:4.1.1' + //主页底部导航栏 + compile 'com.ashokvarma.android:bottom-navigation-bar:1.3.0' + + //http://www.recyclerview.org/ + compile 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.9' + + compile 'com.contrarywind:Android-PickerView:3.2.4' + + compile 'com.github.johnkil.print:print:1.2.2' + + compile 'me.zhanghai.android.materialprogressbar:library:1.3.0' + + //底部弹出菜单 + //compile 'com.github.Kennyc1012:BottomSheet:2.3.1' + compile 'me.shaohui:bottomdialog:1.1.9' + + compile 'com.jakewharton:butterknife:8.5.1' + //输入框 + compile 'com.mylhyl:circleDialog:2.1.6' + compile 'com.youth.banner:banner:1.4.9' + compile 'com.github.yaozs:ImageShowPicker:1.0.0' + compile 'com.zhihu.android:matisse:0.4.3' + //动图的imageview + compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7' + compile 'com.rengwuxian.materialedittext:library:2.1.4' + //自动大小textview + compile 'me.grantland:autofittextview:0.2.+' + compile 'com.github.pinguo-zhouwei:CustomPopwindow:2.0.0' + //自增textview + compile 'com.github.chaychan:PowerfulViewLibrary:1.2.1' + //侧滑删除 + compile 'com.github.mcxtzhang:SwipeDelMenuLayout:V1.2.2' + //九宫格 + compile 'cn.bingoogolapple:bga-photopicker:1.2.3@aar' + compile 'cn.bingoogolapple:bga-adapter:1.1.8@aar' + //九宫格 + compile 'com.lqr.ninegridimageview:library:1.0.0' + compile 'pub.devrel:easypermissions:0.1.9' + //可以拖动的rv + compile 'com.yanzhenjie:recyclerview-swipe:1.1.1' + //Android在桌面图标上显示角标 + compile 'com.anbetter:badger-helper:1.0.0' +} diff --git a/widget/proguard-rules.pro b/widget/proguard-rules.pro new file mode 100644 index 0000000..41afecb --- /dev/null +++ b/widget/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\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 *; +#} diff --git a/widget/src/androidTest/java/com/hzecool/widget/ExampleInstrumentedTest.java b/widget/src/androidTest/java/com/hzecool/widget/ExampleInstrumentedTest.java new file mode 100644 index 0000000..af59bb1 --- /dev/null +++ b/widget/src/androidTest/java/com/hzecool/widget/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.hzecool.widget.test", appContext.getPackageName()); + } +} diff --git a/widget/src/main/AndroidManifest.xml b/widget/src/main/AndroidManifest.xml new file mode 100644 index 0000000..23a3cce --- /dev/null +++ b/widget/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/ActionSheetDialog.java b/widget/src/main/java/com/hzecool/widget/ActionSheetDialog.java new file mode 100644 index 0000000..788f220 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ActionSheetDialog.java @@ -0,0 +1,510 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.ArrayRes; +import android.support.annotation.StringRes; +import android.support.annotation.StyleRes; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * 底部弹出选择列表框 + * Created by tutu on 2017/4/21. + */ + +public class ActionSheetDialog extends AlertDialog { + private static final String TAG = "ActionSheetDialog"; + + protected ActionSheetDialog(Context context) { + super(context); + } + + protected ActionSheetDialog(Context context, boolean cancelable, OnCancelListener cancelListener) { + super(context, cancelable, cancelListener); + } + + protected ActionSheetDialog(Context context, @StyleRes int themeResId) { + super(context, themeResId); + + } + + @Override + public Button getButton(int whichButton) { + switch (whichButton) { + + } + return super.getButton(whichButton); + } + + public static class ActionSheetBuilder extends AlertDialog.Builder { + private Context mContext; + private String mTitle; + private String mMessage; + private String mNegativeText; + private String mPositiveText; + private boolean mCancelable; + private List mActionSheetItems; + private OnClickListener mNegativeClickListener; + private OnClickListener mPositiveClickListener; + private ActionSheetDialog mActionSheetDialog; + + //Attributes + //Title attrs + private int mTitleTextColor; + private int mTitleTextSize; + private int mTitleHeight; + private Drawable mTitleDivider; + private int mTitleDividerInset; + private int mTitleDividerHeight; + //Message attrs + private int mMessageTextColor; + private int mMessageTextSize; + private int mMessageHeight; + private Drawable mMessageDivider; + private int mMessageDividerInset; + private int mMessageDividerHeight; + //Item attrs + private int mItemTextColor; + private int mItemTextSize; + private int mItemHeight; + private Drawable mItemDivider; + private int mItemDividerInset; + private int mItemDividerHeight; + //Positive button attrs + private int mPositiveTextColor; + private int mPositiveTextSize; + private int mPositiveHeight; + //Cancel button attrs + private int mCancelTextColor; + private int mCancelTextSize; + private int mCancelHeight; + private int mCancelTopMargin; + private Drawable mCancelBackground; + //Dialog attrs + private int mLayoutMargins; + private int mSheetMargins; + private Drawable mContentBackground; + private int mWindowAnimationId; + private static final int DEFAULT_VALUE = -1; + + + public ActionSheetBuilder setmItemTextColor(int mItemTextColor) { + this.mItemTextColor = mItemTextColor; + + return this; + } + + public ActionSheetBuilder(Context context) { + super(context); + mContext = context; + mActionSheetItems = new ArrayList<>(); + TypedArray defaultTypedArray = context.obtainStyledAttributes(R.style.ActionSheetDialogBase, R.styleable.ActionSheetDialog); + if (null != defaultTypedArray) { + initDefaultAttributes(defaultTypedArray); + defaultTypedArray.recycle(); + } + } + + public ActionSheetBuilder(Context context, int themeResId) { + this(context); + TypedArray typedArray = context.obtainStyledAttributes(themeResId, R.styleable.ActionSheetDialog); + if (null != typedArray) { + initAttributes(typedArray); + typedArray.recycle(); + } + + } + + private void initAttributes(TypedArray typedArray) { + if (null != typedArray) { + mTitleTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_titleTextColor, mTitleTextColor); + mTitleTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleTextSize, mTitleTextSize); + mTitleHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleHeight, mTitleHeight); + mTitleDivider = typedArray.getDrawable(R.styleable.ActionSheetDialog_titleDivider); + mTitleDividerInset = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerInset, mTitleDividerInset); + mTitleDividerHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerHeight, mTitleDividerHeight); + + mMessageTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_messageTextColor, mMessageTextColor); + mMessageTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageTextSize, mMessageTextSize); + mMessageHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageHeight, mMessageHeight); + mMessageDivider = typedArray.getDrawable(R.styleable.ActionSheetDialog_messageDivider); + mMessageDividerInset = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerInset, mMessageDividerInset); + mMessageDividerHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerHeight, mMessageDividerHeight); + + mItemTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_itemTextColor, mItemTextColor); + mItemTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemTextSize, mItemTextSize); + mItemHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemHeight, mItemHeight); + mItemDivider = typedArray.getDrawable(R.styleable.ActionSheetDialog_itemDivider); + mItemDividerInset = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerInset, mItemDividerInset); + mItemDividerHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerHeight, mItemDividerHeight); + + mPositiveTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_positiveTextColor, mPositiveTextColor); + mPositiveTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveTextSize, mPositiveTextSize); + mPositiveHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveHeight, mPositiveHeight); + + mCancelTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_cancelTextColor, mCancelTextColor); + mCancelTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTextSize, mCancelTextSize); + mCancelHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelHeight, mCancelHeight); + mCancelBackground = typedArray.getDrawable(R.styleable.ActionSheetDialog_cancelBackground); + mCancelTopMargin = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTopMargins, mCancelTopMargin); + + mSheetMargins = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_sheetMargins, mSheetMargins); + mContentBackground = typedArray.getDrawable(R.styleable.ActionSheetDialog_contentBackground); + mWindowAnimationId = typedArray.getResourceId(R.styleable.ActionSheetDialog_windowAnimations, mWindowAnimationId); + } + } + + private void initDefaultAttributes(TypedArray defaultTypedArray) { + if (null != defaultTypedArray) { + mTitleTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_titleTextColor, DEFAULT_VALUE); + mTitleTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleTextSize, DEFAULT_VALUE); + mTitleHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleHeight, DEFAULT_VALUE); + mTitleDivider = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_titleDivider); + mTitleDividerInset = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerInset, DEFAULT_VALUE); + mTitleDividerHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerHeight, DEFAULT_VALUE); + + mMessageTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_messageTextColor, DEFAULT_VALUE); + mMessageTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageTextSize, DEFAULT_VALUE); + mMessageHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageHeight, DEFAULT_VALUE); + mMessageDivider = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_messageDivider); + mMessageDividerInset = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerInset, DEFAULT_VALUE); + mMessageDividerHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerHeight, DEFAULT_VALUE); + + mItemTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_itemTextColor, DEFAULT_VALUE); + mItemTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemTextSize, DEFAULT_VALUE); + mItemHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemHeight, DEFAULT_VALUE); + mItemDivider = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_itemDivider); + mItemDividerInset = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerInset, DEFAULT_VALUE); + mItemDividerHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerHeight, DEFAULT_VALUE); + + mPositiveTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_positiveTextColor, DEFAULT_VALUE); + mPositiveTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveTextSize, DEFAULT_VALUE); + mPositiveHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveHeight, DEFAULT_VALUE); + + mCancelTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_cancelTextColor, DEFAULT_VALUE); + mCancelTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTextSize, DEFAULT_VALUE); + mCancelHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelHeight, DEFAULT_VALUE); + mCancelBackground = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_cancelBackground); + mCancelTopMargin = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTopMargins, DEFAULT_VALUE); + + mSheetMargins = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_sheetMargins, DEFAULT_VALUE); + mContentBackground = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_contentBackground); + mWindowAnimationId = defaultTypedArray.getResourceId(R.styleable.ActionSheetDialog_windowAnimations, DEFAULT_VALUE); + + } + } + + + @Override + public ActionSheetBuilder setCancelable(boolean cancelable) { + mCancelable = cancelable; + return this; + } + + @Override + public ActionSheetBuilder setMessage(CharSequence message) { + mMessage = (String) message; + return this; + } + + @Override + public ActionSheetBuilder setMessage(@StringRes int messageId) { + mMessage = mContext.getString(messageId); + return this; + } + + @Override + public ActionSheetBuilder setTitle(CharSequence title) { + mTitle = (String) title; + return this; + } + + @Override + public ActionSheetBuilder setTitle(@StringRes int titleId) { + mTitle = mContext.getString(titleId); + return this; + } + + @Override + public ActionSheetBuilder setNegativeButton(CharSequence text, OnClickListener listener) { + mNegativeText = (String) text; + mNegativeClickListener = listener; + mCancelable = true; + return this; + } + + @Override + public ActionSheetBuilder setPositiveButton(CharSequence text, OnClickListener listener) { + mPositiveText = (String) text; + mPositiveClickListener = listener; + return this; + } + + @Override + public ActionSheetBuilder setItems(CharSequence[] items, OnClickListener listener) { + for (int i = 0; i < items.length; i++) { + ActionSheetItem item = new ActionSheetItem((String) items[i], listener); + mActionSheetItems.add(item); + } + return this; + } + + @Override + public ActionSheetBuilder setItems(@ArrayRes int itemsId, OnClickListener listener) { + this.setItems(mContext.getResources().getStringArray(itemsId), listener); + return this; + } + + @Override + public ActionSheetDialog create() { + mActionSheetDialog = new ActionSheetDialog(mContext); + Window window = mActionSheetDialog.getWindow(); + window.setGravity(Gravity.BOTTOM); + window.setWindowAnimations(mWindowAnimationId); + WindowManager.LayoutParams params = window.getAttributes(); + params.y = dpToPx(mSheetMargins); + params.x = 0; + WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + + int width = windowManager.getDefaultDisplay().getWidth(); + params.width = width - 2 * mSheetMargins; + Drawable drawable = new ColorDrawable(); + drawable.setAlpha(0); + window.setBackgroundDrawable(drawable); + mActionSheetDialog.setCancelable(mCancelable); + if (mCancelable) { + mActionSheetDialog.setCanceledOnTouchOutside(true); + } + initViews(); + return mActionSheetDialog; + } + + TextView mTitleView; + ImageView mTitleDividerView; + TextView mMessageView; + ImageView mMessageDividerView; + LinearLayout mSheetItemContainer; + LinearLayout mContentPanel; + TextView mCancelView; + TextView mPositiveView; + SheetItemOnClickListener mSheetItemOnClickListener = new SheetItemOnClickListener(); + + private void initViews() { + View rootView = LayoutInflater.from(mContext) + .inflate(R.layout.layout_action_sheet_dialog, null); + + mContentPanel = (LinearLayout) rootView.findViewById(R.id.content_panel); + mTitleView = (TextView) rootView.findViewById(R.id.tv_title); + mTitleDividerView = (ImageView) rootView.findViewById(R.id.title_divider); + mMessageView = (TextView) rootView.findViewById(R.id.tv_message); + mMessageDividerView = (ImageView) rootView.findViewById(R.id.message_divider); + mSheetItemContainer = (LinearLayout) rootView.findViewById(R.id.scrollView_sheet_list); + mCancelView = (TextView) rootView.findViewById(R.id.tv_cancel); + if (null != mContentBackground) { + mContentPanel.setBackground(mContentBackground); + } + handleTitle(); + handleMessage(); + handleContent(); + handleCancel(); + handlePositive(); + + mActionSheetDialog.setView(rootView); + + + } + + private ImageView createDivider(Drawable background, int inset) { + ImageView divider = new ImageView(mContext); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + Log.d(TAG, "createDivider: inset = " + inset); + params.setMargins(inset, 0, inset, 0); + divider.setLayoutParams(params); + divider.setBackground(background); + divider.setMinimumHeight(mItemDividerHeight); + return divider; + } + + private void handlePositive() { + if (null != mPositiveText) { + mPositiveView = new TextView(mContext); + mPositiveView.setGravity(Gravity.CENTER); + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + mPositiveView.setLayoutParams(params); + //mPositiveView.setPadding(0, dpToPx(5.0f), 0, dpToPx(5.0f)); + mPositiveView.setText(mPositiveText); + mPositiveView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPositiveTextSize); + mPositiveView.setTextColor(mPositiveTextColor); + mPositiveView.setTag(AlertDialog.BUTTON_POSITIVE); + mPositiveView.setOnClickListener(mSheetItemOnClickListener); + mPositiveView.setMinHeight(mPositiveHeight); + mSheetItemContainer.addView(createDivider(mItemDivider, mItemDividerInset)); + mSheetItemContainer.addView(mPositiveView); + } + + } + + private void handleCancel() { + if (null != mCancelView) { + if (mCancelable) { + mCancelView.setMinHeight(mCancelHeight); + mCancelView.setTextColor(mCancelTextColor); + mCancelView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCancelTextSize); + if (null != mCancelBackground) { + mCancelView.setBackground(mCancelBackground); + } + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, mCancelTopMargin, 0, 0); + mCancelView.setLayoutParams(params); + if (null != mNegativeText) { + mCancelView.setText(mNegativeText); + } + mCancelView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (null != mNegativeClickListener) { + mNegativeClickListener.onClick(mActionSheetDialog, AlertDialog.BUTTON_NEGATIVE); + } + mActionSheetDialog.dismiss(); + } + }); + } + } + } + + private void handleContent() { + if (null == mActionSheetItems || mActionSheetItems.isEmpty()) { + mSheetItemContainer.setVisibility(View.GONE); + } else { + for (int i = 0, size = mActionSheetItems.size(); i < size; i++) { + ActionSheetItem item = mActionSheetItems.get(i); + TextView sheetItemView = new TextView(mContext); + sheetItemView.setGravity(Gravity.CENTER); + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + sheetItemView.setLayoutParams(params); + //sheetItemView.setPadding(0, dpToPx(5.0f), 0, dpToPx(5.0f)); + sheetItemView.setText(item.text); + sheetItemView.setMinHeight(mItemHeight); + sheetItemView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mItemTextSize); + sheetItemView.setTextColor(mItemTextColor); + sheetItemView.setTag(i); + sheetItemView.setOnClickListener(mSheetItemOnClickListener); + mSheetItemContainer.addView(sheetItemView); + if (i < (size - 1)) { + mSheetItemContainer.addView(createDivider(mItemDivider, mItemDividerInset)); + } + } + + } + } + + private void handleMessage() { + if (null != mMessageView) { + if (null == mMessage) { + mMessageView.setVisibility(View.GONE); + mMessageDividerView.setVisibility(View.GONE); + } else { + mMessageDividerView.setBackground(mMessageDivider); + mMessageDividerView.setMinimumHeight(mMessageDividerHeight); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(mMessageDividerInset, 0, mMessageDividerInset, 0); + mMessageDividerView.setLayoutParams(params); + + mMessageView.setMinHeight(mMessageHeight); + mMessageView.setGravity(Gravity.CENTER); + mMessageView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mMessageTextSize); + mMessageView.setTextColor(mMessageTextColor); + mMessageView.setText(mMessage); + } + } + } + + private void handleTitle() { + if (null != mTitleView) { + if (null == mTitle) { + mTitleView.setVisibility(View.GONE); + mTitleDividerView.setVisibility(View.GONE); + } else { + mTitleDividerView.setBackground(mTitleDivider); + mTitleDividerView.setMinimumHeight(mTitleDividerHeight); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(mTitleDividerInset, 0, mTitleDividerInset, 0); + mTitleDividerView.setLayoutParams(params); + + mTitleView.setMinHeight(mTitleHeight); + mTitleView.setGravity(Gravity.CENTER); + mTitleView.setTextColor(mTitleTextColor); + mTitleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTitleTextSize); + mTitleView.setText(mTitle); + } + } + } + + public int dpToPx(float dp) { + float scale = mContext.getResources().getDisplayMetrics().density; + return (int) (dp * scale + 0.5f); + } + + public int spToPx(float sp) { + float scale = mContext.getResources().getDisplayMetrics().scaledDensity; + return (int) (sp * scale + 0.5f); + } + + + static class ActionSheetItem { + + String text; + OnClickListener listener; + + public ActionSheetItem(String text, OnClickListener listener) { + this.text = text; + this.listener = listener; + } + } + + public interface ActionSheetItemClickListener { + void onClick(int position); + } + + private class SheetItemOnClickListener implements View.OnClickListener { + + @Override + public void onClick(View v) { + + int tag = (int) v.getTag(); + Log.d(TAG, "onClick: tag = " + tag); + if (BUTTON_POSITIVE == tag) { + if (null != mPositiveClickListener) { + mPositiveClickListener.onClick(mActionSheetDialog, BUTTON_POSITIVE); + mActionSheetDialog.dismiss(); + } + mActionSheetDialog.dismiss(); + } else { + mActionSheetItems.get(tag).listener.onClick(mActionSheetDialog, tag); + } + } + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/BalanceEditText.java b/widget/src/main/java/com/hzecool/widget/BalanceEditText.java new file mode 100644 index 0000000..e602404 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/BalanceEditText.java @@ -0,0 +1,80 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.widget.EditText; + +import java.math.BigDecimal; + +/** + * Created by wangzhiguo on 2017/6/8 + */ +public class BalanceEditText extends android.support.v7.widget.AppCompatEditText { + public BalanceEditText(Context context) { + super(context); + } + + public BalanceEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BalanceEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * 字符串转人民币 + * + * @param value + * @return + */ + public static String str2Rmb(String value) { + double money = Double.parseDouble(value); + BigDecimal bigDecimal = BigDecimal.valueOf(money); + return "¥" + bigDecimal.setScale(2,BigDecimal.ROUND_HALF_UP); + } + + /** + * double 类型 转人民币 + * + * @param value + * @return + */ + public static String db2Rmb(double value) { + try { + return "¥" + BigDecimal.valueOf(value).setScale(2,BigDecimal.ROUND_HALF_UP); + } catch (NumberFormatException ex) { + throw new NumberFormatException(); + } + } + + public void setText(String text){ + super.setText(str2Rmb(text)); + } + + public void setText(double text){ + super.setText(db2Rmb(text)); + } + + public String getTextValue(){ + return removeRmbStr(super.getText().toString()); + } + + public double getDoubleValue(){ + if (TextUtils.isEmpty(this.getTextValue())){ + return 0.00; + } + return Double.parseDouble(this.getTextValue()); + } + + private String removeRmbStr(@NonNull String text) { + return text.replace("¥", ""); + } + + + public void setTextZero(){ + this.setText(0.00); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/BalanceTextView.java b/widget/src/main/java/com/hzecool/widget/BalanceTextView.java new file mode 100644 index 0000000..28492d8 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/BalanceTextView.java @@ -0,0 +1,93 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import java.math.BigDecimal; + +/** + * Created by wangzhiguo on 2017/6/8 + */ +public class BalanceTextView extends android.support.v7.widget.AppCompatTextView { + + private double showText; + public BalanceTextView(Context context) { + super(context); + } + + public BalanceTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public BalanceTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * 字符串转人民币 + * + * @param value + * @return + */ + public static String str2Rmb(String value) { + double money = Double.parseDouble(value); + BigDecimal bigDecimal = BigDecimal.valueOf(money); + return "¥" + bigDecimal.setScale(2,BigDecimal.ROUND_HALF_UP); + } + + /** + * double 类型 转人民币 + * + * @param value + * @return + */ + public static String db2Rmb(double value) { + try { + return "¥" + new BigDecimal(value).setScale(2,BigDecimal.ROUND_HALF_UP).toString(); + } catch (NumberFormatException ex) { + throw new NumberFormatException(); + } + } + + public void setText(@NonNull String text){ + super.setText(str2Rmb(text)); + } + + public String getText(){ + return removeRmbStr(super.getText().toString()); + } + + public void setText(@NonNull double text){ + super.setText(db2Rmb(text)); + } + + public double getDoubleValue(){ + return Double.parseDouble(this.getText()); + } + + private String removeRmbStr(@NonNull String text) { + return text.replace("¥", ""); + } + + + public double getShowText(){ + if (isLessZero){ + return new BigDecimal(removeRmbStr(super.getText().toString())).multiply(new BigDecimal(-1)).floatValue(); + }else { + return new BigDecimal(removeRmbStr(super.getText().toString())).multiply(new BigDecimal(1)).floatValue(); + } + } + + private boolean isLessZero = false; + + public void setShowText(double text){ + if (text <= 0){ + isLessZero = true; + }else if (text > 0){ + isLessZero = false; + } + setText(Math.abs(text)); + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/BottomNavigationView.java b/widget/src/main/java/com/hzecool/widget/BottomNavigationView.java new file mode 100644 index 0000000..c115dfd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/BottomNavigationView.java @@ -0,0 +1,877 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.internal.BottomNavigationItemView; +import android.support.design.internal.BottomNavigationMenuView; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; + +/** + * Created by tutu on 2017/5/2. + * 引用自 https://github.com/ittianyu/BottomNavigationViewEx#zh + */ +public class BottomNavigationView extends android.support.design.widget.BottomNavigationView { + // used for animation + private int mShiftAmount; + private float mScaleUpFactor; + private float mScaleDownFactor; + private boolean animationRecord; + private float mLargeLabelSize; + private float mSmallLabelSize; + private boolean visibilityTextSizeRecord; + private boolean visibilityHeightRecord; + private int mItemHeight; + // used for animation end + + // used for setupWithViewPager + private ViewPager mViewPager; + private MyOnNavigationItemSelectedListener mMyOnNavigationItemSelectedListener; + private BottomNavigationViewExOnPageChangeListener mPageChangeListener; + private BottomNavigationMenuView mMenuView; + private BottomNavigationItemView[] mButtons; + // used for setupWithViewPager end + + public BottomNavigationView(Context context) { + super(context); + } + + public BottomNavigationView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * change the visibility of icon + * + * @param visibility + */ + public void setIconVisibility(boolean visibility) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mButtons + private BottomNavigationItemView[] mButtons; + + 3. get mIcon in mButtons + private ImageView mIcon + + 4. set mIcon visibility gone + + 5. change mItemHeight to only text size in mMenuView + */ + // 1. get mMenuView + final BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. get mIcon in mButtons + for (BottomNavigationItemView button : mButtons) { + ImageView mIcon = getField(button.getClass(), button, "mIcon"); + // 4. set mIcon visibility gone + mIcon.setVisibility(visibility ? View.VISIBLE : View.INVISIBLE); + } + + // 5. change mItemHeight to only text size in mMenuView + if (!visibility) { + // if not record mItemHeight + if (!visibilityHeightRecord) { + visibilityHeightRecord = true; + mItemHeight = getItemHeight(); + } + + // change mItemHeight + BottomNavigationItemView button = mButtons[0]; + if (null != button) { + final ImageView mIcon = getField(button.getClass(), button, "mIcon"); +// System.out.println("mIcon.getMeasuredHeight():" + mIcon.getMeasuredHeight()); + if (null != mIcon) { + mIcon.post(new Runnable() { + @Override + public void run() { +// System.out.println("mIcon.getMeasuredHeight():" + mIcon.getMeasuredHeight()); + setItemHeight(mItemHeight - mIcon.getMeasuredHeight()); + } + }); + } + } + } else { + // if not record the mItemHeight, we need do nothing. + if (!visibilityHeightRecord) + return; + + // restore it + setItemHeight(mItemHeight); + } + + mMenuView.updateMenuView(); + } + + /** + * change the visibility of text + * + * @param visibility + */ + public void setTextVisibility(boolean visibility) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mButtons + private BottomNavigationItemView[] mButtons; + + 3. set text size in mButtons + private final TextView mLargeLabel + private final TextView mSmallLabel + + 4. change mItemHeight to only icon size in mMenuView + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. change field mShiftingMode value in mButtons + for (BottomNavigationItemView button : mButtons) { + TextView mLargeLabel = getField(button.getClass(), button, "mLargeLabel"); + TextView mSmallLabel = getField(button.getClass(), button, "mSmallLabel"); + + if (!visibility) { + // if not record the font size, record it + if (!visibilityTextSizeRecord && !animationRecord) { + visibilityTextSizeRecord = true; + mLargeLabelSize = mLargeLabel.getTextSize(); + mSmallLabelSize = mSmallLabel.getTextSize(); + } + + // if not visitable, set font size to 0 + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0); + mSmallLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0); + + } else { + // if not record the font size, we need do nothing. + if (!visibilityTextSizeRecord) + break; + + // restore it + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLargeLabelSize); + mSmallLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mSmallLabelSize); + } + } + + // 4 change mItemHeight to only icon size in mMenuView + if (!visibility) { + // if not record mItemHeight + if (!visibilityHeightRecord) { + visibilityHeightRecord = true; + mItemHeight = getItemHeight(); + } + + // change mItemHeight to only icon size in mMenuView + // private final int mItemHeight; + + // change mItemHeight +// System.out.println("mLargeLabel.getMeasuredHeight():" + getFontHeight(mSmallLabelSize)); + setItemHeight(mItemHeight - getFontHeight(mSmallLabelSize)); + + } else { + // if not record the mItemHeight, we need do nothing. + if (!visibilityHeightRecord) + return; + // restore mItemHeight + setItemHeight(mItemHeight); + } + + mMenuView.updateMenuView(); + } + + /** + * get text height by font size + * + * @param fontSize + * @return + */ + private static int getFontHeight(float fontSize) { + Paint paint = new Paint(); + paint.setTextSize(fontSize); + Paint.FontMetrics fm = paint.getFontMetrics(); + return (int) Math.ceil(fm.descent - fm.top) + 2; + } + + /** + * enable or disable click item animation(text scale and icon move animation in no item shifting mode) + * + * @param enable It means the text won't scale and icon won't move when active it in no item shifting mode if false. + */ + public void enableAnimation(boolean enable) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mButtons + private BottomNavigationItemView[] mButtons; + + 3. chang mShiftAmount to 0 in mButtons + private final int mShiftAmount + + change mScaleUpFactor and mScaleDownFactor to 1f in mButtons + private final float mScaleUpFactor + private final float mScaleDownFactor + + 4. change label font size in mButtons + private final TextView mLargeLabel + private final TextView mSmallLabel + */ + + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. change field mShiftingMode value in mButtons + for (BottomNavigationItemView button : mButtons) { + TextView mLargeLabel = getField(button.getClass(), button, "mLargeLabel"); + TextView mSmallLabel = getField(button.getClass(), button, "mSmallLabel"); + + // if disable animation, need animationRecord the source value + if (!enable) { + if (!animationRecord) { + animationRecord = true; + mShiftAmount = getField(button.getClass(), button, "mShiftAmount"); + mScaleUpFactor = getField(button.getClass(), button, "mScaleUpFactor"); + mScaleDownFactor = getField(button.getClass(), button, "mScaleDownFactor"); + + mLargeLabelSize = mLargeLabel.getTextSize(); + mSmallLabelSize = mSmallLabel.getTextSize(); + +// System.out.println("mShiftAmount:" + mShiftAmount + " mScaleUpFactor:" +// + mScaleUpFactor + " mScaleDownFactor:" + mScaleDownFactor +// + " mLargeLabel:" + mLargeLabelSize + " mSmallLabel:" + mSmallLabelSize); + } + // disable + setField(button.getClass(), button, "mShiftAmount", 0); + setField(button.getClass(), button, "mScaleUpFactor", 1); + setField(button.getClass(), button, "mScaleDownFactor", 1); + + // let the mLargeLabel font size equal to mSmallLabel + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mSmallLabelSize); + + // debug start +// mLargeLabelSize = mLargeLabel.getTextSize(); +// System.out.println("mLargeLabel:" + mLargeLabelSize); + // debug end + + } else { + // haven't change the value. It means it was the first call this method. So nothing need to do. + if (!animationRecord) + return; + // enable animation + setField(button.getClass(), button, "mShiftAmount", mShiftAmount); + setField(button.getClass(), button, "mScaleUpFactor", mScaleUpFactor); + setField(button.getClass(), button, "mScaleDownFactor", mScaleDownFactor); + // restore + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLargeLabelSize); + } + } + mMenuView.updateMenuView(); + } + + /** + * enable the shifting mode for navigation + * + * @param enable It will has a shift animation if true. Otherwise all items are the same width. + */ + public void enableShiftingMode(boolean enable) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. change field mShiftingMode value in mMenuView + private boolean mShiftingMode = true; + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. change field mShiftingMode value in mMenuView + setField(mMenuView.getClass(), mMenuView, "mShiftingMode", enable); + + mMenuView.updateMenuView(); + } + + /** + * enable the shifting mode for each item + * + * @param enable It will has a shift animation for item if true. Otherwise the item text always be shown. + */ + public void enableItemShiftingMode(boolean enable) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in this mMenuView + private BottomNavigationItemView[] mButtons; + + 3. change field mShiftingMode value in mButtons + private boolean mShiftingMode = true; + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. change field mShiftingMode value in mButtons + for (BottomNavigationItemView button : mButtons) { + setField(button.getClass(), button, "mShiftingMode", enable); + } + mMenuView.updateMenuView(); + } + + /** + * get the current checked item position + * + * @return index of item, start from 0. + */ + public int getCurrentItem() { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mMenuView + private BottomNavigationItemView[] mButtons; + + 3. get menu and traverse it to get the checked one + */ + + // 1. get mMenuView +// BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. get menu and traverse it to get the checked one + Menu menu = getMenu(); + for (int i = 0; i < mButtons.length; i++) { + if (menu.getItem(i).isChecked()) { + return i; + } + } + return 0; + } + + /** + * get menu item position in menu + * + * @param item + * @return position if success, -1 otherwise + */ + public int getMenuItemPosition(MenuItem item) { + // get item id + int itemId = item.getItemId(); + // get meunu + Menu menu = getMenu(); + int size = menu.size(); + for (int i = 0; i < size; i++) { + if (menu.getItem(i).getItemId() == itemId) { + return i; + } + } + return -1; + } + + /** + * set the current checked item + * + * @param item start from 0. + */ + public void setCurrentItem(int item) { + // check bounds + if (item < 0 || item >= getMaxItemCount()) { + throw new ArrayIndexOutOfBoundsException("item is out of bounds, we expected 0 - " + + (getMaxItemCount() - 1) + ". Actually " + item); + } + + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mMenuView + private BottomNavigationItemView[] mButtons; + private final OnClickListener mOnClickListener; + + 3. call mOnClickListener.onClick(); + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // get mOnClickListener + OnClickListener mOnClickListener = getField(mMenuView.getClass(), mMenuView, "mOnClickListener"); + +// System.out.println("mMenuView:" + mMenuView + " mButtons:" + mButtons + " mOnClickListener" + mOnClickListener); + // 3. call mOnClickListener.onClick(); + mOnClickListener.onClick(mButtons[item]); + + } + + /** + * get OnNavigationItemSelectedListener + * + * @return + */ + public OnNavigationItemSelectedListener getOnNavigationItemSelectedListener() { + // private OnNavigationItemSelectedListener mListener; + OnNavigationItemSelectedListener mListener = getField(getClass().getSuperclass(), this, "mListener"); + return mListener; + } + + @Override + public void setOnNavigationItemSelectedListener(@Nullable OnNavigationItemSelectedListener listener) { + // if not set up with view pager, the same with father + if (null == mMyOnNavigationItemSelectedListener) { + super.setOnNavigationItemSelectedListener(listener); + return; + } + + mMyOnNavigationItemSelectedListener.setOnNavigationItemSelectedListener(listener); + } + + /** + * get private mMenuView + * + * @return + */ + private BottomNavigationMenuView getBottomNavigationMenuView() { + if (null == mMenuView) + mMenuView = getField(getClass().getSuperclass(), this, "mMenuView"); + return mMenuView; + } + + /** + * get private mButtons in mMenuView + * + * @return + */ + public BottomNavigationItemView[] getBottomNavigationItemViews() { + if (null != mButtons) + return mButtons; + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + */ + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + mButtons = getField(mMenuView.getClass(), mMenuView, "mButtons"); + return mButtons; + } + + /** + * get private mButton in mMenuView at position + * + * @param position + * @return + */ + public BottomNavigationItemView getBottomNavigationItemView(int position) { + return getBottomNavigationItemViews()[position]; + } + + /** + * get icon at position + * + * @param position + * @return + */ + public ImageView getIconAt(int position) { + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + * 3 private ImageView mIcon; + */ + BottomNavigationItemView mButtons = getBottomNavigationItemView(position); + ImageView mIcon = getField(BottomNavigationItemView.class, mButtons, "mIcon"); + return mIcon; + } + + /** + * get small label at position + * Each item has tow label, one is large, another is small. + * + * @param position + * @return + */ + public TextView getSmallLabelAt(int position) { + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + * 3 private final TextView mSmallLabel; + */ + BottomNavigationItemView mButtons = getBottomNavigationItemView(position); + TextView mSmallLabel = getField(BottomNavigationItemView.class, mButtons, "mSmallLabel"); + return mSmallLabel; + } + + /** + * get large label at position + * Each item has tow label, one is large, another is small. + * + * @param position + * @return + */ + public TextView getLargeLabelAt(int position) { + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + * 3 private final TextView mLargeLabel; + */ + BottomNavigationItemView mButtons = getBottomNavigationItemView(position); + TextView mLargeLabel = getField(BottomNavigationItemView.class, mButtons, "mLargeLabel"); + return mLargeLabel; + } + + /** + * return item count + * + * @return + */ + public int getItemCount() { + return getBottomNavigationItemViews().length; + } + + /** + * set all item small TextView size + * Each item has tow label, one is large, another is small. + * Small one will be shown when item state is normal + * Large one will be shown when item checked. + * + * @param sp + */ + public void setSmallTextSize(float sp) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getSmallLabelAt(i).setTextSize(sp); + } + mMenuView.updateMenuView(); + } + + /** + * set all item large TextView size + * Each item has tow label, one is large, another is small. + * Small one will be shown when item state is normal. + * Large one will be shown when item checked. + * + * @param sp + */ + public void setLargeTextSize(float sp) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getLargeLabelAt(i).setTextSize(sp); + } + mMenuView.updateMenuView(); + } + + /** + * set all item large and small TextView size + * Each item has tow label, one is large, another is small. + * Small one will be shown when item state is normal + * Large one will be shown when item checked. + * + * @param sp + */ + public void setTextSize(float sp) { + setLargeTextSize(sp); + setSmallTextSize(sp); + } + + /** + * set item ImageView size which at position + * + * @param position position start from 0 + * @param width in dp + * @param height in dp + */ + public void setIconSizeAt(int position, float width, float height) { + ImageView icon = getIconAt(position); + // update size + ViewGroup.LayoutParams layoutParams = icon.getLayoutParams(); + layoutParams.width = dp2px(getContext(), width); + layoutParams.height = dp2px(getContext(), height); + icon.setLayoutParams(layoutParams); + + mMenuView.updateMenuView(); + } + + /** + * set all item ImageView size + * + * @param width in dp + * @param height in dp + */ + public void setIconSize(float width, float height) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + setIconSizeAt(i, width, height); + } + } + + /** + * set menu item height + * + * @param height in px + */ + public void setItemHeight(int height) { + // 1. get mMenuView + final BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. set private final int mItemHeight in mMenuView + setField(mMenuView.getClass(), mMenuView, "mItemHeight", height); + + mMenuView.updateMenuView(); + } + + /** + * get menu item height + * + * @return in px + */ + public int getItemHeight() { + // 1. get mMenuView + final BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get private final int mItemHeight in mMenuView + return getField(mMenuView.getClass(), mMenuView, "mItemHeight"); + } + + /** + * dp to px + * + * @param context + * @param dpValue dp + * @return px + */ + public static int dp2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * set Typeface for all item TextView + * + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + */ + public void setTypeface(Typeface typeface, int style) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getLargeLabelAt(i).setTypeface(typeface, style); + getSmallLabelAt(i).setTypeface(typeface, style); + } + mMenuView.updateMenuView(); + } + + /** + * set Typeface for all item TextView + * + * @attr ref android.R.styleable#TextView_typeface + */ + public void setTypeface(Typeface typeface) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getLargeLabelAt(i).setTypeface(typeface); + getSmallLabelAt(i).setTypeface(typeface); + } + mMenuView.updateMenuView(); + } + + /** + * get private filed in this specific object + * + * @param targetClass + * @param instance the filed owner + * @param fieldName + * @param + * @return field if success, null otherwise. + */ + private T getField(Class targetClass, Object instance, String fieldName) { + try { + Field field = targetClass.getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(instance); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + /** + * change the field value + * + * @param targetClass + * @param instance the filed owner + * @param fieldName + * @param value + */ + private void setField(Class targetClass, Object instance, String fieldName, Object value) { + try { + Field field = targetClass.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, value); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * This method will link the given ViewPager and this BottomNavigationViewEx together so that + * changes in one are automatically reflected in the other. This includes scroll state changes + * and clicks. + * + * @param viewPager + */ + public void setupWithViewPager(@Nullable final ViewPager viewPager) { + setupWithViewPager(viewPager, false); + } + + /** + * This method will link the given ViewPager and this BottomNavigationViewEx together so that + * changes in one are automatically reflected in the other. This includes scroll state changes + * and clicks. + * + * @param viewPager + * @param smoothScroll whether ViewPager changed with smooth scroll animation + */ + public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean smoothScroll) { + if (mViewPager != null) { + // If we've already been setup with a ViewPager, remove us from it + if (mPageChangeListener != null) { + mViewPager.removeOnPageChangeListener(mPageChangeListener); + } + } + + if (null == viewPager) { + mViewPager = null; + super.setOnNavigationItemSelectedListener(null); + return; + } + + mViewPager = viewPager; + + // Add our custom OnPageChangeListener to the ViewPager + if (mPageChangeListener == null) { + mPageChangeListener = new BottomNavigationViewExOnPageChangeListener(this); + } + viewPager.addOnPageChangeListener(mPageChangeListener); + + // Now we'll add a navigation item selected listener to set ViewPager's current item + OnNavigationItemSelectedListener listener = getOnNavigationItemSelectedListener(); + mMyOnNavigationItemSelectedListener = new MyOnNavigationItemSelectedListener(viewPager, this, smoothScroll, listener); + super.setOnNavigationItemSelectedListener(mMyOnNavigationItemSelectedListener); + } + + /** + * A {@link ViewPager.OnPageChangeListener} class which contains the + * necessary calls back to the provided {@link BottomNavigationView} so that the tab position is + * kept in sync. + *

+ *

This class stores the provided BottomNavigationViewEx weakly, meaning that you can use + * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) + * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and + * not cause a leak. + */ + private static class BottomNavigationViewExOnPageChangeListener implements ViewPager.OnPageChangeListener { + private final WeakReference mBnveRef; + + public BottomNavigationViewExOnPageChangeListener(BottomNavigationView bnve) { + mBnveRef = new WeakReference<>(bnve); + } + + @Override + public void onPageScrollStateChanged(final int state) { + } + + @Override + public void onPageScrolled(final int position, final float positionOffset, + final int positionOffsetPixels) { + } + + @Override + public void onPageSelected(final int position) { + final BottomNavigationView bnve = mBnveRef.get(); + if (null != bnve) + bnve.setCurrentItem(position); +// Log.d("onPageSelected", "--------- position " + position + " ------------"); + } + } + + /** + * Decorate OnNavigationItemSelectedListener for setupWithViewPager + */ + private static class MyOnNavigationItemSelectedListener implements OnNavigationItemSelectedListener { + private OnNavigationItemSelectedListener listener; + private final WeakReference viewPagerRef; + private boolean smoothScroll; + private SparseIntArray items;// used for change ViewPager selected item + private int previousPosition = -1; + + + MyOnNavigationItemSelectedListener(ViewPager viewPager, BottomNavigationView bnve, boolean smoothScroll, OnNavigationItemSelectedListener listener) { + this.viewPagerRef = new WeakReference<>(viewPager); + this.listener = listener; + this.smoothScroll = smoothScroll; + + // create items + Menu menu = bnve.getMenu(); + int size = menu.size(); + items = new SparseIntArray(size); + for (int i = 0; i < size; i++) { + int itemId = menu.getItem(i).getItemId(); + items.put(itemId, i); + } + } + + public void setOnNavigationItemSelectedListener(OnNavigationItemSelectedListener listener) { + this.listener = listener; + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + int position = items.get(item.getItemId()); + // only set item when item changed + if (previousPosition == position) { + return true; + } + + // user listener + if (null != listener) { + boolean bool = listener.onNavigationItemSelected(item); + // if the selected is invalid, no need change the view pager + if (!bool) + return false; + } + + // change view pager + ViewPager viewPager = viewPagerRef.get(); + if (null == viewPager) + return false; + + viewPager.setCurrentItem(items.get(item.getItemId()), smoothScroll); + + // update previous position + previousPosition = position; + + return true; + } + + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/CheckImageView.java b/widget/src/main/java/com/hzecool/widget/CheckImageView.java new file mode 100644 index 0000000..830067a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/CheckImageView.java @@ -0,0 +1,72 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Checkable; + +/** + * Created by tutu on 2017/5/3. + */ + + +public class CheckImageView extends AppCompatImageView implements Checkable { + private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; + private boolean mChecked; + + public CheckImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CheckImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CheckImageView(Context context) { + super(context); + } + + @Override + public void setChecked(boolean checked) { + if (checked != mChecked) { + mChecked = checked; + refreshDrawableState(); + } + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void toggle() { + mChecked = !mChecked; + refreshDrawableState(); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setCheckable(true); + info.setChecked(mChecked); + } +} + diff --git a/widget/src/main/java/com/hzecool/widget/ClearableEditText.java b/widget/src/main/java/com/hzecool/widget/ClearableEditText.java new file mode 100644 index 0000000..6231a46 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ClearableEditText.java @@ -0,0 +1,120 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +/** + * Created by slh on 2017/3/1. + */ + +public class ClearableEditText extends android.support.v7.widget.AppCompatEditText implements View.OnFocusChangeListener, TextWatcher { + /** + * 删除按钮的引用 + */ + private Drawable mClearDrawable; + /** + * 控件是否有焦点 + */ + private boolean hasFoucs; + + public ClearableEditText(Context context) { + this(context, null); + } + + public ClearableEditText(Context context, AttributeSet attrs) { + // 这里构造方法也很重要,不加这个很多属性不能再XML里面定义 + this(context, attrs, android.R.attr.editTextStyle); + } + + public ClearableEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + // 获取EditText的DrawableRight,假如没有设置我们就使用默认的图片 + mClearDrawable = getCompoundDrawables()[2]; + if (mClearDrawable == null) { + // throw new + // NullPointerException("You can add drawableRight attribute in XML"); + mClearDrawable = getResources().getDrawable(R.mipmap.ic_delete); + } + + mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight()); + // 默认设置隐藏图标 + setClearIconVisible(false); + // 设置焦点改变的监听 + setOnFocusChangeListener(this); + // 设置输入框里面内容发生改变的监听 + addTextChangedListener(this); + } + + /** + * 因为我们不能直接给EditText设置点击事件,所以我们用记住我们按下的位置来模拟点击事件 当我们按下的位置 在 EditText的宽度 - + * 图标到控件右边的间距 - 图标的宽度 和 EditText的宽度 - 图标到控件右边的间距之间我们就算点击了图标,竖直方向就没有考虑 + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (getCompoundDrawables()[2] != null) { + + boolean touchable = event.getX() > (getWidth() - getTotalPaddingRight()) && (event.getX() < ((getWidth() - getPaddingRight()))); + + if (touchable) { + this.setText(""); + } + } + } + + return super.onTouchEvent(event); + } + + /** + * 当ClearEditText焦点发生变化的时候,判断里面字符串长度设置清除图标的显示与隐藏 + */ + @Override + public void onFocusChange(View v, boolean hasFocus) { + this.hasFoucs = hasFocus; + if (hasFocus) { + setClearIconVisible(getText().length() > 0); + } else { + setClearIconVisible(false); + } + } + + /** + * 设置清除图标的显示与隐藏,调用setCompoundDrawables为EditText绘制上去 + * + * @param visible + */ + protected void setClearIconVisible(boolean visible) { + Drawable right = visible ? mClearDrawable : null; + setCompoundDrawables(getCompoundDrawables()[0], getCompoundDrawables()[1], right, getCompoundDrawables()[3]); + } + + /** + * 当输入框里面内容发生变化的时候回调的方法 + */ + @Override + public void onTextChanged(CharSequence s, int start, int count, int after) { + if (hasFoucs) { + setClearIconVisible(s.length() > 0); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/CountDownButton/CountDownButton.java b/widget/src/main/java/com/hzecool/widget/CountDownButton/CountDownButton.java new file mode 100644 index 0000000..9ede67b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/CountDownButton/CountDownButton.java @@ -0,0 +1,214 @@ +package com.hzecool.widget.CountDownButton; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.CountDownTimer; +import android.support.v7.widget.AppCompatButton; +import android.util.AttributeSet; + +import com.hzecool.widget.R; + +import java.util.Locale; + +/** + * Created by tutu on 2017/3/16. + */ + +public class CountDownButton extends AppCompatButton { + + /** + * 默认时间间隔1000ms + */ + private static final long DEFAULT_INTERVAL = 1000; + /** + * 默认时长60s + */ + private static final long DEFAULT_COUNT = 60 * 1000; + /** + * 默认倒计时文字格式(显示秒数) + */ + private static final String DEFAULT_COUNT_FORMAT = "%d"; + /** + * 默认按钮文字 {@link #getText()} + */ + private String mDefaultText; + /** + * 倒计时时长,单位为毫秒 + */ + private long mCount; + /** + * 时间间隔,单位为毫秒 + */ + private long mInterval; + /** + * 倒计时文字格式 + */ + private String mCountDownFormat = DEFAULT_COUNT_FORMAT; + /** + * 倒计时是否可用 + */ + private boolean mEnableCountDown = true; + /** + * 点击事件监听器 + */ + private OnClickListener onClickListener; + + /** + * 倒计时 + */ + private CountDownTimer mCountDownTimer; + + /** + * 是否正在执行倒计时 + */ + private boolean isCountDownNow; + + private int enableFalseTextColor; + private int enableTextColor; + + public CountDownButton(Context context) { + super(context); + } + + public CountDownButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public CountDownButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + + private void init(Context context, AttributeSet attrs) { + // 获取自定义属性值 + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CountDownButton); + mCountDownFormat = typedArray.getString(R.styleable.CountDownButton_countDownFormat); + enableFalseTextColor = typedArray.getColor(R.styleable.CountDownButton_enableFalseTextColor,getResources().getColor(R.color.white)); + enableTextColor = typedArray.getColor(R.styleable.CountDownButton_enableTextColor,getResources().getColor(R.color.white)); + if (typedArray.hasValue(R.styleable.CountDownButton_countDown)) { + mCount = (int) typedArray.getFloat(R.styleable.CountDownButton_countDown, DEFAULT_COUNT); + } + mInterval = (int) typedArray.getFloat(R.styleable.CountDownButton_countDownInterval, DEFAULT_INTERVAL); + mEnableCountDown = (mCount > mInterval) && typedArray.getBoolean(R.styleable.CountDownButton_enableCountDown, true); + typedArray.recycle(); + // 初始化倒计时Timer + if (mCountDownTimer == null) { + mCountDownTimer = new CountDownTimer(mCount, mInterval) { + @Override + public void onTick(long millisUntilFinished) { + setText(String.format(Locale.CHINA, mCountDownFormat, millisUntilFinished / 1000)); + } + + @Override + public void onFinish() { + isCountDownNow = false; + setEnabled(true); + setClickable(true); + setText(mDefaultText); + setTextColor(enableTextColor); + } + }; + } + } + + + @Override + public void setOnClickListener(OnClickListener onClickListener) { + super.setOnClickListener(onClickListener); + this.onClickListener = onClickListener; + } + + + public void start(){ + if (mEnableCountDown){ + setTextColor(enableFalseTextColor); + mDefaultText = getText().toString(); + // 设置按钮不可点击 + setEnabled(false); + setClickable(false); + // 开始倒计时 + mCountDownTimer.start(); + isCountDownNow = true; + } + } + +// @Override +// public boolean onTouchEvent(MotionEvent event) { +// switch (event.getAction()) { +// case MotionEvent.ACTION_UP: +// Rect rect = new Rect(); +// this.getGlobalVisibleRect(rect); +// if (onClickListener != null && rect.contains((int) event.getRawX(), (int) event.getRawY())) { +// onClickListener.onClick(this); +// } +// if (mEnableCountDown && rect.contains((int) event.getRawX(), (int) event.getRawY())) { +// mDefaultText = getText().toString(); +// // 设置按钮不可点击 +// setEnabled(false); +// setClickable(false); +// // 开始倒计时 +// mCountDownTimer.start(); +// isCountDownNow = true; +// } +// break; +// case MotionEvent.ACTION_MOVE: +// break; +// } +// return super.onTouchEvent(event); +// } + + public void setEnableCountDown(boolean enableCountDown) { + this.mEnableCountDown = (mCount > mInterval) && enableCountDown; + } + + public void setCountDownFormat(String countDownFormat) { + this.mCountDownFormat = countDownFormat; + } + + public void setCount(long count) { + this.mCount = count; + } + + public void setInterval(long interval) { + mInterval = interval; + } + + /** + * 是否正在执行倒计时 + * + * @return 倒计时期间返回true否则返回false + */ + public boolean isCountDownNow() { + return isCountDownNow; + } + + /** + * 设置倒计时数据 + * + * @param count 时长 + * @param interval 间隔 + * @param countDownFormat 文字格式 + */ + public void setCountDown(long count, long interval, String countDownFormat) { + this.mCount = count; + this.mCountDownFormat = countDownFormat; + this.mInterval = interval; + setEnableCountDown(true); + } + + /** + * 移除倒计时 + */ + public void removeCountDown() { + if (mCountDownTimer != null) { + mCountDownTimer.cancel(); + } + setTextColor(enableTextColor); + isCountDownNow = false; + setText(mDefaultText); + setEnabled(true); + setClickable(true); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/FlowLayout.java b/widget/src/main/java/com/hzecool/widget/FlowLayout.java new file mode 100644 index 0000000..be572c1 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/FlowLayout.java @@ -0,0 +1,200 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * 流式布局 + * Created by wangzhiguo on 2017/6/19 + */ +public class FlowLayout extends ViewGroup { + + + public FlowLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public FlowLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FlowLayout(Context context) { + this(context, null); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); + int modeWidth = MeasureSpec.getMode(widthMeasureSpec); + int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); + int modeHeight = MeasureSpec.getMode(heightMeasureSpec); + + // 如果是warp_content情况下,记录宽和高 + int width = 0; + int height = 0; + + // 记录每一行的宽度与高度 + int lineWidth = 0; + int lineHeight = 0; + + // 得到内部元素的个数 + int cCount = getChildCount(); + + for (int i = 0; i < cCount; i++) { + // 通过索引拿到每一个子view + View child = getChildAt(i); + // 测量子View的宽和高,系统提供的measureChild + measureChild(child, widthMeasureSpec, heightMeasureSpec); + // 得到LayoutParams + MarginLayoutParams lp = (MarginLayoutParams) child + .getLayoutParams(); + + // 子View占据的宽度 + int childWidth = child.getMeasuredWidth() + lp.leftMargin + + lp.rightMargin; + // 子View占据的高度 + int childHeight = child.getMeasuredHeight() + lp.topMargin + + lp.bottomMargin; + + // 换行 判断 当前的宽度大于 开辟新行 + if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) { + // 对比得到最大的宽度 + width = Math.max(width, lineWidth); + // 重置lineWidth + lineWidth = childWidth; + // 记录行高 + height += lineHeight; + lineHeight = childHeight; + } else + // 未换行 + { + // 叠加行宽 + lineWidth += childWidth; + // 得到当前行最大的高度 + lineHeight = Math.max(lineHeight, childHeight); + } + // 特殊情况,最后一个控件 + if (i == cCount - 1) { + width = Math.max(lineWidth, width); + height += lineHeight; + } + } + setMeasuredDimension( + modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(), + modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()// + ); + + } + + /** + * 存储所有的View + */ + private List> mAllViews = new ArrayList>(); + /** + * 每一行的高度 + */ + private List mLineHeight = new ArrayList(); + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mAllViews.clear(); + mLineHeight.clear(); + + // 当前ViewGroup的宽度 + int width = getWidth(); + + int lineWidth = 0; + int lineHeight = 0; + + // 存放每一行的子view + List lineViews = new ArrayList(); + + int cCount = getChildCount(); + + for (int i = 0; i < cCount; i++) { + View child = getChildAt(i); + MarginLayoutParams lp = (MarginLayoutParams) child + .getLayoutParams(); + + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + // 如果需要换行 + if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) { + // 记录LineHeight + mLineHeight.add(lineHeight); + // 记录当前行的Views + mAllViews.add(lineViews); + + // 重置我们的行宽和行高 + lineWidth = 0; + lineHeight = childHeight + lp.topMargin + lp.bottomMargin; + // 重置我们的View集合 + lineViews = new ArrayList(); + } + lineWidth += childWidth + lp.leftMargin + lp.rightMargin; + lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + + lp.bottomMargin); + lineViews.add(child); + + }// for end + // 处理最后一行 + mLineHeight.add(lineHeight); + mAllViews.add(lineViews); + + // 设置子View的位置 + + int left = getPaddingLeft(); + int top = getPaddingTop(); + + // 行数 + int lineNum = mAllViews.size(); + + for (int i = 0; i < lineNum; i++) { + // 当前行的所有的View + lineViews = mAllViews.get(i); + lineHeight = mLineHeight.get(i); + + for (int j = 0; j < lineViews.size(); j++) { + View child = lineViews.get(j); + // 判断child的状态 + if (child.getVisibility() == View.GONE) { + continue; + } + + MarginLayoutParams lp = (MarginLayoutParams) child + .getLayoutParams(); + + int lc = left + lp.leftMargin; + int tc = top + lp.topMargin; + int rc = lc + child.getMeasuredWidth(); + int bc = tc + child.getMeasuredHeight(); + + // 为子View进行布局 + child.layout(lc, tc, rc, bc); + + left += child.getMeasuredWidth() + lp.leftMargin + + lp.rightMargin; + } + left = getPaddingLeft(); + top += lineHeight; + } + + } + + /** + * 与当前ViewGroup对应的LayoutParams + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/RecycleViewDivider.java b/widget/src/main/java/com/hzecool/widget/RecycleViewDivider.java new file mode 100644 index 0000000..b562e8b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/RecycleViewDivider.java @@ -0,0 +1,148 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** + * RecyclerView 分割线 + * Created by tutu on 2017/4/19. + */ + +public class RecycleViewDivider extends RecyclerView.ItemDecoration { + private Paint mPaint; + private Drawable mDivider; + private int mDividerHeight = 2;//分割线高度,默认为1px + private int mOrientation;//列表的方向:LinearLayoutManager.VERTICAL或LinearLayoutManager.HORIZONTAL + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + private int marginLeft = 0; + private int marginRight = 0; + + /** + * 默认分割线:高度为2px,颜色为灰色 + * + * @param context + * @param orientation 列表方向 + */ + public RecycleViewDivider(Context context, int orientation) { + if (orientation != LinearLayoutManager.VERTICAL && orientation != LinearLayoutManager.HORIZONTAL) { + throw new IllegalArgumentException("请输入正确的参数!"); + } + mOrientation = orientation; + + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + } + + /** + * 自定义分割线 + * + * @param context + * @param orientation 列表方向 + * @param drawableId 分割线图片 + */ + public RecycleViewDivider(Context context, int orientation, int drawableId) { + this(context, orientation); + mDivider = ContextCompat.getDrawable(context, drawableId); + mDividerHeight = mDivider.getIntrinsicHeight(); + } + + /** + * 自定义分割线 + * + * @param context + * @param orientation 列表方向 + * @param dividerHeight 分割线高度 + * @param dividerColor 分割线颜色 + */ + public RecycleViewDivider(Context context, int orientation, int dividerHeight, int dividerColor) { + this(context, orientation); + mDividerHeight = dividerHeight; + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(dividerColor); + mPaint.setStyle(Paint.Style.FILL); + } + + + public int getMarginLeft() { + return marginLeft; + } + + public void setMarginLeft(int marginLeft) { + this.marginLeft = marginLeft; + } + + public int getMarginRight() { + return marginRight; + } + + public void setMarginRight(int marginRight) { + this.marginRight = marginRight; + } + + //获取分割线尺寸 + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + outRect.set(0, 0, 0, mDividerHeight); + } + + //绘制分割线 + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + super.onDraw(c, parent, state); + if (mOrientation == LinearLayoutManager.VERTICAL) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + //绘制横向 item 分割线 + private void drawHorizontal(Canvas canvas, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getMeasuredWidth() - parent.getPaddingRight(); + final int childSize = parent.getChildCount(); + for (int i = 0; i < childSize-1; i++) { + final View child = parent.getChildAt(i); + RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); + final int top = child.getBottom() + layoutParams.bottomMargin; + final int bottom = top + mDividerHeight; + if (mDivider != null) { + mDivider.setBounds(left+marginLeft, top, right-marginRight, bottom); + mDivider.draw(canvas); + } + if (mPaint != null) { + canvas.drawRect(left+marginLeft, top, right-marginRight, bottom, mPaint); + } + } + } + + //绘制纵向 item 分割线 + private void drawVertical(Canvas canvas, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom(); + final int childSize = parent.getChildCount(); + for (int i = 0; i < childSize; i++) { + final View child = parent.getChildAt(i); + RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); + final int left = child.getRight() + layoutParams.rightMargin; + final int right = left + mDividerHeight; + if (mDivider != null) { + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + if (mPaint != null) { + canvas.drawRect(left, top, right, bottom, mPaint); + } + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/WaveSideBar.java b/widget/src/main/java/com/hzecool/widget/WaveSideBar.java new file mode 100644 index 0000000..21fe252 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/WaveSideBar.java @@ -0,0 +1,398 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; + +import java.util.Arrays; + +/** + * 列表添加索引 + * add by tutu on 2017/4/12 + * refer to https://github.com/gjiazhe/WaveSideBar + */ +public class WaveSideBar extends View { + private final static int DEFAULT_TEXT_SIZE = 14; // sp + private final static int DEFAULT_MAX_OFFSET = 80; //dp + + private final static String[] DEFAULT_INDEX_ITEMS = {"A", "B", "C", "D", "E", "F", "G", "H", "I", + "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}; + + private String[] mIndexItems; + + /** + * the index in {@link #mIndexItems} of the current selected index item, + * it's reset to -1 when the finger up + */ + private int mCurrentIndex = -1; + + /** + * Y coordinate of the point where finger is touching, + * the baseline is top of {@link #mStartTouchingArea} + * it's reset to -1 when the finger up + */ + private float mCurrentY = -1; + + private Paint mPaint; + private int mTextColor; + private float mTextSize; + + /** + * the height of each index item + */ + private float mIndexItemHeight; + + /** + * offset of the current selected index item + */ + private float mMaxOffset; + + /** + * {@link #mStartTouching} will be set to true when {@link MotionEvent#ACTION_DOWN} + * happens in this area, and the side bar should start working. + */ + private RectF mStartTouchingArea = new RectF(); + + /** + * height and width of {@link #mStartTouchingArea} + */ + private float mBarHeight; + private float mBarWidth; + + /** + * Flag that the finger is starting touching. + * If true, it means the {@link MotionEvent#ACTION_DOWN} happened but + * {@link MotionEvent#ACTION_UP} not yet. + */ + private boolean mStartTouching = false; + + /** + * if true, the {@link OnSelectIndexItemListener#onSelectIndexItem(String)} + * will not be called until the finger up. + * if false, it will be called when the finger down, up and move. + */ + private boolean mLazyRespond = false; + + /** + * the position of the side bar, default is {@link #POSITION_RIGHT}. + * You can set it to {@link #POSITION_LEFT} for people who use phone with left hand. + */ + private int mSideBarPosition; + public static final int POSITION_RIGHT = 0; + public static final int POSITION_LEFT = 1; + + /** + * the alignment of items, default is {@link #TEXT_ALIGN_CENTER}. + */ + private int mTextAlignment; + public static final int TEXT_ALIGN_CENTER = 0; + public static final int TEXT_ALIGN_LEFT = 1; + public static final int TEXT_ALIGN_RIGHT = 2; + + + /** + * observe the current selected index item + */ + private OnSelectIndexItemListener onSelectIndexItemListener; + + /** + * the baseline of the first index item text to draw + */ + private float mFirstItemBaseLineY; + + /** + * for {@link #dp2px(int)} and {@link #sp2px(int)} + */ + private DisplayMetrics mDisplayMetrics; + + + public WaveSideBar(Context context) { + this(context, null); + } + + public WaveSideBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WaveSideBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDisplayMetrics = context.getResources().getDisplayMetrics(); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveSideBar); + mLazyRespond = typedArray.getBoolean(R.styleable.WaveSideBar_sidebar_lazy_respond, false); + mTextColor = typedArray.getColor(R.styleable.WaveSideBar_sidebar_text_color, Color.GRAY); + mTextSize = typedArray.getDimension(R.styleable.WaveSideBar_sidebar_text_size, sp2px(DEFAULT_TEXT_SIZE)); + mMaxOffset = typedArray.getDimension(R.styleable.WaveSideBar_sidebar_max_offset, dp2px(DEFAULT_MAX_OFFSET)); + mSideBarPosition = typedArray.getInt(R.styleable.WaveSideBar_sidebar_position, POSITION_RIGHT); + mTextAlignment = typedArray.getInt(R.styleable.WaveSideBar_sidebar_text_alignment, TEXT_ALIGN_CENTER); + typedArray.recycle(); + + mIndexItems = DEFAULT_INDEX_ITEMS; + + initPaint(); + } + + private void initPaint() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(mTextColor); + mPaint.setTextSize(mTextSize); + switch (mTextAlignment) { + case TEXT_ALIGN_CENTER: + mPaint.setTextAlign(Paint.Align.CENTER); + break; + case TEXT_ALIGN_LEFT: + mPaint.setTextAlign(Paint.Align.LEFT); + break; + case TEXT_ALIGN_RIGHT: + mPaint.setTextAlign(Paint.Align.RIGHT); + break; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + + Paint.FontMetrics fontMetrics = mPaint.getFontMetrics(); + mIndexItemHeight = fontMetrics.bottom - fontMetrics.top; + mBarHeight = mIndexItems.length * mIndexItemHeight; + + // calculate the width of the longest text as the width of side bar + for (String indexItem : mIndexItems) { + mBarWidth = Math.max(mBarWidth, mPaint.measureText(indexItem)); + } + + float areaLeft = (mSideBarPosition == POSITION_LEFT) ? 0 : (width - mBarWidth - getPaddingRight()); + float areaRight = (mSideBarPosition == POSITION_LEFT) ? (getPaddingLeft() + areaLeft + mBarWidth) : width; + float areaTop = height / 2 - mBarHeight / 2; + float areaBottom = areaTop + mBarHeight; + mStartTouchingArea.set( + areaLeft, + areaTop, + areaRight, + areaBottom); + + // the baseline Y of the first item' text to draw + mFirstItemBaseLineY = (height / 2 - mIndexItems.length * mIndexItemHeight / 2) + + (mIndexItemHeight / 2 - (fontMetrics.descent - fontMetrics.ascent) / 2) + - fontMetrics.ascent; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // draw each item + for (int i = 0, mIndexItemsLength = mIndexItems.length; i < mIndexItemsLength; i++) { + float baseLineY = mFirstItemBaseLineY + mIndexItemHeight * i; + + // calculate the scale factor of the item to draw + float scale = getItemScale(i); + + int alphaScale = (i == mCurrentIndex) ? (255) : (int) (255 * (1 - scale)); + mPaint.setAlpha(alphaScale); + + mPaint.setTextSize(mTextSize + mTextSize * scale); + + float baseLineX = 0f; + if (mSideBarPosition == POSITION_LEFT) { + switch (mTextAlignment) { + case TEXT_ALIGN_CENTER: + baseLineX = getPaddingLeft() + mBarWidth / 2 + mMaxOffset * scale; + break; + case TEXT_ALIGN_LEFT: + baseLineX = getPaddingLeft() + mMaxOffset * scale; + break; + case TEXT_ALIGN_RIGHT: + baseLineX = getPaddingLeft() + mBarWidth + mMaxOffset * scale; + break; + } + } else { + switch (mTextAlignment) { + case TEXT_ALIGN_CENTER: + baseLineX = getWidth() - getPaddingRight() - mBarWidth / 2 - mMaxOffset * scale; + break; + case TEXT_ALIGN_RIGHT: + baseLineX = getWidth() - getPaddingRight() - mMaxOffset * scale; + break; + case TEXT_ALIGN_LEFT: + baseLineX = getWidth() - getPaddingRight() - mBarWidth - mMaxOffset * scale; + break; + } + } + + // draw + canvas.drawText( + mIndexItems[i], //item text to draw + baseLineX, //baseLine X + baseLineY, // baseLine Y + mPaint); + } + + // reset paint + mPaint.setAlpha(255); + mPaint.setTextSize(mTextSize); + } + + /** + * calculate the scale factor of the item to draw + * + * @param index the index of the item in array {@link #mIndexItems} + * @return the scale factor of the item to draw + */ + private float getItemScale(int index) { + float scale = 0; + if (mCurrentIndex != -1) { + float distance = Math.abs(mCurrentY - (mIndexItemHeight * index + mIndexItemHeight / 2)) / mIndexItemHeight; + scale = 1 - distance * distance / 16; + scale = Math.max(scale, 0); + } + return scale; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mIndexItems.length == 0) { + return super.onTouchEvent(event); + } + + float eventY = event.getY(); + float eventX = event.getX(); + mCurrentIndex = getSelectedIndex(eventY); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (mStartTouchingArea.contains(eventX, eventY)) { + mStartTouching = true; + if (!mLazyRespond && onSelectIndexItemListener != null) { + onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]); + } + invalidate(); + return true; + } else { + mCurrentIndex = -1; + return false; + } + + case MotionEvent.ACTION_MOVE: + if (mStartTouching && !mLazyRespond && onSelectIndexItemListener != null) { + onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]); + } + invalidate(); + return true; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mLazyRespond && onSelectIndexItemListener != null) { + onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]); + } + mCurrentIndex = -1; + mStartTouching = false; + invalidate(); + return true; + } + + return super.onTouchEvent(event); + } + + private int getSelectedIndex(float eventY) { + mCurrentY = eventY - (getHeight() / 2 - mBarHeight / 2); + if (mCurrentY <= 0) { + return 0; + } + + int index = (int) (mCurrentY / this.mIndexItemHeight); + if (index >= this.mIndexItems.length) { + index = this.mIndexItems.length - 1; + } + return index; + } + + private float dp2px(int dp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.mDisplayMetrics); + } + + private float sp2px(int sp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, this.mDisplayMetrics); + } + + public void setIndexItems(String... indexItems) { + mIndexItems = Arrays.copyOf(indexItems, indexItems.length); + requestLayout(); + } + + public void setTextColor(int color) { + mTextColor = color; + mPaint.setColor(color); + invalidate(); + } + + public void setPosition(int position) { + if (position != POSITION_RIGHT && position != POSITION_LEFT) { + throw new IllegalArgumentException("the position must be POSITION_RIGHT or POSITION_LEFT"); + } + + mSideBarPosition = position; + requestLayout(); + } + + public void setMaxOffset(int offset) { + mMaxOffset = offset; + invalidate(); + } + + public void setLazyRespond(boolean lazyRespond) { + mLazyRespond = lazyRespond; + } + + public void setTextAlign(int align) { + if (mTextAlignment == align) { + return; + } + switch (align) { + case TEXT_ALIGN_CENTER: + mPaint.setTextAlign(Paint.Align.CENTER); + break; + case TEXT_ALIGN_LEFT: + mPaint.setTextAlign(Paint.Align.LEFT); + break; + case TEXT_ALIGN_RIGHT: + mPaint.setTextAlign(Paint.Align.RIGHT); + break; + default: + throw new IllegalArgumentException( + "the alignment must be TEXT_ALIGN_CENTER, TEXT_ALIGN_LEFT or TEXT_ALIGN_RIGHT"); + } + mTextAlignment = align; + invalidate(); + } + + public void setTextSize(float size) { + if (mTextSize == size) { + return; + } + mTextSize = size; + mPaint.setTextSize(size); + invalidate(); + } + + public void setOnSelectIndexItemListener(OnSelectIndexItemListener onSelectIndexItemListener) { + this.onSelectIndexItemListener = onSelectIndexItemListener; + } + + public interface OnSelectIndexItemListener { + void onSelectIndexItem(String index); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/WaveView.java b/widget/src/main/java/com/hzecool/widget/WaveView.java new file mode 100644 index 0000000..60366cf --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/WaveView.java @@ -0,0 +1,166 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * 水波纹特效 + * Created by fbchen2 on 2016/5/25. + */ +public class WaveView extends View { + private float mInitialRadius; // 初始波纹半径 + private float mMaxRadius; // 最大波纹半径 + private long mDuration = 2000; // 一个波纹从创建到消失的持续时间 + private int mSpeed = 500; // 波纹的创建速度,每500ms创建一个 + private float mMaxRadiusRate = 0.85f; + private boolean mMaxRadiusSet; + private boolean mIsRunning; + private long mLastCreateTime; + private List mCircleList = new ArrayList(); + + private Runnable mCreateCircle = new Runnable() { + @Override + public void run() { + if (mIsRunning) { + newCircle(); + postDelayed(mCreateCircle, mSpeed); + } + } + }; + + private Interpolator mInterpolator = new LinearInterpolator(); + + private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public WaveView(Context context) { + super(context); + } + + public WaveView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setStyle(Paint.Style style) { + mPaint.setStyle(style); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (!mMaxRadiusSet) { + mMaxRadius = Math.min(w, h) * mMaxRadiusRate / 2.0f; + } + } + + public void setMaxRadiusRate(float maxRadiusRate) { + mMaxRadiusRate = maxRadiusRate; + } + + public void setColor(int color) { + mPaint.setColor(color); + } + + /** + * 开始 + */ + public void start() { + if (!mIsRunning) { + mIsRunning = true; + mCreateCircle.run(); + } + } + + /** + * 缓慢停止 + */ + public void stop() { + mIsRunning = false; + } + + /** + * 立即停止 + */ + public void stopImmediately() { + mIsRunning = false; + mCircleList.clear(); + invalidate(); + } + + protected void onDraw(Canvas canvas) { + Iterator iterator = mCircleList.iterator(); + while (iterator.hasNext()) { + Circle circle = iterator.next(); + float radius = circle.getCurrentRadius(); + if (System.currentTimeMillis() - circle.mCreateTime < mDuration) { + mPaint.setAlpha(circle.getAlpha()); + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint); + } else { + iterator.remove(); + } + } + if (mCircleList.size() > 0) { + postInvalidateDelayed(10); + } + } + + public void setInitialRadius(float radius) { + mInitialRadius = radius; + } + + public void setDuration(long duration) { + mDuration = duration; + } + + public void setMaxRadius(float maxRadius) { + mMaxRadius = maxRadius; + mMaxRadiusSet = true; + } + + public void setSpeed(int speed) { + mSpeed = speed; + } + + private void newCircle() { + long currentTime = System.currentTimeMillis(); + if (currentTime - mLastCreateTime < mSpeed) { + return; + } + Circle circle = new Circle(); + mCircleList.add(circle); + invalidate(); + mLastCreateTime = currentTime; + } + + private class Circle { + private long mCreateTime; + + Circle() { + mCreateTime = System.currentTimeMillis(); + } + + int getAlpha() { + float percent = (getCurrentRadius() - mInitialRadius) / (mMaxRadius - mInitialRadius); + return (int) (255 - mInterpolator.getInterpolation(percent) * 255); + } + + float getCurrentRadius() { + float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration; + return mInitialRadius + mInterpolator.getInterpolation(percent) * (mMaxRadius - mInitialRadius); + } + } + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + if (mInterpolator == null) { + mInterpolator = new LinearInterpolator(); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/badgeview/BadgeView.java b/widget/src/main/java/com/hzecool/widget/badgeview/BadgeView.java new file mode 100644 index 0000000..3fe637c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/badgeview/BadgeView.java @@ -0,0 +1,221 @@ +package com.hzecool.widget.badgeview; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.TabWidget; + +/** + * 自定义角标控件 + * Created by wangzg on 2017/4/12. + */ + +public class BadgeView extends AppCompatTextView { + + private boolean mHideOnNull = true; + + public BadgeView(Context context) { + this(context, null); + } + + public BadgeView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + public BadgeView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(); + } + + private void init() { + if (!(getLayoutParams() instanceof LayoutParams)) { + LayoutParams layoutParams = + new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.RIGHT | Gravity.TOP); + setLayoutParams(layoutParams); + } + + // set default font + setTextColor(Color.WHITE); + setTypeface(Typeface.DEFAULT_BOLD); + setTextSize(TypedValue.COMPLEX_UNIT_SP, 11); + setPadding(dip2Px(5), dip2Px(1), dip2Px(5), dip2Px(1)); + + // set default background + setBackground(9, Color.parseColor("#d3321b")); + + setGravity(Gravity.CENTER); + + // default values + setHideOnNull(true); + setBadgeCount("0"); + } + + public void setBackground(int dipRadius, int badgeColor) { + int radius = dip2Px(dipRadius); + float[] radiusArray = new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; + + RoundRectShape roundRect = new RoundRectShape(radiusArray, null, null); + ShapeDrawable bgDrawable = new ShapeDrawable(roundRect); + bgDrawable.getPaint().setColor(badgeColor); + setBackground(bgDrawable); + } + + /** + * @return Returns true if view is hidden on badge value 0 or null; + */ + public boolean isHideOnNull() { + return mHideOnNull; + } + + /** + * @param hideOnNull the hideOnNull to set + */ + public void setHideOnNull(boolean hideOnNull) { + mHideOnNull = hideOnNull; + setText(getText()); + } + + /* + * (non-Javadoc) + * + * @see android.widget.TextView#setText(java.lang.CharSequence, android.widget.TextView.BufferType) + */ + @Override + public void setText(CharSequence text, BufferType type) { + if (isHideOnNull() && (text == null || text.toString().equalsIgnoreCase("0")) || text.toString().equalsIgnoreCase("0.0")) { + setVisibility(View.GONE); + } else { + setVisibility(View.VISIBLE); + } + super.setText(text, type); + } + + public void setBadgeCount(String count) { + setText(String.valueOf(count)); + } + + public Integer getBadgeCount() { + if (getText() == null) { + return null; + } + + String text = getText().toString(); + try { + return Integer.parseInt(text); + } catch (NumberFormatException e) { + return null; + } + } + + public void setBadgeGravity(int gravity) { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + params.gravity = gravity; + setLayoutParams(params); + } + + public int getBadgeGravity() { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + return params.gravity; + } + + public void setBadgeMargin(int dipMargin) { + setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin); + } + + public void setBadgeMargin(int leftDipMargin, int topDipMargin, int rightDipMargin, int bottomDipMargin) { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + params.leftMargin = dip2Px(leftDipMargin); + params.topMargin = dip2Px(topDipMargin); + params.rightMargin = dip2Px(rightDipMargin); + params.bottomMargin = dip2Px(bottomDipMargin); + setLayoutParams(params); + } + + public int[] getBadgeMargin() { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + return new int[]{params.leftMargin, params.topMargin, params.rightMargin, params.bottomMargin}; + } + + public void incrementBadgeCount(String increment) { + Integer count = getBadgeCount(); + if (count == null) { + setBadgeCount(increment); + } else { + setBadgeCount(increment + count); + } + } + + /* + * Attach the BadgeView to the TabWidget + * + * @param target the TabWidget to attach the BadgeView + * + * @param tabIndex index of the tab + */ + public void setTargetView(TabWidget target, int tabIndex) { + View tabView = target.getChildTabViewAt(tabIndex); + setTargetView(tabView); + } + + /* + * Attach the BadgeView to the target view + * + * @param target the view to attach the BadgeView + */ + public void setTargetView(View target) { + if (getParent() != null) { + ((ViewGroup) getParent()).removeView(this); + } + + if (target == null) { + return; + } + + if (target.getParent() instanceof FrameLayout) { + ((FrameLayout) target.getParent()).addView(this); + + } else if (target.getParent() instanceof ViewGroup) { + // use a new Framelayout container for adding badge + ViewGroup parentContainer = (ViewGroup) target.getParent(); + int groupIndex = parentContainer.indexOfChild(target); + parentContainer.removeView(target); + + FrameLayout badgeContainer = new FrameLayout(getContext()); + ViewGroup.LayoutParams parentLayoutParams = target.getLayoutParams(); + + badgeContainer.setLayoutParams(parentLayoutParams); + target.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams); + badgeContainer.addView(target); + + badgeContainer.addView(this); + } else if (target.getParent() == null) { + Log.e(getClass().getSimpleName(), "ParentView is needed"); + } + + } + + /* + * converts dip to px + */ + private int dip2Px(float dip) { + return (int) (dip * getContext().getResources().getDisplayMetrics().density + 0.5f); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/circleimageview/CircleImageView.java b/widget/src/main/java/com/hzecool/widget/circleimageview/CircleImageView.java new file mode 100644 index 0000000..f697ef3 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/circleimageview/CircleImageView.java @@ -0,0 +1,440 @@ +/* + * Copyright 2014 - 2017 Henning Dodenhof + * + * 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. + */ +package com.hzecool.widget.circleimageview; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.hzecool.widget.R; + +public class CircleImageView extends android.support.v7.widget.AppCompatImageView { + + private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; + + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + private static final int COLORDRAWABLE_DIMENSION = 2; + + private static final int DEFAULT_BORDER_WIDTH = 0; + private static final int DEFAULT_BORDER_COLOR = Color.BLACK; + private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT; + private static final boolean DEFAULT_BORDER_OVERLAY = false; + + private final RectF mDrawableRect = new RectF(); + private final RectF mBorderRect = new RectF(); + + private final Matrix mShaderMatrix = new Matrix(); + private final Paint mBitmapPaint = new Paint(); + private final Paint mBorderPaint = new Paint(); + private final Paint mFillPaint = new Paint(); + + private int mBorderColor = DEFAULT_BORDER_COLOR; + private int mBorderWidth = DEFAULT_BORDER_WIDTH; + private int mFillColor = DEFAULT_FILL_COLOR; + + private Bitmap mBitmap; + private BitmapShader mBitmapShader; + private int mBitmapWidth; + private int mBitmapHeight; + + private float mDrawableRadius; + private float mBorderRadius; + + private ColorFilter mColorFilter; + + private boolean mReady; + private boolean mSetupPending; + private boolean mBorderOverlay; + private boolean mDisableCircularTransformation; + + public CircleImageView(Context context) { + super(context); + + init(); + } + + public CircleImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CircleImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0); + + mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH); + mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR); + mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY); + mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR); + + a.recycle(); + + init(); + } + + private void init() { + super.setScaleType(SCALE_TYPE); + mReady = true; + + if (mSetupPending) { + setup(); + mSetupPending = false; + } + } + + @Override + public ScaleType getScaleType() { + return SCALE_TYPE; + } + + @Override + public void setScaleType(ScaleType scaleType) { + if (scaleType != SCALE_TYPE) { + throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)); + } + } + + @Override + public void setAdjustViewBounds(boolean adjustViewBounds) { + if (adjustViewBounds) { + throw new IllegalArgumentException("adjustViewBounds not supported."); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDisableCircularTransformation) { + super.onDraw(canvas); + return; + } + + if (mBitmap == null) { + return; + } + + if (mFillColor != Color.TRANSPARENT) { + canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint); + } + canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint); + if (mBorderWidth > 0) { + canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + setup(); + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + super.setPadding(left, top, right, bottom); + setup(); + } + + @Override + public void setPaddingRelative(int start, int top, int end, int bottom) { + super.setPaddingRelative(start, top, end, bottom); + setup(); + } + + public int getBorderColor() { + return mBorderColor; + } + + public void setBorderColor(@ColorInt int borderColor) { + if (borderColor == mBorderColor) { + return; + } + + mBorderColor = borderColor; + mBorderPaint.setColor(mBorderColor); + invalidate(); + } + + /** + * @deprecated Use {@link #setBorderColor(int)} instead + */ + @Deprecated + public void setBorderColorResource(@ColorRes int borderColorRes) { + setBorderColor(getContext().getResources().getColor(borderColorRes)); + } + + /** + * Return the color drawn behind the circle-shaped drawable. + * + * @return The color drawn behind the drawable + * + * @deprecated Fill color support is going to be removed in the future + */ + @Deprecated + public int getFillColor() { + return mFillColor; + } + + /** + * Set a color to be drawn behind the circle-shaped drawable. Note that + * this has no effect if the drawable is opaque or no drawable is set. + * + * @param fillColor The color to be drawn behind the drawable + * + * @deprecated Fill color support is going to be removed in the future + */ + @Deprecated + public void setFillColor(@ColorInt int fillColor) { + if (fillColor == mFillColor) { + return; + } + + mFillColor = fillColor; + mFillPaint.setColor(fillColor); + invalidate(); + } + + /** + * Set a color to be drawn behind the circle-shaped drawable. Note that + * this has no effect if the drawable is opaque or no drawable is set. + * + * @param fillColorRes The color resource to be resolved to a color and + * drawn behind the drawable + * + * @deprecated Fill color support is going to be removed in the future + */ + @Deprecated + public void setFillColorResource(@ColorRes int fillColorRes) { + setFillColor(getContext().getResources().getColor(fillColorRes)); + } + + public int getBorderWidth() { + return mBorderWidth; + } + + public void setBorderWidth(int borderWidth) { + if (borderWidth == mBorderWidth) { + return; + } + + mBorderWidth = borderWidth; + setup(); + } + + public boolean isBorderOverlay() { + return mBorderOverlay; + } + + public void setBorderOverlay(boolean borderOverlay) { + if (borderOverlay == mBorderOverlay) { + return; + } + + mBorderOverlay = borderOverlay; + setup(); + } + + public boolean isDisableCircularTransformation() { + return mDisableCircularTransformation; + } + + public void setDisableCircularTransformation(boolean disableCircularTransformation) { + if (mDisableCircularTransformation == disableCircularTransformation) { + return; + } + + mDisableCircularTransformation = disableCircularTransformation; + initializeBitmap(); + } + + @Override + public void setImageBitmap(Bitmap bm) { + super.setImageBitmap(bm); + initializeBitmap(); + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + initializeBitmap(); + } + + @Override + public void setImageResource(@DrawableRes int resId) { + super.setImageResource(resId); + initializeBitmap(); + } + + @Override + public void setImageURI(Uri uri) { + super.setImageURI(uri); + initializeBitmap(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + if (cf == mColorFilter) { + return; + } + + mColorFilter = cf; + applyColorFilter(); + invalidate(); + } + + @Override + public ColorFilter getColorFilter() { + return mColorFilter; + } + + private void applyColorFilter() { + if (mBitmapPaint != null) { + mBitmapPaint.setColorFilter(mColorFilter); + } + } + + private Bitmap getBitmapFromDrawable(Drawable drawable) { + if (drawable == null) { + return null; + } + + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + try { + Bitmap bitmap; + + if (drawable instanceof ColorDrawable) { + bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG); + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private void initializeBitmap() { + if (mDisableCircularTransformation) { + mBitmap = null; + } else { + mBitmap = getBitmapFromDrawable(getDrawable()); + } + setup(); + } + + private void setup() { + if (!mReady) { + mSetupPending = true; + return; + } + + if (getWidth() == 0 && getHeight() == 0) { + return; + } + + if (mBitmap == null) { + invalidate(); + return; + } + + mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + + mBitmapPaint.setAntiAlias(true); + mBitmapPaint.setShader(mBitmapShader); + + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setAntiAlias(true); + mBorderPaint.setColor(mBorderColor); + mBorderPaint.setStrokeWidth(mBorderWidth); + + mFillPaint.setStyle(Paint.Style.FILL); + mFillPaint.setAntiAlias(true); + mFillPaint.setColor(mFillColor); + + mBitmapHeight = mBitmap.getHeight(); + mBitmapWidth = mBitmap.getWidth(); + + mBorderRect.set(calculateBounds()); + mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f); + + mDrawableRect.set(mBorderRect); + if (!mBorderOverlay && mBorderWidth > 0) { + mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f); + } + mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f); + + applyColorFilter(); + updateShaderMatrix(); + invalidate(); + } + + private RectF calculateBounds() { + int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + + int sideLength = Math.min(availableWidth, availableHeight); + + float left = getPaddingLeft() + (availableWidth - sideLength) / 2f; + float top = getPaddingTop() + (availableHeight - sideLength) / 2f; + + return new RectF(left, top, left + sideLength, top + sideLength); + } + + private void updateShaderMatrix() { + float scale; + float dx = 0; + float dy = 0; + + mShaderMatrix.set(null); + + if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { + scale = mDrawableRect.height() / (float) mBitmapHeight; + dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; + } else { + scale = mDrawableRect.width() / (float) mBitmapWidth; + dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; + } + + mShaderMatrix.setScale(scale, scale); + mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top); + + mBitmapShader.setLocalMatrix(mShaderMatrix); + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/colorpicker/ColorPickerView.java b/widget/src/main/java/com/hzecool/widget/colorpicker/ColorPickerView.java new file mode 100644 index 0000000..90ebca9 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/colorpicker/ColorPickerView.java @@ -0,0 +1,187 @@ +package com.hzecool.widget.colorpicker; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.ImageView; + +import com.hzecool.widget.R; + +public class ColorPickerView extends ImageView { + Context context; + private Bitmap iconBitMap; + float iconRadius;// 吸管圆的半径 + float iconCenterX; + float iconCenterY; + PointF iconPoint;// 点击位置坐标 + + public ColorPickerView(Context context) { + this(context, null); + } + + public ColorPickerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + init(); + } + + public ColorPickerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + init(); + } + + Paint mBitmapPaint; + Bitmap imageBitmap; + float viewRadius;// 整个view半径 + float radius;// 图片半径 + + /** + * 初始化画笔 + */ + private void init() { + iconBitMap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.pickup);// 吸管的图片 + iconRadius = iconBitMap.getWidth() / 2;// 吸管的图片一半 + + mBitmapPaint = new Paint(); + iconPoint = new PointF(); + + imageBitmap = ((BitmapDrawable) getDrawable()).getBitmap(); + radius = imageBitmap.getHeight() / 2;// 图片半径,180 + + // // 初始化 + iconPoint.x = radius; + iconPoint.y = radius; + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // TODO Auto-generated method stub + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + Canvas canvas; + + @Override + protected void onDraw(Canvas canvas) { + // TODO Auto-generated method stub + super.onDraw(canvas); + this.canvas = canvas; + + viewRadius = this.getWidth() / 2;// 整个view半径 + + canvas.drawBitmap(iconBitMap, iconPoint.x - iconRadius, iconPoint.y + - iconRadius, mBitmapPaint); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + int pixel; + int r; + int g; + int b; + switch (event.getAction()) { + case MotionEvent.ACTION_MOVE: + proofLeft(x, y); + pixel = getImagePixel(iconPoint.x, iconPoint.y); + r = Color.red(pixel); + g = Color.green(pixel); + b = Color.blue(pixel); + if (mChangedListener != null) { + mChangedListener.onMoveColor(r, g, b); + } + if (isMove) { + isMove = !isMove; + invalidate(); + } + break; + case MotionEvent.ACTION_UP: + pixel = getImagePixel(iconPoint.x, iconPoint.y); + r = Color.red(pixel); + g = Color.green(pixel); + b = Color.blue(pixel); + if (mChangedListener != null) { + mChangedListener.onColorChanged(r, g, b); + } + break; + + default: + break; + } + return true; + } + + public int getImagePixel(float x, float y) { + + Bitmap bitmap = imageBitmap; + // 为了防止越界 + int intX = (int) x; + int intY = (int) y; + if (intX < 0) + intX = 0; + if (intY < 0) + intY = 0; + if (intX >= bitmap.getWidth()) { + intX = bitmap.getWidth() - 1; + } + if (intY >= bitmap.getHeight()) { + intY = bitmap.getHeight() - 1; + } + int pixel = bitmap.getPixel(intX, intY); + return pixel; + + } + + /** + * R = sqrt(x * x + y * y)
+ * point.x = x * r / R + r
+ * point.y = y * r / R + r + */ + private void proofLeft(float x, float y) { + + float h = x - viewRadius; // 取xy点和圆点 的三角形宽 + float w = y - viewRadius;// 取xy点和圆点 的三角形长 + float h2 = h * h; + float w2 = w * w; + float distance = (float) Math.sqrt((h2 + w2)); // 勾股定理求 斜边距离 + if (distance > radius) { // 如果斜边距离大于半径,则取点和圆最近的一个点为x,y + float maxX = x - viewRadius; + float maxY = y - viewRadius; + x = ((radius * maxX) / distance) + viewRadius; // 通过三角形一边平行原理求出x,y + y = ((radius * maxY) / distance) + viewRadius; + } + iconPoint.x = x; + iconPoint.y = y; + + isMove = true; + } + + boolean isMove; + + public void setOnColorChangedListenner(OnColorChangedListener l) { + this.mChangedListener = l; + } + + private OnColorChangedListener mChangedListener; + + // 内部接口 回调颜色 rgb值 + public interface OnColorChangedListener { + // 手指抬起,确定颜色回调 + void onColorChanged(int r, int g, int b); + + // 移动时颜色回调 + void onMoveColor(int r, int g, int b); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelector.java b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelector.java new file mode 100644 index 0000000..ab0a1a9 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelector.java @@ -0,0 +1,117 @@ +package com.hzecool.widget.imgselector; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.widget.Toast; + +import com.hzecool.widget.R; + +import java.util.ArrayList; + +/** + * 图片选择器 + * Created by nereo on 16/3/17. + */ +public class MultiImageSelector { + + public static final String EXTRA_RESULT = MultiImageSelectorActivity.EXTRA_RESULT; + + private boolean mShowCamera = true; + private int mMaxCount = 9; + private int mMode = MultiImageSelectorActivity.MODE_MULTI; + private ArrayList mOriginData; + private static MultiImageSelector sSelector; + + @Deprecated + private MultiImageSelector(Context context){ + + } + + private MultiImageSelector(){} + + @Deprecated + public static MultiImageSelector create(Context context){ + if(sSelector == null){ + sSelector = new MultiImageSelector(context); + } + return sSelector; + } + + public static MultiImageSelector create(){ + if(sSelector == null){ + sSelector = new MultiImageSelector(); + } + return sSelector; + } + + public MultiImageSelector showCamera(boolean show){ + mShowCamera = show; + return sSelector; + } + + public MultiImageSelector count(int count){ + mMaxCount = count; + return sSelector; + } + + public MultiImageSelector single(){ + mMode = MultiImageSelectorActivity.MODE_SINGLE; + return sSelector; + } + + public MultiImageSelector multi(){ + mMode = MultiImageSelectorActivity.MODE_MULTI; + return sSelector; + } + + public MultiImageSelector origin(ArrayList images){ + mOriginData = images; + return sSelector; + } + + public void start(Activity activity, int requestCode){ + final Context context = activity; + if(hasPermission(context)) { + activity.startActivityForResult(createIntent(context), requestCode); + }else{ + Toast.makeText(context, R.string.mis_error_no_permission, Toast.LENGTH_SHORT).show(); + } + } + + public void start(Fragment fragment, int requestCode){ + final Context context = fragment.getContext(); + if(hasPermission(context)) { + fragment.startActivityForResult(createIntent(context), requestCode); + }else{ + Toast.makeText(context, R.string.mis_error_no_permission, Toast.LENGTH_SHORT).show(); + } + } + + private boolean hasPermission(Context context){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){ + // Permission was added in API Level 16 + return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + return true; + } + + private Intent createIntent(Context context){ + + Intent intent = new Intent(context, MultiImageSelectorActivity.class); + intent.putExtra(MultiImageSelectorActivity.EXTRA_SHOW_CAMERA, mShowCamera); + intent.putExtra(MultiImageSelectorActivity.EXTRA_SELECT_COUNT, mMaxCount); + if(mOriginData != null){ + intent.putStringArrayListExtra(MultiImageSelectorActivity.EXTRA_DEFAULT_SELECTED_LIST, mOriginData); + } + intent.putExtra(MultiImageSelectorActivity.EXTRA_SELECT_MODE, mMode); + + return intent; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorActivity.java b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorActivity.java new file mode 100644 index 0000000..e52f714 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorActivity.java @@ -0,0 +1,182 @@ +package com.hzecool.widget.imgselector; + +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; + +import com.hzecool.widget.R; + +import java.io.File; +import java.util.ArrayList; + +/** + * Multi image selector + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/1/19. + * Updated by nereo on 2016/5/18. + */ +public class MultiImageSelectorActivity extends AppCompatActivity + implements MultiImageSelectorFragment.Callback{ + + // Single choice + public static final int MODE_SINGLE = 0; + // Multi choice + public static final int MODE_MULTI = 1; + + /** Max image size,int,{@link #DEFAULT_IMAGE_SIZE} by default */ + public static final String EXTRA_SELECT_COUNT = "max_select_count"; + /** Select mode,{@link #MODE_MULTI} by default */ + public static final String EXTRA_SELECT_MODE = "select_count_mode"; + /** Whether show camera,true by default */ + public static final String EXTRA_SHOW_CAMERA = "show_camera"; + /** Result data set,ArrayList<String>*/ + public static final String EXTRA_RESULT = "select_result"; + /** Original data set */ + public static final String EXTRA_DEFAULT_SELECTED_LIST = "default_list"; + // Default image size + private static final int DEFAULT_IMAGE_SIZE = 9; + + private ArrayList resultList = new ArrayList<>(); + private Button mSubmitButton; + private int mDefaultCount = DEFAULT_IMAGE_SIZE; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(R.style.MIS_NO_ACTIONBAR); + setContentView(R.layout.mis_activity_default); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(Color.BLACK); + } + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + if(toolbar != null){ + setSupportActionBar(toolbar); + } + + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + final Intent intent = getIntent(); + mDefaultCount = intent.getIntExtra(EXTRA_SELECT_COUNT, DEFAULT_IMAGE_SIZE); + final int mode = intent.getIntExtra(EXTRA_SELECT_MODE, MODE_MULTI); + final boolean isShow = intent.getBooleanExtra(EXTRA_SHOW_CAMERA, true); + if(mode == MODE_MULTI && intent.hasExtra(EXTRA_DEFAULT_SELECTED_LIST)) { + resultList = intent.getStringArrayListExtra(EXTRA_DEFAULT_SELECTED_LIST); + } + + mSubmitButton = (Button) findViewById(R.id.commit); + if(mode == MODE_MULTI){ + updateDoneText(resultList); + mSubmitButton.setVisibility(View.VISIBLE); + mSubmitButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if(resultList != null && resultList.size() >0){ + // Notify success + Intent data = new Intent(); + data.putStringArrayListExtra(EXTRA_RESULT, resultList); + setResult(RESULT_OK, data); + }else{ + setResult(RESULT_CANCELED); + } + finish(); + } + }); + }else{ + mSubmitButton.setVisibility(View.GONE); + } + + if(savedInstanceState == null){ + Bundle bundle = new Bundle(); + bundle.putInt(MultiImageSelectorFragment.EXTRA_SELECT_COUNT, mDefaultCount); + bundle.putInt(MultiImageSelectorFragment.EXTRA_SELECT_MODE, mode); + bundle.putBoolean(MultiImageSelectorFragment.EXTRA_SHOW_CAMERA, isShow); + bundle.putStringArrayList(MultiImageSelectorFragment.EXTRA_DEFAULT_SELECTED_LIST, resultList); + + getSupportFragmentManager().beginTransaction() + .add(R.id.image_grid, Fragment.instantiate(this, MultiImageSelectorFragment.class.getName(), bundle)) + .commit(); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + setResult(RESULT_CANCELED); + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * Update done button by select image data + * @param resultList selected image data + */ + private void updateDoneText(ArrayList resultList){ + int size = 0; + if(resultList == null || resultList.size()<=0){ + mSubmitButton.setText(R.string.mis_action_done); + mSubmitButton.setEnabled(false); + }else{ + size = resultList.size(); + mSubmitButton.setEnabled(true); + } + mSubmitButton.setText(getString(R.string.mis_action_button_string, + getString(R.string.mis_action_done), size, mDefaultCount)); + } + + @Override + public void onSingleImageSelected(String path) { + Intent data = new Intent(); + resultList.add(path); + data.putStringArrayListExtra(EXTRA_RESULT, resultList); + setResult(RESULT_OK, data); + finish(); + } + + @Override + public void onImageSelected(String path) { + if(!resultList.contains(path)) { + resultList.add(path); + } + updateDoneText(resultList); + } + + @Override + public void onImageUnselected(String path) { + if(resultList.contains(path)){ + resultList.remove(path); + } + updateDoneText(resultList); + } + + @Override + public void onCameraShot(File imageFile) { + if(imageFile != null) { + // notify system the image has change + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(imageFile))); + + Intent data = new Intent(); + resultList.add(imageFile.getAbsolutePath()); + data.putStringArrayListExtra(EXTRA_RESULT, resultList); + setResult(RESULT_OK, data); + finish(); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorFragment.java b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorFragment.java new file mode 100644 index 0000000..d1b2381 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorFragment.java @@ -0,0 +1,567 @@ +package com.hzecool.widget.imgselector; + +import android.Manifest; +import android.app.Activity; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.ListPopupWindow; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.TextView; +import android.widget.Toast; + +import com.hzecool.widget.R; +import com.hzecool.widget.imgselector.adapter.FolderAdapter; +import com.hzecool.widget.imgselector.adapter.ImageGridAdapter; +import com.hzecool.widget.imgselector.bean.Folder; +import com.hzecool.widget.imgselector.bean.Image; +import com.hzecool.widget.imgselector.utils.FileUtils; +import com.hzecool.widget.imgselector.utils.ScreenUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +/** + * Multi image selector Fragment + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/5/18. + */ +public class MultiImageSelectorFragment extends Fragment { + + public static final String TAG = "MultiImageSelectorFragment"; + + private static final int REQUEST_STORAGE_WRITE_ACCESS_PERMISSION = 110; + private static final int REQUEST_CAMERA = 100; + + private static final String KEY_TEMP_FILE = "key_temp_file"; + + // Single choice + public static final int MODE_SINGLE = 0; + // Multi choice + public static final int MODE_MULTI = 1; + + /** + * Max image size,int, + */ + public static final String EXTRA_SELECT_COUNT = "max_select_count"; + /** + * Select mode,{@link #MODE_MULTI} by default + */ + public static final String EXTRA_SELECT_MODE = "select_count_mode"; + /** + * Whether show camera,true by default + */ + public static final String EXTRA_SHOW_CAMERA = "show_camera"; + /** + * Original data set + */ + public static final String EXTRA_DEFAULT_SELECTED_LIST = "default_list"; + + // loaders + private static final int LOADER_ALL = 0; + private static final int LOADER_CATEGORY = 1; + + // image result data set + private ArrayList resultList = new ArrayList<>(); + // folder result data set + private ArrayList mResultFolder = new ArrayList<>(); + + private GridView mGridView; + private Callback mCallback; + + private ImageGridAdapter mImageAdapter; + private FolderAdapter mFolderAdapter; + + private ListPopupWindow mFolderPopupWindow; + + private TextView mCategoryText; + private View mPopupAnchorView; + + private boolean hasFolderGened = false; + + private File mTmpFile; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + mCallback = (Callback) getActivity(); + } catch (ClassCastException e) { + throw new ClassCastException("The Activity must implement MultiImageSelectorFragment.Callback interface..."); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.mis_fragment_multi_image, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final int mode = selectMode(); + if (mode == MODE_MULTI) { + ArrayList tmp = getArguments().getStringArrayList(EXTRA_DEFAULT_SELECTED_LIST); + if (tmp != null && tmp.size() > 0) { + resultList = tmp; + } + } + mImageAdapter = new ImageGridAdapter(getActivity(), showCamera(), 3); + mImageAdapter.showSelectIndicator(mode == MODE_MULTI); + + mPopupAnchorView = view.findViewById(R.id.footer); + + mCategoryText = (TextView) view.findViewById(R.id.category_btn); + mCategoryText.setText(R.string.mis_folder_all); + mCategoryText.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + if (mFolderPopupWindow == null) { + createPopupFolderList(); + } + + if (mFolderPopupWindow.isShowing()) { + mFolderPopupWindow.dismiss(); + } else { + mFolderPopupWindow.show(); + int index = mFolderAdapter.getSelectIndex(); + index = index == 0 ? index : index - 1; + mFolderPopupWindow.getListView().setSelection(index); + } + } + }); + + mGridView = (GridView) view.findViewById(R.id.grid); + mGridView.setAdapter(mImageAdapter); + mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + if (mImageAdapter.isShowCamera()) { + if (i == 0) { + showCameraAction(); + } else { + Image image = (Image) adapterView.getAdapter().getItem(i); + selectImageFromGrid(image, mode); + } + } else { + Image image = (Image) adapterView.getAdapter().getItem(i); + selectImageFromGrid(image, mode); + } + } + }); + mGridView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { +// if (scrollState == SCROLL_STATE_FLING) { +// Glide.with(view.getContext()); +// } else { +// Glide.with(view.getContext()); +// } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + + } + }); + + mFolderAdapter = new FolderAdapter(getActivity()); + } + + /** + * Create popup ListView + */ + private void createPopupFolderList() { + Point point = ScreenUtils.getScreenSize(getActivity()); + int width = point.x; + int height = (int) (point.y * (4.5f / 8.0f)); + mFolderPopupWindow = new ListPopupWindow(getActivity()); + mFolderPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); + mFolderPopupWindow.setAdapter(mFolderAdapter); + mFolderPopupWindow.setContentWidth(width); + mFolderPopupWindow.setWidth(width); + mFolderPopupWindow.setHeight(height); + mFolderPopupWindow.setAnchorView(mPopupAnchorView); + mFolderPopupWindow.setModal(true); + mFolderPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + + mFolderAdapter.setSelectIndex(i); + + final int index = i; + final AdapterView v = adapterView; + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + mFolderPopupWindow.dismiss(); + + if (index == 0) { + getActivity().getSupportLoaderManager().restartLoader(LOADER_ALL, null, mLoaderCallback); + mCategoryText.setText(R.string.mis_folder_all); + if (showCamera()) { + mImageAdapter.setShowCamera(true); + } else { + mImageAdapter.setShowCamera(false); + } + } else { + Folder folder = (Folder) v.getAdapter().getItem(index); + if (null != folder) { + mImageAdapter.setData(folder.images); + mCategoryText.setText(folder.name); + if (resultList != null && resultList.size() > 0) { + mImageAdapter.setDefaultSelected(resultList); + } + } + mImageAdapter.setShowCamera(false); + } + + mGridView.smoothScrollToPosition(0); + } + }, 100); + + } + }); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(KEY_TEMP_FILE, mTmpFile); + } + + @Override + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + if (savedInstanceState != null) { + mTmpFile = (File) savedInstanceState.getSerializable(KEY_TEMP_FILE); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // load image data + getActivity().getSupportLoaderManager().initLoader(LOADER_ALL, null, mLoaderCallback); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_CAMERA) { + if (resultCode == Activity.RESULT_OK) { + if (mTmpFile != null) { + if (mCallback != null) { + mCallback.onCameraShot(mTmpFile); + } + } + } else { + // delete tmp file + while (mTmpFile != null && mTmpFile.exists()) { + boolean success = mTmpFile.delete(); + if (success) { + mTmpFile = null; + } + } + } + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + if (mFolderPopupWindow != null) { + if (mFolderPopupWindow.isShowing()) { + mFolderPopupWindow.dismiss(); + } + } + super.onConfigurationChanged(newConfig); + } + + /** + * Open camera + */ + private void showCameraAction() { + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, + getString(R.string.mis_permission_rationale_write_storage), + REQUEST_STORAGE_WRITE_ACCESS_PERMISSION); + } else { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + try { + mTmpFile = FileUtils.createTmpFile(getActivity()); + } catch (IOException e) { + e.printStackTrace(); + } + if (mTmpFile != null && mTmpFile.exists()) { +// intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTmpFile)); +// startActivityForResult(intent, REQUEST_CAMERA); + + int currentapiVersion = android.os.Build.VERSION.SDK_INT; + if (currentapiVersion<24){ + intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTmpFile)); + startActivityForResult(intent, REQUEST_CAMERA); + }else { + ContentValues contentValues = new ContentValues(1); + contentValues.put(MediaStore.Images.Media.DATA, mTmpFile.getAbsolutePath()); + Uri uri = getContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues); + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + startActivityForResult(intent, REQUEST_CAMERA); + } + + } else { + Toast.makeText(getActivity(), R.string.mis_error_image_not_exist, Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity(), R.string.mis_msg_no_camera, Toast.LENGTH_SHORT).show(); + } + } + } + + + private void doTakePhoto() { + try { + ContentValues values = new ContentValues(1); + values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpg"); + Uri mCameraTempUri = getActivity().getContentResolver() + .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + + takePhoto(this, REQUEST_CAMERA, mCameraTempUri); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void takePhoto(Fragment fragment, int token, Uri uri) { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (uri != null) { + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); + } + fragment.startActivityForResult(intent, token); + } + + private void requestPermission(final String permission, String rationale, final int requestCode) { + if (shouldShowRequestPermissionRationale(permission)) { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.mis_permission_dialog_title) + .setMessage(rationale) + .setPositiveButton(R.string.mis_permission_dialog_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + requestPermissions(new String[]{permission}, requestCode); + } + }) + .setNegativeButton(R.string.mis_permission_dialog_cancel, null) + .create().show(); + } else { + requestPermissions(new String[]{permission}, requestCode); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == REQUEST_STORAGE_WRITE_ACCESS_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showCameraAction(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + /** + * notify callback + * + * @param image image data + */ + private void selectImageFromGrid(Image image, int mode) { + if (image != null) { + if (mode == MODE_MULTI) { + if (resultList.contains(image.path)) { + resultList.remove(image.path); + if (mCallback != null) { + mCallback.onImageUnselected(image.path); + } + } else { + if (selectImageCount() == resultList.size()) { + Toast.makeText(getActivity(), R.string.mis_msg_amount_limit, Toast.LENGTH_SHORT).show(); + return; + } + resultList.add(image.path); + if (mCallback != null) { + mCallback.onImageSelected(image.path); + } + } + mImageAdapter.select(image); + } else if (mode == MODE_SINGLE) { + if (mCallback != null) { + mCallback.onSingleImageSelected(image.path); + } + } + } + } + + private LoaderManager.LoaderCallbacks mLoaderCallback = new LoaderManager.LoaderCallbacks() { + + private final String[] IMAGE_PROJECTION = { + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATE_ADDED, + MediaStore.Images.Media.MIME_TYPE, + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media._ID}; + + @Override + public Loader onCreateLoader(int id, Bundle args) { + CursorLoader cursorLoader = null; + if (id == LOADER_ALL) { + cursorLoader = new CursorLoader(getActivity(), + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, + IMAGE_PROJECTION[4] + ">0 AND " + IMAGE_PROJECTION[3] + "=? OR " + IMAGE_PROJECTION[3] + "=? ", + new String[]{"image/jpeg", "image/png"}, IMAGE_PROJECTION[2] + " DESC"); + } else if (id == LOADER_CATEGORY) { + cursorLoader = new CursorLoader(getActivity(), + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, + IMAGE_PROJECTION[4] + ">0 AND " + IMAGE_PROJECTION[0] + " like '%" + args.getString("path") + "%'", + null, IMAGE_PROJECTION[2] + " DESC"); + } + return cursorLoader; + } + + private boolean fileExist(String path) { + if (!TextUtils.isEmpty(path)) { + return new File(path).exists(); + } + return false; + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (data != null) { + if (data.getCount() > 0) { + List images = new ArrayList<>(); + data.moveToFirst(); + do { + String path = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[0])); + String name = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[1])); + long dateTime = data.getLong(data.getColumnIndexOrThrow(IMAGE_PROJECTION[2])); + if (!fileExist(path)) { + continue; + } + Image image = null; + if (!TextUtils.isEmpty(name)) { + image = new Image(path, name, dateTime); + images.add(image); + } + if (!hasFolderGened) { + // get all folder data + File folderFile = new File(path).getParentFile(); + if (folderFile != null && folderFile.exists()) { + String fp = folderFile.getAbsolutePath(); + Folder f = getFolderByPath(fp); + if (f == null) { + Folder folder = new Folder(); + folder.name = folderFile.getName(); + folder.path = fp; + folder.cover = image; + List imageList = new ArrayList<>(); + imageList.add(image); + folder.images = imageList; + mResultFolder.add(folder); + } else { + f.images.add(image); + } + } + } + + } while (data.moveToNext()); + + mImageAdapter.setData(images); + if (resultList != null && resultList.size() > 0) { + mImageAdapter.setDefaultSelected(resultList); + } + if (!hasFolderGened) { + mFolderAdapter.setData(mResultFolder); + hasFolderGened = true; + } + } + } + } + + @Override + public void onLoaderReset(Loader loader) { + + } + }; + + private Folder getFolderByPath(String path) { + if (mResultFolder != null) { + for (Folder folder : mResultFolder) { + if (TextUtils.equals(folder.path, path)) { + return folder; + } + } + } + return null; + } + + private boolean showCamera() { + return getArguments() == null || getArguments().getBoolean(EXTRA_SHOW_CAMERA, true); + } + + private int selectMode() { + return getArguments() == null ? MODE_MULTI : getArguments().getInt(EXTRA_SELECT_MODE); + } + + private int selectImageCount() { + return getArguments() == null ? 9 : getArguments().getInt(EXTRA_SELECT_COUNT); + } + + /** + * Callback for host activity + */ + public interface Callback { + void onSingleImageSelected(String path); + + void onImageSelected(String path); + + void onImageUnselected(String path); + + void onCameraShot(File imageFile); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/adapter/FolderAdapter.java b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/FolderAdapter.java new file mode 100644 index 0000000..142e027 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/FolderAdapter.java @@ -0,0 +1,178 @@ +package com.hzecool.widget.imgselector.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.hzecool.widget.R; +import com.hzecool.widget.imgselector.bean.Folder; +import com.hzecool.widget.utils.GlideSetting; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + + +/** + * 文件夹Adapter + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/1/19. + */ +public class FolderAdapter extends BaseAdapter { + + private Context mContext; + private LayoutInflater mInflater; + + private List mFolders = new ArrayList<>(); + + int mImageSize; + + int lastSelected = 0; + + public FolderAdapter(Context context) { + mContext = context; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mImageSize = mContext.getResources().getDimensionPixelOffset(R.dimen.mis_folder_cover_size); + } + + /** + * 设置数据集 + * + * @param folders + */ + public void setData(List folders) { + if (folders != null && folders.size() > 0) { + mFolders = folders; + } else { + mFolders.clear(); + } + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mFolders.size() + 1; + } + + @Override + public Folder getItem(int i) { + if (i == 0) return null; + return mFolders.get(i - 1); + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + ViewHolder holder; + if (view == null) { + view = mInflater.inflate(R.layout.mis_list_item_folder, viewGroup, false); + holder = new ViewHolder(view); + } else { + holder = (ViewHolder) view.getTag(); + } + if (holder != null) { + if (i == 0) { + holder.name.setText(R.string.mis_folder_all); + holder.path.setText("/sdcard"); + holder.size.setText(String.format("%d%s", + getTotalImageSize(), mContext.getResources().getString(R.string.mis_photo_unit))); + if (mFolders.size() > 0) { + Folder f = mFolders.get(0); + if (f != null) { + + Glide.with(mContext) + .load(new File(f.cover.path)) + .apply(GlideSetting.getGlideSetting() + .error(R.mipmap.mis_default_error) + .override(R.dimen.mis_folder_cover_size, R.dimen.mis_folder_cover_size) + .centerCrop() + ) + .into(holder.cover); + } else { + holder.cover.setImageResource(R.mipmap.mis_default_error); + } + } + } else { + holder.bindData(getItem(i)); + } + if (lastSelected == i) { + holder.indicator.setVisibility(View.VISIBLE); + } else { + holder.indicator.setVisibility(View.INVISIBLE); + } + } + return view; + } + + private int getTotalImageSize() { + int result = 0; + if (mFolders != null && mFolders.size() > 0) { + for (Folder f : mFolders) { + result += f.images.size(); + } + } + return result; + } + + public void setSelectIndex(int i) { + if (lastSelected == i) return; + + lastSelected = i; + notifyDataSetChanged(); + } + + public int getSelectIndex() { + return lastSelected; + } + + class ViewHolder { + ImageView cover; + TextView name; + TextView path; + TextView size; + ImageView indicator; + + ViewHolder(View view) { + cover = (ImageView) view.findViewById(R.id.cover); + name = (TextView) view.findViewById(R.id.name); + path = (TextView) view.findViewById(R.id.path); + size = (TextView) view.findViewById(R.id.size); + indicator = (ImageView) view.findViewById(R.id.indicator); + view.setTag(this); + } + + void bindData(Folder data) { + if (data == null) { + return; + } + name.setText(data.name); + path.setText(data.path); + if (data.images != null) { + size.setText(String.format("%d%s", data.images.size(), mContext.getResources().getString(R.string.mis_photo_unit))); + } else { + size.setText("*" + mContext.getResources().getString(R.string.mis_photo_unit)); + } + if (data.cover != null) { + Glide.with(mContext) + .load(new File(data.cover.path)) + .apply(GlideSetting.getGlideSetting() + .placeholder(R.mipmap.mis_default_error) + .override(R.dimen.mis_folder_cover_size, R.dimen.mis_folder_cover_size) + .centerCrop()) + .into(cover); + } else { + cover.setImageResource(R.mipmap.mis_default_error); + } + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/adapter/ImageGridAdapter.java b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/ImageGridAdapter.java new file mode 100644 index 0000000..9303e0c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/ImageGridAdapter.java @@ -0,0 +1,243 @@ +package com.hzecool.widget.imgselector.adapter; + +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.BaseAdapter; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.hzecool.widget.R; +import com.hzecool.widget.imgselector.bean.Image; +import com.hzecool.widget.utils.GlideSetting; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + + +/** + * 图片Adapter + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/1/19. + */ +public class ImageGridAdapter extends BaseAdapter { + + private static final int TYPE_CAMERA = 0; + private static final int TYPE_NORMAL = 1; + + private Context mContext; + + private LayoutInflater mInflater; + private boolean showCamera = true; + private boolean showSelectIndicator = true; + + private List mImages = new ArrayList<>(); + private List mSelectedImages = new ArrayList<>(); + + final int mGridWidth; + + public ImageGridAdapter(Context context, boolean showCamera, int column) { + mContext = context; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + this.showCamera = showCamera; + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + int width = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + Point size = new Point(); + wm.getDefaultDisplay().getSize(size); + width = size.x; + } else { + width = wm.getDefaultDisplay().getWidth(); + } + mGridWidth = width / column; + } + + /** + * 显示选择指示器 + * + * @param b + */ + public void showSelectIndicator(boolean b) { + showSelectIndicator = b; + } + + public void setShowCamera(boolean b) { + if (showCamera == b) return; + + showCamera = b; + notifyDataSetChanged(); + } + + public boolean isShowCamera() { + return showCamera; + } + + /** + * 选择某个图片,改变选择状态 + * + * @param image + */ + public void select(Image image) { + if (mSelectedImages.contains(image)) { + mSelectedImages.remove(image); + } else { + mSelectedImages.add(image); + } + notifyDataSetChanged(); + } + + /** + * 通过图片路径设置默认选择 + * + * @param resultList + */ + public void setDefaultSelected(ArrayList resultList) { + for (String path : resultList) { + Image image = getImageByPath(path); + if (image != null) { + mSelectedImages.add(image); + } + } + if (mSelectedImages.size() > 0) { + notifyDataSetChanged(); + } + } + + private Image getImageByPath(String path) { + if (mImages != null && mImages.size() > 0) { + for (Image image : mImages) { + if (image.path.equalsIgnoreCase(path)) { + return image; + } + } + } + return null; + } + + /** + * 设置数据集 + * + * @param images + */ + public void setData(List images) { + mSelectedImages.clear(); + + if (images != null && images.size() > 0) { + mImages = images; + } else { + mImages.clear(); + } + notifyDataSetChanged(); + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public int getItemViewType(int position) { + if (showCamera) { + return position == 0 ? TYPE_CAMERA : TYPE_NORMAL; + } + return TYPE_NORMAL; + } + + @Override + public int getCount() { + return showCamera ? mImages.size() + 1 : mImages.size(); + } + + @Override + public Image getItem(int i) { + if (showCamera) { + if (i == 0) { + return null; + } + return mImages.get(i - 1); + } else { + return mImages.get(i); + } + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + + if (isShowCamera()) { + if (i == 0) { + view = mInflater.inflate(R.layout.mis_list_item_camera, viewGroup, false); + return view; + } + } + + ViewHolder holder; + if (view == null) { + view = mInflater.inflate(R.layout.mis_list_item_image, viewGroup, false); + holder = new ViewHolder(view); + } else { + holder = (ViewHolder) view.getTag(); + } + + if (holder != null) { + holder.bindData(getItem(i)); + } + + return view; + } + + class ViewHolder { + ImageView image; + ImageView indicator; + View mask; + + ViewHolder(View view) { + image = (ImageView) view.findViewById(R.id.image); + indicator = (ImageView) view.findViewById(R.id.checkmark); + mask = view.findViewById(R.id.mask); + view.setTag(this); + } + + void bindData(final Image data) { + if (data == null) return; + // 处理单选和多选状态 + if (showSelectIndicator) { + indicator.setVisibility(View.VISIBLE); + if (mSelectedImages.contains(data)) { + // 设置选中状态 + indicator.setImageResource(R.mipmap.mis_btn_selected); + mask.setVisibility(View.VISIBLE); + } else { + // 未选择 + indicator.setImageResource(R.mipmap.mis_btn_unselected); + mask.setVisibility(View.GONE); + } + } else { + indicator.setVisibility(View.GONE); + } + File imageFile = new File(data.path); + if (imageFile.exists()) { + + Glide.with(mContext) + .load(imageFile) + .apply(GlideSetting.getGlideSetting() + .placeholder(R.mipmap.mis_default_error) + .override(mGridWidth, mGridWidth) + .centerCrop()) + .into(image); + } else { + image.setImageResource(R.mipmap.mis_default_error); + } + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/bean/Folder.java b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Folder.java new file mode 100644 index 0000000..3981711 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Folder.java @@ -0,0 +1,27 @@ +package com.hzecool.widget.imgselector.bean; + +import android.text.TextUtils; + +import java.util.List; + +/** + * 文件夹 + * Created by Nereo on 2015/4/7. + */ +public class Folder { + public String name; + public String path; + public Image cover; + public List images; + + @Override + public boolean equals(Object o) { + try { + Folder other = (Folder) o; + return TextUtils.equals(other.path, path); + }catch (ClassCastException e){ + e.printStackTrace(); + } + return super.equals(o); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/bean/Image.java b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Image.java new file mode 100644 index 0000000..0fd1bbc --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Image.java @@ -0,0 +1,30 @@ +package com.hzecool.widget.imgselector.bean; + +import android.text.TextUtils; + +/** + * 图片实体 + * Created by Nereo on 2015/4/7. + */ +public class Image { + public String path; + public String name; + public long time; + + public Image(String path, String name, long time){ + this.path = path; + this.name = name; + this.time = time; + } + + @Override + public boolean equals(Object o) { + try { + Image other = (Image) o; + return TextUtils.equals(this.path, other.path); + }catch (ClassCastException e){ + e.printStackTrace(); + } + return super.equals(o); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/utils/FileUtils.java b/widget/src/main/java/com/hzecool/widget/imgselector/utils/FileUtils.java new file mode 100644 index 0000000..57c0885 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/utils/FileUtils.java @@ -0,0 +1,129 @@ +package com.hzecool.widget.imgselector.utils; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.text.TextUtils; + +import java.io.File; +import java.io.IOException; + +import static android.os.Environment.MEDIA_MOUNTED; + +/** + * 文件操作类 + * Created by Nereo on 2015/4/8. + */ +public class FileUtils { + + private static final String JPEG_FILE_PREFIX = "IMG_"; + private static final String JPEG_FILE_SUFFIX = ".jpg"; + + public static File createTmpFile(Context context) throws IOException{ + File dir = null; + if(TextUtils.equals(Environment.getExternalStorageState(), Environment.MEDIA_MOUNTED)) { + dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + if (!dir.exists()) { + dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM + "/Camera"); + if (!dir.exists()) { + dir = getCacheDirectory(context, true); + } + } + }else{ + dir = getCacheDirectory(context, true); + } + return File.createTempFile(JPEG_FILE_PREFIX, JPEG_FILE_SUFFIX, dir); + } + + + private static final String EXTERNAL_STORAGE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE"; + + /** + * Returns application cache directory. Cache directory will be created on SD card + * ("/Android/data/[app_package_name]/cache") if card is mounted and app has appropriate permission. Else - + * Android defines cache directory on device's file system. + * + * @param context Application context + * @return Cache {@link File directory}.
+ * NOTE: Can be null in some unpredictable cases (if SD card is unmounted and + * {@link Context#getCacheDir() Context.getCacheDir()} returns null). + */ + public static File getCacheDirectory(Context context) { + return getCacheDirectory(context, true); + } + + /** + * Returns application cache directory. Cache directory will be created on SD card + * ("/Android/data/[app_package_name]/cache") (if card is mounted and app has appropriate permission) or + * on device's file system depending incoming parameters. + * + * @param context Application context + * @param preferExternal Whether prefer external location for cache + * @return Cache {@link File directory}.
+ * NOTE: Can be null in some unpredictable cases (if SD card is unmounted and + * {@link Context#getCacheDir() Context.getCacheDir()} returns null). + */ + public static File getCacheDirectory(Context context, boolean preferExternal) { + File appCacheDir = null; + String externalStorageState; + try { + externalStorageState = Environment.getExternalStorageState(); + } catch (NullPointerException e) { // (sh)it happens (Issue #660) + externalStorageState = ""; + } catch (IncompatibleClassChangeError e) { // (sh)it happens too (Issue #989) + externalStorageState = ""; + } + if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState) && hasExternalStoragePermission(context)) { + appCacheDir = getExternalCacheDir(context); + } + if (appCacheDir == null) { + appCacheDir = context.getCacheDir(); + } + if (appCacheDir == null) { + String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/"; + appCacheDir = new File(cacheDirPath); + } + return appCacheDir; + } + + /** + * Returns individual application cache directory (for only image caching from ImageLoader). Cache directory will be + * created on SD card ("/Android/data/[app_package_name]/cache/uil-images") if card is mounted and app has + * appropriate permission. Else - Android defines cache directory on device's file system. + * + * @param context Application context + * @param cacheDir Cache directory path (e.g.: "AppCacheDir", "AppDir/cache/images") + * @return Cache {@link File directory} + */ + public static File getIndividualCacheDirectory(Context context, String cacheDir) { + File appCacheDir = getCacheDirectory(context); + File individualCacheDir = new File(appCacheDir, cacheDir); + if (!individualCacheDir.exists()) { + if (!individualCacheDir.mkdir()) { + individualCacheDir = appCacheDir; + } + } + return individualCacheDir; + } + + private static File getExternalCacheDir(Context context) { + File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"); + File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache"); + if (!appCacheDir.exists()) { + if (!appCacheDir.mkdirs()) { + return null; + } + try { + new File(appCacheDir, ".nomedia").createNewFile(); + } catch (IOException e) { + } + } + return appCacheDir; + } + + private static boolean hasExternalStoragePermission(Context context) { + int perm = context.checkCallingOrSelfPermission(EXTERNAL_STORAGE_PERMISSION); + return perm == PackageManager.PERMISSION_GRANTED; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/utils/ScreenUtils.java b/widget/src/main/java/com/hzecool/widget/imgselector/utils/ScreenUtils.java new file mode 100644 index 0000000..d8484ee --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/utils/ScreenUtils.java @@ -0,0 +1,29 @@ +package com.hzecool.widget.imgselector.utils; + +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.view.Display; +import android.view.WindowManager; + +/** + * 屏幕工具 + * Created by nereo on 15/11/19. + * Updated by nereo on 2016/1/19. + */ +public class ScreenUtils { + + public static Point getScreenSize(Context context){ + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point out = new Point(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + display.getSize(out); + }else{ + int width = display.getWidth(); + int height = display.getHeight(); + out.set(width, height); + } + return out; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/utils/TimeUtils.java b/widget/src/main/java/com/hzecool/widget/imgselector/utils/TimeUtils.java new file mode 100644 index 0000000..99c9e2c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/utils/TimeUtils.java @@ -0,0 +1,31 @@ +package com.hzecool.widget.imgselector.utils; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * 时间处理工具 + * Created by Nereo on 2015/4/8. + */ +public class TimeUtils { + + public static String timeFormat(long timeMillis, String pattern){ + SimpleDateFormat format = new SimpleDateFormat(pattern, Locale.CHINA); + return format.format(new Date(timeMillis)); + } + + public static String formatPhotoDate(long time){ + return timeFormat(time, "yyyy-MM-dd"); + } + + public static String formatPhotoDate(String path){ + File file = new File(path); + if(file.exists()){ + long time = file.lastModified(); + return formatPhotoDate(time); + } + return "1970-01-01"; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/view/SquareFrameLayout.java b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquareFrameLayout.java new file mode 100644 index 0000000..7d0e43d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquareFrameLayout.java @@ -0,0 +1,24 @@ +package com.hzecool.widget.imgselector.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * Created by nereo on 15/11/10. + */ +public class SquareFrameLayout extends FrameLayout{ + public SquareFrameLayout(Context context) { + super(context); + } + + public SquareFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/view/SquaredImageView.java b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquaredImageView.java new file mode 100644 index 0000000..96a8840 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquaredImageView.java @@ -0,0 +1,24 @@ +package com.hzecool.widget.imgselector.view; + +import android.content.Context; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; + +/** + * An image view which always remains square with respect to its width. + */ +public class SquaredImageView extends AppCompatImageView { + public SquaredImageView(Context context) { + super(context); + } + + public SquaredImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayout.java b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayout.java new file mode 100644 index 0000000..695ee79 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayout.java @@ -0,0 +1,685 @@ +package com.hzecool.widget.loadingLayout; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntDef; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.hzecool.widget.R; + + +/** + * create by tutu 2017/1/11 + * 全局加载底层布局 + * 提供加载成功,加载失败,网络连接失败,加载中四种状态 + */ +public class LoadingLayout extends FrameLayout { + + public final static int Success = 0; + public final static int Empty = 1; + public final static int Error = 2; + public final static int No_Network = 3; + public final static int Loading = 4; + private int state; + + private Context mContext; + private View loadingPage; + private View errorPage; + private View emptyPage; + private View networkPage; + private View defineLoadingPage; + + private ImageView errorImg; + private ImageView emptyImg; + private ImageView networkImg; + + private TextView errorText; + private TextView emptyText; + private TextView networkText; + + private TextView errorReloadBtn; + private TextView networkReloadBtn; + + private View contentView; + private OnReloadListener listener; + private boolean isFirstVisible; //是否一开始显示contentview,默认不显示 + + //配置 + private static Config mConfig = new Config(); + private static String emptyStr = "暂无数据"; + private static String errorStr = "加载失败,请稍后重试···"; + private static String netwrokStr = "无网络连接,请检查网络···"; + private static String reloadBtnStr = "点击重试"; + private static int emptyImgId = R.mipmap.empty; + private static int errorImgId = R.mipmap.error; + private static int networkImgId = R.mipmap.no_network; + private static int reloadBtnId = R.drawable.selector_btn_back_gray; + private static int tipTextSize = 14; + private static int buttonTextSize = 14; + private static int tipTextColor = R.color.black; + private static int buttonTextColor = R.color.black; + private static int buttonWidth = -1; + private static int buttonHeight = -1; + private static int loadingLayoutId = R.layout.widget_loading_page; + private static int backgroundColor = R.color.loading_background; + + public LoadingLayout(Context context, AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoadingLayout); + isFirstVisible = a.getBoolean(R.styleable.LoadingLayout_isFirstVisible, false); + a.recycle(); + } + + public LoadingLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.mContext = context; + } + + public LoadingLayout(Context context) { + super(context); + this.mContext = context; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (getChildCount() > 1) { + throw new IllegalStateException("LoadingLayout can host only one direct child"); + } + contentView = this.getChildAt(0); + if (!isFirstVisible) { + contentView.setVisibility(View.GONE); + } + build(); + } + + private void build() { + + loadingPage = LayoutInflater.from(mContext).inflate(loadingLayoutId, null); + errorPage = LayoutInflater.from(mContext).inflate(R.layout.widget_error_page, null); + emptyPage = LayoutInflater.from(mContext).inflate(R.layout.widget_empty_page, null); + networkPage = LayoutInflater.from(mContext).inflate(R.layout.widget_nonetwork_page, null); + defineLoadingPage = null; + loadingPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + errorPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + emptyPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + networkPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + + errorText = LoadingLayoutUtils.findViewById(errorPage, R.id.error_text); + emptyText = LoadingLayoutUtils.findViewById(emptyPage, R.id.empty_text); + networkText = LoadingLayoutUtils.findViewById(networkPage, R.id.no_network_text); + + errorImg = LoadingLayoutUtils.findViewById(errorPage, R.id.error_img); + emptyImg = LoadingLayoutUtils.findViewById(emptyPage, R.id.empty_img); + networkImg = LoadingLayoutUtils.findViewById(networkPage, R.id.no_network_img); + + errorReloadBtn = LoadingLayoutUtils.findViewById(errorPage, R.id.error_reload_btn); + networkReloadBtn = LoadingLayoutUtils.findViewById(networkPage, R.id.no_network_reload_btn); + + errorReloadBtn.setOnClickListener(v -> { + + if (listener != null) { + listener.onReload(v); + } + }); + networkReloadBtn.setOnClickListener(v -> { + + if (listener != null) { + listener.onReload(v); + } + }); + + errorText.setText(errorStr); + emptyText.setText(emptyStr); + networkText.setText(netwrokStr); + + errorText.setTextSize(tipTextSize); + emptyText.setTextSize(tipTextSize); + networkText.setTextSize(tipTextSize); + + errorText.setTextColor(LoadingLayoutUtils.getColor(mContext, tipTextColor)); + emptyText.setTextColor(LoadingLayoutUtils.getColor(mContext, tipTextColor)); + networkText.setTextColor(LoadingLayoutUtils.getColor(mContext, tipTextColor)); + + errorImg.setImageResource(errorImgId); + emptyImg.setImageResource(emptyImgId); + networkImg.setImageResource(networkImgId); + + + errorReloadBtn.setBackgroundResource(reloadBtnId); + networkReloadBtn.setBackgroundResource(reloadBtnId); + + errorReloadBtn.setText(reloadBtnStr); + networkReloadBtn.setText(reloadBtnStr); + + errorReloadBtn.setTextSize(buttonTextSize); + networkReloadBtn.setTextSize(buttonTextSize); + + errorReloadBtn.setTextColor(LoadingLayoutUtils.getColor(mContext, buttonTextColor)); + networkReloadBtn.setTextColor(LoadingLayoutUtils.getColor(mContext, buttonTextColor)); + + if (buttonHeight != -1) { + + errorReloadBtn.setHeight(LoadingLayoutUtils.dp2px(mContext, buttonHeight)); + networkReloadBtn.setHeight(LoadingLayoutUtils.dp2px(mContext, buttonHeight)); + } + if (buttonWidth != -1) { + + errorReloadBtn.setWidth(LoadingLayoutUtils.dp2px(mContext, buttonWidth)); + networkReloadBtn.setWidth(LoadingLayoutUtils.dp2px(mContext, buttonWidth)); + } + + this.addView(networkPage); + this.addView(emptyPage); + this.addView(errorPage); + this.addView(loadingPage); + } + + public void setStatus(@Flavour int status) { + + this.state = status; + + switch (status) { + case Success: + + contentView.setVisibility(View.VISIBLE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + case Loading: + + contentView.setVisibility(View.GONE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + defineLoadingPage.setVisibility(View.VISIBLE); + } else { + loadingPage.setVisibility(View.VISIBLE); + } + break; + + case Empty: + + contentView.setVisibility(View.GONE); + emptyPage.setVisibility(View.VISIBLE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + case Error: + + contentView.setVisibility(View.GONE); + loadingPage.setVisibility(View.GONE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.VISIBLE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + case No_Network: + + contentView.setVisibility(View.GONE); + loadingPage.setVisibility(View.GONE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.VISIBLE); + if (defineLoadingPage != null) { + + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + default: + break; + } + + } + + + /** + * 返回当前状态{Success, Empty, Error, No_Network, Loading} + * + * @return + */ + public int getStatus() { + + return state; + } + + /** + * 设置Empty状态提示文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setEmptyText(String text) { + + emptyText.setText(text); + return this; + } + + /** + * 设置Error状态提示文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setErrorText(String text) { + + errorText.setText(text); + return this; + } + + /** + * 设置No_Network状态提示文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setNoNetworkText(String text) { + + networkText.setText(text); + return this; + } + + /** + * 设置Empty状态显示图片,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setEmptyImage(@DrawableRes int id) { + + + emptyImg.setImageResource(id); + return this; + } + + /** + * 设置Error状态显示图片,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setErrorImage(@DrawableRes int id) { + + errorImg.setImageResource(id); + return this; + } + + /** + * 设置No_Network状态显示图片,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setNoNetworkImage(@DrawableRes int id) { + + networkImg.setImageResource(id); + return this; + } + + /** + * 设置Empty状态提示文本的字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setEmptyTextSize(int sp) { + + emptyText.setTextSize(sp); + return this; + } + + /** + * 设置Error状态提示文本的字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setErrorTextSize(int sp) { + + errorText.setTextSize(sp); + return this; + } + + /** + * 设置No_Network状态提示文本的字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setNoNetworkTextSize(int sp) { + + networkText.setTextSize(sp); + return this; + } + + /** + * 设置Empty状态图片的显示与否,仅对当前所在的地方有效 + * + * @param bool + * @return + */ + public LoadingLayout setEmptyImageVisible(boolean bool) { + + if (bool) { + emptyImg.setVisibility(View.VISIBLE); + } else { + emptyImg.setVisibility(View.GONE); + } + return this; + } + + /** + * 设置Error状态图片的显示与否,仅对当前所在的地方有效 + * + * @param bool + * @return + */ + public LoadingLayout setErrorImageVisible(boolean bool) { + + if (bool) { + errorImg.setVisibility(View.VISIBLE); + } else { + errorImg.setVisibility(View.GONE); + } + return this; + } + + /** + * 设置No_Network状态图片的显示与否,仅对当前所在的地方有效 + * + * @param bool + * @return + */ + public LoadingLayout setNoNetworkImageVisible(boolean bool) { + + if (bool) { + networkImg.setVisibility(View.VISIBLE); + } else { + networkImg.setVisibility(View.GONE); + } + return this; + } + + /** + * 设置ReloadButton的文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setReloadButtonText(@NonNull String text) { + + errorReloadBtn.setText(text); + networkReloadBtn.setText(text); + return this; + } + + /** + * 设置ReloadButton的文本字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setReloadButtonTextSize(int sp) { + + errorReloadBtn.setTextSize(sp); + networkReloadBtn.setTextSize(sp); + return this; + } + + /** + * 设置ReloadButton的文本颜色,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setReloadButtonTextColor(@ColorRes int id) { + + errorReloadBtn.setTextColor(LoadingLayoutUtils.getColor(mContext, id)); + networkReloadBtn.setTextSize(LoadingLayoutUtils.getColor(mContext, id)); + return this; + } + + /** + * 设置ReloadButton的背景,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setReloadButtonBackgroundResource(@DrawableRes int id) { + + errorReloadBtn.setBackgroundResource(id); + networkReloadBtn.setBackgroundResource(id); + return this; + } + + /** + * 设置ReloadButton的监听器 + * + * @param listener + * @return + */ + public LoadingLayout setOnReloadListener(OnReloadListener listener) { + + this.listener = listener; + return this; + } + + /** + * 自定义加载页面,仅对当前所在的Activity有效 + * + * @param view + * @return + */ + public LoadingLayout setLoadingPage(View view) { + + defineLoadingPage = view; + this.removeView(loadingPage); + defineLoadingPage.setVisibility(View.GONE); + this.addView(view); + return this; + } + + public LoadingLayout setEmptyImageViewClickListener(OnClickListener viewClickListener){ + emptyImg.setOnClickListener(viewClickListener); + return this; + } + + /** + * 自定义加载页面,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setLoadingPage(@LayoutRes int id) { + + this.removeView(loadingPage); + View view = LayoutInflater.from(mContext).inflate(id, null); + defineLoadingPage = view; + defineLoadingPage.setVisibility(View.GONE); + this.addView(view); + return this; + } + + /** + * 获取当前自定义的loadingpage + * + * @return + */ + public View getLoadingPage() { + + return defineLoadingPage; + } + + + /** + * 获取全局使用的loadingpage + * + * @return + */ + public View getGlobalLoadingPage() { + + return loadingPage; + } + + @IntDef({Success, Empty, Error, No_Network, Loading}) + public @interface Flavour { + + } + + public interface OnReloadListener { + + void onReload(View v); + } + + /** + * 获取全局配置的class + * + * @return + */ + public static Config getConfig() { + + return mConfig; + } + + /** + * 全局配置的Class,对所有使用到的地方有效 + */ + public static class Config { + + public Config setErrorText(@NonNull String text) { + + errorStr = text; + return mConfig; + } + + public Config setEmptyText(@NonNull String text) { + + emptyStr = text; + return mConfig; + } + + public Config setNoNetworkText(@NonNull String text) { + + netwrokStr = text; + return mConfig; + } + + public Config setReloadButtonText(@NonNull String text) { + + reloadBtnStr = text; + return mConfig; + } + + /** + * 设置所有提示文本的字体大小 + * + * @param sp + * @return + */ + public Config setAllTipTextSize(int sp) { + + tipTextSize = sp; + return mConfig; + } + + /** + * 设置所有提示文本的字体颜色 + * + * @param color + * @return + */ + public Config setAllTipTextColor(@ColorRes int color) { + + tipTextColor = color; + return mConfig; + } + + public Config setReloadButtonTextSize(int sp) { + + buttonTextSize = sp; + return mConfig; + } + + public Config setReloadButtonTextColor(@ColorRes int color) { + + buttonTextColor = color; + return mConfig; + } + + public Config setReloadButtonBackgroundResource(@DrawableRes int id) { + + reloadBtnId = id; + return mConfig; + } + + public Config setReloadButtonWidthAndHeight(int width_dp, int height_dp) { + + buttonWidth = width_dp; + buttonHeight = height_dp; + return mConfig; + } + + public Config setErrorImage(@DrawableRes int id) { + + errorImgId = id; + return mConfig; + } + + public Config setEmptyImage(@DrawableRes int id) { + + emptyImgId = id; + return this; + } + + public Config setNoNetworkImage(@DrawableRes int id) { + + networkImgId = id; + return mConfig; + } + + public Config setLoadingPageLayout(@LayoutRes int id) { + + loadingLayoutId = id; + return mConfig; + } + + public Config setAllPageBackgroundColor(@ColorRes int color) { + + backgroundColor = color; + return mConfig; + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayoutUtils.java b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayoutUtils.java new file mode 100644 index 0000000..81d26f6 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayoutUtils.java @@ -0,0 +1,48 @@ +package com.hzecool.widget.loadingLayout; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.view.View; + +/** + * create by tutu + * on date 2017-01-11 + */ + +public class LoadingLayoutUtils { + + + public static Drawable getDrawble(Context conetxt, @DrawableRes int id) { + return ContextCompat.getDrawable(conetxt, id); + } + + public static int getColor(Context conetxt, @ColorRes int id) { + return ContextCompat.getColor(conetxt, id); + } + + public static String getString(Context conetxt, @StringRes int id) { + return conetxt.getResources().getString(id); + } + + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getResources().getDisplayMetrics() + .scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + public static int dp2px(Context context, int dip) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dip * scale + 0.5f); + } + + public static T findViewById(View v, int id) { + + + return (T) v.findViewById(id); + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DefaultRvAdapter.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DefaultRvAdapter.java new file mode 100644 index 0000000..22970d4 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DefaultRvAdapter.java @@ -0,0 +1,210 @@ +package com.hzecool.widget.materialdialog; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; +import android.support.annotation.LayoutRes; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.internal.MDTintHelper; +import com.hzecool.widget.materialdialog.util.DialogUtils; + + +/** + * @author Aidan Follestad (afollestad) + */ +class DefaultRvAdapter extends RecyclerView.Adapter { + + private final MaterialDialog dialog; + @LayoutRes + private final int layout; + private final GravityEnum itemGravity; + private InternalListCallback callback; + + DefaultRvAdapter(MaterialDialog dialog, @LayoutRes int layout) { + this.dialog = dialog; + this.layout = layout; + this.itemGravity = dialog.builder.itemsGravity; + } + + void setCallback(InternalListCallback callback) { + this.callback = callback; + } + + @Override + public DefaultVH onCreateViewHolder(ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(parent.getContext()) + .inflate(layout, parent, false); + DialogUtils.setBackgroundCompat(view, dialog.getListSelector()); + return new DefaultVH(view, this); + } + + @Override + public void onBindViewHolder(DefaultVH holder, int index) { + final View view = holder.itemView; + boolean disabled = DialogUtils.isIn(index, dialog.builder.disabledIndices); + switch (dialog.listType) { + case SINGLE: { + @SuppressLint("CutPasteId") RadioButton radio = (RadioButton) holder.control; + boolean selected = dialog.builder.selectedIndex == index; + if (dialog.builder.choiceWidgetColor != null) { + MDTintHelper.setTint(radio, dialog.builder.choiceWidgetColor); + } else { + MDTintHelper.setTint(radio, dialog.builder.widgetColor); + } + radio.setChecked(selected); + radio.setEnabled(!disabled); + break; + } + case MULTI: { + @SuppressLint("CutPasteId") CheckBox checkbox = (CheckBox) holder.control; + boolean selected = dialog.selectedIndicesList.contains(index); + if (dialog.builder.choiceWidgetColor != null) { + MDTintHelper.setTint(checkbox, dialog.builder.choiceWidgetColor); + } else { + MDTintHelper.setTint(checkbox, dialog.builder.widgetColor); + } + checkbox.setChecked(selected); + checkbox.setEnabled(!disabled); + break; + } + } + + holder.title.setText(dialog.builder.items.get(index)); + holder.title.setTextColor(dialog.builder.itemColor); + dialog.setTypeface(holder.title, dialog.builder.regularFont); + + setupGravity((ViewGroup) view); + + if (dialog.builder.itemIds != null) { + if (index < dialog.builder.itemIds.length) { + view.setId(dialog.builder.itemIds[index]); + } else { + view.setId(-1); + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ViewGroup group = (ViewGroup) view; + if (group.getChildCount() == 2) { + // Remove circular selector from check boxes and radio buttons on Lollipop + if (group.getChildAt(0) instanceof CompoundButton) { + group.getChildAt(0).setBackground(null); + } else if (group.getChildAt(1) instanceof CompoundButton) { + group.getChildAt(1).setBackground(null); + } + } + } + } + + @Override + public int getItemCount() { + return dialog.builder.items != null ? dialog.builder.items.size() : 0; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setupGravity(ViewGroup view) { + final LinearLayout itemRoot = (LinearLayout) view; + final int gravityInt = itemGravity.getGravityInt(); + itemRoot.setGravity(gravityInt | Gravity.CENTER_VERTICAL); + + if (view.getChildCount() == 2) { + if (itemGravity == GravityEnum.END && !isRTL() && view + .getChildAt(0) instanceof CompoundButton) { + CompoundButton first = (CompoundButton) view.getChildAt(0); + view.removeView(first); + + TextView second = (TextView) view.getChildAt(0); + view.removeView(second); + second.setPadding(second.getPaddingRight(), second.getPaddingTop(), + second.getPaddingLeft(), second.getPaddingBottom()); + + view.addView(second); + view.addView(first); + } else if (itemGravity == GravityEnum.START && isRTL() && view + .getChildAt(1) instanceof CompoundButton) { + CompoundButton first = (CompoundButton) view.getChildAt(1); + view.removeView(first); + + TextView second = (TextView) view.getChildAt(0); + view.removeView(second); + second.setPadding(second.getPaddingRight(), second.getPaddingTop(), + second.getPaddingRight(), second.getPaddingBottom()); + + view.addView(first); + view.addView(second); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private boolean isRTL() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return false; + } + Configuration config = dialog.getBuilder().getContext().getResources().getConfiguration(); + return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + + interface InternalListCallback { + + boolean onItemSelected(MaterialDialog dialog, View itemView, int position, CharSequence text, + boolean longPress); + } + + static class DefaultVH extends RecyclerView.ViewHolder implements View.OnClickListener, + View.OnLongClickListener { + + final CompoundButton control; + final TextView title; + final DefaultRvAdapter adapter; + + DefaultVH(View itemView, DefaultRvAdapter adapter) { + super(itemView); + control = (CompoundButton) itemView.findViewById(R.id.md_control); + title = (TextView) itemView.findViewById(R.id.md_title); + this.adapter = adapter; + itemView.setOnClickListener(this); + if (adapter.dialog.builder.listLongCallback != null) { + itemView.setOnLongClickListener(this); + } + } + + @Override + public void onClick(View view) { + if (adapter.callback != null && getAdapterPosition() != RecyclerView.NO_POSITION) { + CharSequence text = null; + if (adapter.dialog.builder.items != null && + getAdapterPosition() < adapter.dialog.builder.items.size()) { + text = adapter.dialog.builder.items.get(getAdapterPosition()); + } + adapter.callback.onItemSelected(adapter.dialog, view, getAdapterPosition(), text, false); + } + } + + @Override + public boolean onLongClick(View view) { + if (adapter.callback != null && getAdapterPosition() != RecyclerView.NO_POSITION) { + CharSequence text = null; + if (adapter.dialog.builder.items != null && + getAdapterPosition() < adapter.dialog.builder.items.size()) { + text = adapter.dialog.builder.items.get(getAdapterPosition()); + } + return adapter.callback + .onItemSelected(adapter.dialog, view, getAdapterPosition(), text, true); + } + return false; + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DialogAction.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogAction.java new file mode 100644 index 0000000..d6ae2dd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogAction.java @@ -0,0 +1,10 @@ +package com.hzecool.widget.materialdialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public enum DialogAction { + POSITIVE, + NEUTRAL, + NEGATIVE +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DialogBase.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogBase.java new file mode 100644 index 0000000..e4cfef8 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogBase.java @@ -0,0 +1,71 @@ +package com.hzecool.widget.materialdialog; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; + +import com.hzecool.widget.materialdialog.internal.MDRootLayout; + + +/** + * @author Aidan Follestad (afollestad) + */ +class DialogBase extends Dialog implements DialogInterface.OnShowListener { + + protected MDRootLayout view; + private OnShowListener showListener; + + DialogBase(Context context, int theme) { + super(context, theme); + } + + @Override + public View findViewById(int id) { + return view.findViewById(id); + } + + @Override + public final void setOnShowListener(OnShowListener listener) { + showListener = listener; + } + + final void setOnShowListenerInternal() { + super.setOnShowListener(this); + } + + final void setViewInternal(View view) { + super.setContentView(view); + } + + @Override + public void onShow(DialogInterface dialog) { + if (showListener != null) { + showListener.onShow(dialog); + } + } + + @Override + @Deprecated + public void setContentView(int layoutResID) throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } + + @Override + @Deprecated + public void setContentView(@NonNull View view) throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } + + @Override + @Deprecated + public void setContentView(@NonNull View view, ViewGroup.LayoutParams params) + throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DialogInit.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogInit.java new file mode 100644 index 0000000..8bb6d25 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogInit.java @@ -0,0 +1,554 @@ +package com.hzecool.widget.materialdialog; + +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.StyleRes; +import android.support.annotation.UiThread; +import android.support.v7.widget.RecyclerView; +import android.text.InputType; +import android.text.method.LinkMovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.internal.MDAdapter; +import com.hzecool.widget.materialdialog.internal.MDButton; +import com.hzecool.widget.materialdialog.internal.MDRootLayout; +import com.hzecool.widget.materialdialog.internal.MDTintHelper; +import com.hzecool.widget.materialdialog.util.DialogUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +import me.zhanghai.android.materialprogressbar.HorizontalProgressDrawable; +import me.zhanghai.android.materialprogressbar.IndeterminateHorizontalProgressDrawable; +import me.zhanghai.android.materialprogressbar.IndeterminateProgressDrawable; + + +/** + * Used by MaterialDialog while initializing the dialog. Offloads some of the code to make the main + * class cleaner and easier to read/maintain. + * + * @author Aidan Follestad (afollestad) + */ +class DialogInit { + + @StyleRes + static int getTheme(@NonNull MaterialDialog.Builder builder) { + boolean darkTheme = DialogUtils + .resolveBoolean(builder.context, R.attr.md_dark_theme, builder.theme == Theme.DARK); + builder.theme = darkTheme ? Theme.DARK : Theme.LIGHT; + return darkTheme ? R.style.MD_Dark : R.style.MD_Light; + } + + @LayoutRes + static int getInflateLayout(MaterialDialog.Builder builder) { + if (builder.customView != null) { + return R.layout.md_dialog_custom; + } else if (builder.items != null || builder.adapter != null) { + if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_list_check; + } + return R.layout.md_dialog_list; + } else if (builder.progress > -2) { + return R.layout.md_dialog_progress; + } else if (builder.indeterminateProgress) { + if (builder.indeterminateIsHorizontalProgress) { + return R.layout.md_dialog_progress_indeterminate_horizontal; + } + return R.layout.md_dialog_progress_indeterminate; + } else if (builder.inputCallback != null) { + if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_input_check; + } + return R.layout.md_dialog_input; + } else if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_basic_check; + } else { + return R.layout.md_dialog_basic; + } + } + + @SuppressWarnings("ConstantConditions") + @UiThread + public static void init(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + + // Set cancelable flag and dialog background color + dialog.setCancelable(builder.cancelable); + dialog.setCanceledOnTouchOutside(builder.canceledOnTouchOutside); + if (builder.backgroundColor == 0) { + builder.backgroundColor = DialogUtils + .resolveColor(builder.context, R.attr.md_background_color, + DialogUtils.resolveColor(dialog.getContext(), R.attr.colorBackgroundFloating)); + } + if (builder.backgroundColor != 0) { + GradientDrawable drawable = new GradientDrawable(); + drawable.setCornerRadius( + builder.context.getResources().getDimension(R.dimen.md_bg_corner_radius)); + drawable.setColor(builder.backgroundColor); + dialog.getWindow().setBackgroundDrawable(drawable); + } + + // Retrieve color theme attributes + if (!builder.positiveColorSet) { + builder.positiveColor = DialogUtils + .resolveActionTextColorStateList(builder.context, R.attr.md_positive_color, + builder.positiveColor); + } + if (!builder.neutralColorSet) { + builder.neutralColor = DialogUtils + .resolveActionTextColorStateList(builder.context, R.attr.md_neutral_color, + builder.neutralColor); + } + if (!builder.negativeColorSet) { + builder.negativeColor = DialogUtils + .resolveActionTextColorStateList(builder.context, R.attr.md_negative_color, + builder.negativeColor); + } + if (!builder.widgetColorSet) { + builder.widgetColor = DialogUtils + .resolveColor(builder.context, R.attr.md_widget_color, builder.widgetColor); + } + + // Retrieve default title/content colors + if (!builder.titleColorSet) { + final int titleColorFallback = DialogUtils + .resolveColor(dialog.getContext(), android.R.attr.textColorPrimary); + builder.titleColor = DialogUtils + .resolveColor(builder.context, R.attr.md_title_color, titleColorFallback); + } + if (!builder.contentColorSet) { + final int contentColorFallback = DialogUtils + .resolveColor(dialog.getContext(), android.R.attr.textColorSecondary); + builder.contentColor = DialogUtils + .resolveColor(builder.context, R.attr.md_content_color, contentColorFallback); + } + if (!builder.itemColorSet) { + builder.itemColor = DialogUtils + .resolveColor(builder.context, R.attr.md_item_color, builder.contentColor); + } + + // Retrieve references to views + dialog.title = (TextView) dialog.view.findViewById(R.id.md_title); + dialog.icon = (ImageView) dialog.view.findViewById(R.id.md_icon); + dialog.titleFrame = dialog.view.findViewById(R.id.md_titleFrame); + dialog.content = (TextView) dialog.view.findViewById(R.id.md_content); + dialog.recyclerView = (RecyclerView) dialog.view.findViewById(R.id.md_contentRecyclerView); + dialog.checkBoxPrompt = (CheckBox) dialog.view.findViewById(R.id.md_promptCheckbox); + + // Button views initially used by checkIfStackingNeeded() + dialog.positiveButton = (MDButton) dialog.view.findViewById(R.id.md_buttonDefaultPositive); + dialog.neutralButton = (MDButton) dialog.view.findViewById(R.id.md_buttonDefaultNeutral); + dialog.negativeButton = (MDButton) dialog.view.findViewById(R.id.md_buttonDefaultNegative); + + // Don't allow the submit button to not be shown for input dialogs + if (builder.inputCallback != null && builder.positiveText == null) { + builder.positiveText = builder.context.getText(android.R.string.ok); + } + + // Set up the initial visibility of action buttons based on whether or not text was set + dialog.positiveButton.setVisibility(builder.positiveText != null ? View.VISIBLE : View.GONE); + dialog.neutralButton.setVisibility(builder.neutralText != null ? View.VISIBLE : View.GONE); + dialog.negativeButton.setVisibility(builder.negativeText != null ? View.VISIBLE : View.GONE); + + // Set up the focus of action buttons + dialog.positiveButton.setFocusable(true); + dialog.neutralButton.setFocusable(true); + dialog.negativeButton.setFocusable(true); + if (builder.positiveFocus) { + dialog.positiveButton.requestFocus(); + } + if (builder.neutralFocus) { + dialog.neutralButton.requestFocus(); + } + if (builder.negativeFocus) { + dialog.negativeButton.requestFocus(); + } + + // Setup icon + if (builder.icon != null) { + dialog.icon.setVisibility(View.VISIBLE); + dialog.icon.setImageDrawable(builder.icon); + } else { + Drawable d = DialogUtils.resolveDrawable(builder.context, R.attr.md_icon); + if (d != null) { + dialog.icon.setVisibility(View.VISIBLE); + dialog.icon.setImageDrawable(d); + } else { + dialog.icon.setVisibility(View.GONE); + } + } + + // Setup icon size limiting + int maxIconSize = builder.maxIconSize; + if (maxIconSize == -1) { + maxIconSize = DialogUtils.resolveDimension(builder.context, R.attr.md_icon_max_size); + } + if (builder.limitIconToDefaultSize || DialogUtils + .resolveBoolean(builder.context, R.attr.md_icon_limit_icon_to_default_size)) { + maxIconSize = builder.context.getResources().getDimensionPixelSize(R.dimen.md_icon_max_size); + } + if (maxIconSize > -1) { + dialog.icon.setAdjustViewBounds(true); + dialog.icon.setMaxHeight(maxIconSize); + dialog.icon.setMaxWidth(maxIconSize); + dialog.icon.requestLayout(); + } + + // Setup divider color in case content scrolls + if (!builder.dividerColorSet) { + final int dividerFallback = DialogUtils.resolveColor(dialog.getContext(), R.attr.md_divider); + builder.dividerColor = DialogUtils + .resolveColor(builder.context, R.attr.md_divider_color, dividerFallback); + } + dialog.view.setDividerColor(builder.dividerColor); + + // Setup title and title frame + if (dialog.title != null) { + dialog.setTypeface(dialog.title, builder.mediumFont); + dialog.title.setTextColor(builder.titleColor); + dialog.title.setGravity(builder.titleGravity.getGravityInt()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + //noinspection ResourceType + dialog.title.setTextAlignment(builder.titleGravity.getTextAlignment()); + } + + if (builder.title == null) { + dialog.titleFrame.setVisibility(View.GONE); + } else { + dialog.title.setText(builder.title); + dialog.titleFrame.setVisibility(View.VISIBLE); + } + } + + // Setup content + if (dialog.content != null) { + dialog.content.setMovementMethod(new LinkMovementMethod()); + dialog.setTypeface(dialog.content, builder.regularFont); + dialog.content.setLineSpacing(0f, builder.contentLineSpacingMultiplier); + if (builder.linkColor == null) { + dialog.content.setLinkTextColor( + DialogUtils.resolveColor(dialog.getContext(), android.R.attr.textColorPrimary)); + } else { + dialog.content.setLinkTextColor(builder.linkColor); + } + dialog.content.setTextColor(builder.contentColor); + dialog.content.setGravity(builder.contentGravity.getGravityInt()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + //noinspection ResourceType + dialog.content.setTextAlignment(builder.contentGravity.getTextAlignment()); + } + + if (builder.content != null) { + dialog.content.setText(builder.content); + dialog.content.setVisibility(View.VISIBLE); + } else { + dialog.content.setVisibility(View.GONE); + } + } + + // Setup prompt checkbox + if (dialog.checkBoxPrompt != null) { + dialog.checkBoxPrompt.setText(builder.checkBoxPrompt); + dialog.checkBoxPrompt.setChecked(builder.checkBoxPromptInitiallyChecked); + dialog.checkBoxPrompt.setOnCheckedChangeListener(builder.checkBoxPromptListener); + dialog.setTypeface(dialog.checkBoxPrompt, builder.regularFont); + dialog.checkBoxPrompt.setTextColor(builder.contentColor); + MDTintHelper.setTint(dialog.checkBoxPrompt, builder.widgetColor); + } + + // Setup action buttons + dialog.view.setButtonGravity(builder.buttonsGravity); + dialog.view.setButtonStackedGravity(builder.btnStackedGravity); + dialog.view.setStackingBehavior(builder.stackingBehavior); + boolean textAllCaps; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + textAllCaps = DialogUtils.resolveBoolean(builder.context, android.R.attr.textAllCaps, true); + if (textAllCaps) { + textAllCaps = DialogUtils.resolveBoolean(builder.context, R.attr.textAllCaps, true); + } + } else { + textAllCaps = DialogUtils.resolveBoolean(builder.context, R.attr.textAllCaps, true); + } + + MDButton positiveTextView = dialog.positiveButton; + dialog.setTypeface(positiveTextView, builder.mediumFont); + positiveTextView.setAllCapsCompat(textAllCaps); + positiveTextView.setText(builder.positiveText); + positiveTextView.setTextColor(builder.positiveColor); + dialog.positiveButton.setStackedSelector(dialog.getButtonSelector(DialogAction.POSITIVE, true)); + dialog.positiveButton + .setDefaultSelector(dialog.getButtonSelector(DialogAction.POSITIVE, false)); + dialog.positiveButton.setTag(DialogAction.POSITIVE); + dialog.positiveButton.setOnClickListener(dialog); + dialog.positiveButton.setVisibility(View.VISIBLE); + + MDButton negativeTextView = dialog.negativeButton; + dialog.setTypeface(negativeTextView, builder.mediumFont); + negativeTextView.setAllCapsCompat(textAllCaps); + negativeTextView.setText(builder.negativeText); + negativeTextView.setTextColor(builder.negativeColor); + dialog.negativeButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEGATIVE, true)); + dialog.negativeButton + .setDefaultSelector(dialog.getButtonSelector(DialogAction.NEGATIVE, false)); + dialog.negativeButton.setTag(DialogAction.NEGATIVE); + dialog.negativeButton.setOnClickListener(dialog); + dialog.negativeButton.setVisibility(View.VISIBLE); + + MDButton neutralTextView = dialog.neutralButton; + dialog.setTypeface(neutralTextView, builder.mediumFont); + neutralTextView.setAllCapsCompat(textAllCaps); + neutralTextView.setText(builder.neutralText); + neutralTextView.setTextColor(builder.neutralColor); + dialog.neutralButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, true)); + dialog.neutralButton.setDefaultSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, false)); + dialog.neutralButton.setTag(DialogAction.NEUTRAL); + dialog.neutralButton.setOnClickListener(dialog); + dialog.neutralButton.setVisibility(View.VISIBLE); + + // Setup list dialog stuff + if (builder.listCallbackMultiChoice != null) { + dialog.selectedIndicesList = new ArrayList<>(); + } + if (dialog.recyclerView != null) { + if (builder.adapter == null) { + // Determine list type + if (builder.listCallbackSingleChoice != null) { + dialog.listType = MaterialDialog.ListType.SINGLE; + } else if (builder.listCallbackMultiChoice != null) { + dialog.listType = MaterialDialog.ListType.MULTI; + if (builder.selectedIndices != null) { + dialog.selectedIndicesList = new ArrayList<>(Arrays.asList(builder.selectedIndices)); + builder.selectedIndices = null; + } + } else { + dialog.listType = MaterialDialog.ListType.REGULAR; + } + builder.adapter = new DefaultRvAdapter(dialog, + MaterialDialog.ListType.getLayoutForType(dialog.listType)); + } else if (builder.adapter instanceof MDAdapter) { + // Notify simple list adapter of the dialog it belongs to + ((MDAdapter) builder.adapter).setDialog(dialog); + } + } + + // Setup progress dialog stuff if needed + setupProgressDialog(dialog); + + // Setup input dialog stuff if needed + setupInputDialog(dialog); + + // Setup custom views + if (builder.customView != null) { + ((MDRootLayout) dialog.view.findViewById(R.id.md_root)).noTitleNoPadding(); + FrameLayout frame = (FrameLayout) dialog.view.findViewById(R.id.md_customViewFrame); + dialog.customViewFrame = frame; + View innerView = builder.customView; + if (innerView.getParent() != null) { + ((ViewGroup) innerView.getParent()).removeView(innerView); + } + if (builder.wrapCustomViewInScroll) { + /* Apply the frame padding to the content, this allows the ScrollView to draw it's + over scroll glow without clipping */ + final Resources r = dialog.getContext().getResources(); + final int framePadding = r.getDimensionPixelSize(R.dimen.md_dialog_frame_margin); + final ScrollView sv = new ScrollView(dialog.getContext()); + int paddingTop = r.getDimensionPixelSize(R.dimen.md_content_padding_top); + int paddingBottom = r.getDimensionPixelSize(R.dimen.md_content_padding_bottom); + sv.setClipToPadding(false); + if (innerView instanceof EditText) { + // Setting padding to an EditText causes visual errors, set it to the parent instead + sv.setPadding(framePadding, paddingTop, framePadding, paddingBottom); + } else { + // Setting padding to scroll view pushes the scroll bars out, don't do it if not necessary (like above) + sv.setPadding(0, paddingTop, 0, paddingBottom); + innerView.setPadding(framePadding, 0, framePadding, 0); + } + sv.addView(innerView, new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + innerView = sv; + } + frame.addView(innerView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + // Setup user listeners + if (builder.showListener != null) { + dialog.setOnShowListener(builder.showListener); + } + if (builder.cancelListener != null) { + dialog.setOnCancelListener(builder.cancelListener); + } + if (builder.dismissListener != null) { + dialog.setOnDismissListener(builder.dismissListener); + } + if (builder.keyListener != null) { + dialog.setOnKeyListener(builder.keyListener); + } + + // Setup internal show listener + dialog.setOnShowListenerInternal(); + + // Other internal initialization + dialog.invalidateList(); + dialog.setViewInternal(dialog.view); + dialog.checkIfListInitScroll(); + + // Min height and max width calculations + WindowManager wm = dialog.getWindow().getWindowManager(); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + final int windowWidth = size.x; + final int windowHeight = size.y; + + final int windowVerticalPadding = builder.context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_vertical_margin); + final int windowHorizontalPadding = builder.context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_horizontal_margin); + final int maxWidth = builder.context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_max_width); + final int calculatedWidth = windowWidth - (windowHorizontalPadding * 2); + + dialog.view.setMaxHeight(windowHeight - windowVerticalPadding * 2); + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = Math.min(maxWidth, calculatedWidth); + dialog.getWindow().setAttributes(lp); + } + + private static void fixCanvasScalingWhenHardwareAccelerated(ProgressBar pb) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + // Canvas scaling when hardware accelerated results in artifacts on older API levels, so + // we need to use software rendering + if (pb.isHardwareAccelerated() && pb.getLayerType() != View.LAYER_TYPE_SOFTWARE) { + pb.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } + } + + private static void setupProgressDialog(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + if (builder.indeterminateProgress || builder.progress > -2) { + dialog.progressBar = (ProgressBar) dialog.view.findViewById(android.R.id.progress); + if (dialog.progressBar == null) { + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (builder.indeterminateProgress) { + if (builder.indeterminateIsHorizontalProgress) { + IndeterminateHorizontalProgressDrawable d = new IndeterminateHorizontalProgressDrawable( + builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } else { + IndeterminateProgressDrawable d = new IndeterminateProgressDrawable( + builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } + } else { + HorizontalProgressDrawable d = new HorizontalProgressDrawable(builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } + } else { + MDTintHelper.setTint(dialog.progressBar, builder.widgetColor); + } + + if (!builder.indeterminateProgress || builder.indeterminateIsHorizontalProgress) { + dialog.progressBar.setIndeterminate( + builder.indeterminateProgress && builder.indeterminateIsHorizontalProgress); + dialog.progressBar.setProgress(0); + dialog.progressBar.setMax(builder.progressMax); + dialog.progressLabel = (TextView) dialog.view.findViewById(R.id.md_label); + if (dialog.progressLabel != null) { + dialog.progressLabel.setTextColor(builder.contentColor); + dialog.setTypeface(dialog.progressLabel, builder.mediumFont); + dialog.progressLabel.setText(builder.progressPercentFormat.format(0)); + } + dialog.progressMinMax = (TextView) dialog.view.findViewById(R.id.md_minMax); + if (dialog.progressMinMax != null) { + dialog.progressMinMax.setTextColor(builder.contentColor); + dialog.setTypeface(dialog.progressMinMax, builder.regularFont); + + if (builder.showMinMax) { + dialog.progressMinMax.setVisibility(View.VISIBLE); + dialog.progressMinMax.setText(String.format(builder.progressNumberFormat, + 0, builder.progressMax)); + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) dialog.progressBar + .getLayoutParams(); + lp.leftMargin = 0; + lp.rightMargin = 0; + } else { + dialog.progressMinMax.setVisibility(View.GONE); + } + } else { + builder.showMinMax = false; + } + } + } + + if (dialog.progressBar != null) { + fixCanvasScalingWhenHardwareAccelerated(dialog.progressBar); + } + } + + private static void setupInputDialog(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + dialog.input = (EditText) dialog.view.findViewById(android.R.id.input); + if (dialog.input == null) { + return; + } + dialog.setTypeface(dialog.input, builder.regularFont); + if (builder.inputPrefill != null) { + dialog.input.setText(builder.inputPrefill); + } + dialog.setInternalInputCallback(); + dialog.input.setHint(builder.inputHint); + dialog.input.setSingleLine(); + dialog.input.setTextColor(builder.contentColor); + dialog.input.setHintTextColor(DialogUtils.adjustAlpha(builder.contentColor, 0.3f)); + MDTintHelper.setTint(dialog.input, dialog.builder.widgetColor); + + if (builder.inputType != -1) { + dialog.input.setInputType(builder.inputType); + if (builder.inputType != InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD && + (builder.inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD) + == InputType.TYPE_TEXT_VARIATION_PASSWORD) { + // If the flags contain TYPE_TEXT_VARIATION_PASSWORD, apply the password transformation method automatically + dialog.input.setTransformationMethod(PasswordTransformationMethod.getInstance()); + } + } + + dialog.inputMinMax = (TextView) dialog.view.findViewById(R.id.md_minMax); + if (builder.inputMinLength > 0 || builder.inputMaxLength > -1) { + dialog.invalidateInputMinMaxIndicator(dialog.input.getText().toString().length(), + !builder.inputAllowEmpty); + } else { + dialog.inputMinMax.setVisibility(View.GONE); + dialog.inputMinMax = null; + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/GravityEnum.java b/widget/src/main/java/com/hzecool/widget/materialdialog/GravityEnum.java new file mode 100644 index 0000000..f4c1846 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/GravityEnum.java @@ -0,0 +1,40 @@ +package com.hzecool.widget.materialdialog; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.Gravity; +import android.view.View; + +public enum GravityEnum { + START, CENTER, END; + + private static final boolean HAS_RTL = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + + @SuppressLint("RtlHardcoded") + public int getGravityInt() { + switch (this) { + case START: + return HAS_RTL ? Gravity.START : Gravity.LEFT; + case CENTER: + return Gravity.CENTER_HORIZONTAL; + case END: + return HAS_RTL ? Gravity.END : Gravity.RIGHT; + default: + throw new IllegalStateException("Invalid gravity constant"); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getTextAlignment() { + switch (this) { + case CENTER: + return View.TEXT_ALIGNMENT_CENTER; + case END: + return View.TEXT_ALIGNMENT_VIEW_END; + default: + return View.TEXT_ALIGNMENT_VIEW_START; + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/MaterialDialog.java b/widget/src/main/java/com/hzecool/widget/materialdialog/MaterialDialog.java new file mode 100644 index 0000000..f4cb88d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/MaterialDialog.java @@ -0,0 +1,2270 @@ +package com.hzecool.widget.materialdialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.support.annotation.ArrayRes; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.DimenRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntRange; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.UiThread; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.Html; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.internal.MDButton; +import com.hzecool.widget.materialdialog.internal.MDRootLayout; +import com.hzecool.widget.materialdialog.internal.MDTintHelper; +import com.hzecool.widget.materialdialog.internal.ThemeSingleton; +import com.hzecool.widget.materialdialog.util.DialogUtils; +import com.hzecool.widget.materialdialog.util.RippleHelper; +import com.hzecool.widget.materialdialog.util.TypefaceHelper; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * @author Aidan Follestad (afollestad) + */ +public class MaterialDialog extends DialogBase implements + View.OnClickListener, DefaultRvAdapter.InternalListCallback { + + protected final Builder builder; + private final Handler handler; + protected ImageView icon; + protected TextView title; + protected TextView content; + + EditText input; + RecyclerView recyclerView; + View titleFrame; + FrameLayout customViewFrame; + ProgressBar progressBar; + TextView progressLabel; + TextView progressMinMax; + TextView inputMinMax; + CheckBox checkBoxPrompt; + MDButton positiveButton; + MDButton neutralButton; + MDButton negativeButton; + ListType listType; + List selectedIndicesList; + + @SuppressLint("InflateParams") + protected MaterialDialog(Builder builder) { + super(builder.context, DialogInit.getTheme(builder)); + handler = new Handler(); + this.builder = builder; + final LayoutInflater inflater = LayoutInflater.from(builder.context); + view = (MDRootLayout) inflater.inflate(DialogInit.getInflateLayout(builder), null); + DialogInit.init(this); + } + + public final Builder getBuilder() { + return builder; + } + + public final void setTypeface(TextView target, Typeface t) { + if (t == null) { + return; + } + int flags = target.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG; + target.setPaintFlags(flags); + target.setTypeface(t); + } + + @SuppressWarnings("unused") + @Nullable + public Object getTag() { + return builder.tag; + } + + final void checkIfListInitScroll() { + if (recyclerView == null) { + return; + } + recyclerView.getViewTreeObserver() + .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @SuppressWarnings("ConstantConditions") + @Override + public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + //noinspection deprecation + recyclerView.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + + if (listType == ListType.SINGLE || listType == ListType.MULTI) { + int selectedIndex; + if (listType == ListType.SINGLE) { + if (builder.selectedIndex < 0) { + return; + } + selectedIndex = builder.selectedIndex; + } else { + if (selectedIndicesList == null || selectedIndicesList.size() == 0) { + return; + } + Collections.sort(selectedIndicesList); + selectedIndex = selectedIndicesList.get(0); + } + + final int fSelectedIndex = selectedIndex; + recyclerView.post(new Runnable() { + @Override + public void run() { + recyclerView.requestFocus(); + builder.layoutManager.scrollToPosition(fSelectedIndex); + } + }); + } + } + }); + } + + /** + * Sets the dialog RecyclerView's adapter/layout manager, and it's item click listener. + */ + final void invalidateList() { + if (recyclerView == null) { + return; + } else if ((builder.items == null || + builder.items.size() == 0) && builder.adapter == null) { + return; + } + if (builder.layoutManager == null) { + builder.layoutManager = new LinearLayoutManager(getContext()); + } + recyclerView.setLayoutManager(builder.layoutManager); + recyclerView.setAdapter(builder.adapter); + if (listType != null) { + ((DefaultRvAdapter) builder.adapter).setCallback(this); + } + } + + @Override + public boolean onItemSelected(MaterialDialog dialog, View view, + int position, CharSequence text, boolean longPress) { + if (!view.isEnabled()) { + return false; + } + if (listType == null || listType == ListType.REGULAR) { + // Default adapter, non choice mode + if (builder.autoDismiss) { + // If auto dismiss is enabled, dismiss the dialog when a list item is selected + dismiss(); + } + if (!longPress && builder.listCallback != null) { + builder.listCallback.onSelection(this, view, position, + builder.items.get(position)); + } + if (longPress && builder.listLongCallback != null) { + return builder.listLongCallback.onLongSelection(this, view, + position, builder.items.get(position)); + } + } else { + // Default adapter, choice mode + if (listType == ListType.MULTI) { + final CheckBox cb = (CheckBox) view.findViewById(R.id.md_control); + if (!cb.isEnabled()) { + return false; + } + final boolean shouldBeChecked = !selectedIndicesList.contains(position); + if (shouldBeChecked) { + // Add the selection to the states first so the callback includes it (when alwaysCallMultiChoiceCallback) + selectedIndicesList.add(position); + if (builder.alwaysCallMultiChoiceCallback) { + // If the checkbox wasn't previously selected, and the callback returns true, add it to the states and check it + if (sendMultiChoiceCallback()) { + cb.setChecked(true); + } else { + // The callback cancelled selection, remove it from the states + selectedIndicesList.remove(Integer.valueOf(position)); + } + } else { + // The callback was not used to check if selection is allowed, just select it + cb.setChecked(true); + } + } else { + // Remove the selection from the states first so the callback does not include it (when alwaysCallMultiChoiceCallback) + selectedIndicesList.remove(Integer.valueOf(position)); + if (builder.alwaysCallMultiChoiceCallback) { + // If the checkbox was previously selected, and the callback returns true, remove it from the states and uncheck it + if (sendMultiChoiceCallback()) { + cb.setChecked(false); + } else { + // The callback cancelled unselection, re-add it to the states + selectedIndicesList.add(position); + } + } else { + // The callback was not used to check if the unselection is allowed, just uncheck it + cb.setChecked(false); + } + } + } else if (listType == ListType.SINGLE) { + final RadioButton radio = (RadioButton) view.findViewById(R.id.md_control); + if (!radio.isEnabled()) { + return false; + } + boolean allowSelection = true; + final int oldSelected = builder.selectedIndex; + + if (builder.autoDismiss && builder.positiveText == null) { + // If auto dismiss is enabled, and no action button is visible to approve the selection, dismiss the dialog + dismiss(); + // Don't allow the selection to be updated since the dialog is being dismissed anyways + allowSelection = false; + // Update selected index and send callback + builder.selectedIndex = position; + sendSingleChoiceCallback(view); + } else if (builder.alwaysCallSingleChoiceCallback) { + // Temporarily set the new index so the callback uses the right one + builder.selectedIndex = position; + // Only allow the radio button to be checked if the callback returns true + allowSelection = sendSingleChoiceCallback(view); + // Restore the old selected index, so the state is updated below + builder.selectedIndex = oldSelected; + } + // Update the checked states + if (allowSelection) { + builder.selectedIndex = position; + radio.setChecked(true); + builder.adapter.notifyItemChanged(oldSelected); + builder.adapter.notifyItemChanged(position); + } + } + } + return true; + } + + final Drawable getListSelector() { + if (builder.listSelector != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.listSelector, null); + } + final Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_list_selector); + if (d != null) { + return d; + } + return DialogUtils.resolveDrawable(getContext(), R.attr.md_list_selector); + } + + public RecyclerView getRecyclerView() { + return recyclerView; + } + + public boolean isPromptCheckBoxChecked() { + return checkBoxPrompt != null && checkBoxPrompt.isChecked(); + } + + @SuppressWarnings("unused") + public void setPromptCheckBoxChecked(boolean checked) { + if (checkBoxPrompt != null) { + checkBoxPrompt.setChecked(checked); + } + } + + /* package */ Drawable getButtonSelector(DialogAction which, boolean isStacked) { + if (isStacked) { + if (builder.btnSelectorStacked != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorStacked, null); + } + final Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_stacked_selector); + if (d != null) { + return d; + } + return DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_stacked_selector); + } else { + switch (which) { + default: { + if (builder.btnSelectorPositive != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorPositive, null); + } + Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_positive_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), + R.attr.md_btn_positive_selector); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + RippleHelper.applyColor(d, builder.buttonRippleColor); + } + return d; + } + case NEUTRAL: { + if (builder.btnSelectorNeutral != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorNeutral, null); + } + Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_neutral_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_neutral_selector); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + RippleHelper.applyColor(d, builder.buttonRippleColor); + } + return d; + } + case NEGATIVE: { + if (builder.btnSelectorNegative != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorNegative, null); + } + Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_negative_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), + R.attr.md_btn_negative_selector); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + RippleHelper.applyColor(d, builder.buttonRippleColor); + } + return d; + } + } + } + } + + private boolean sendSingleChoiceCallback(View v) { + if (builder.listCallbackSingleChoice == null) { + return false; + } + CharSequence text = null; + if (builder.selectedIndex >= 0 && builder.selectedIndex < builder.items.size()) { + text = builder.items.get(builder.selectedIndex); + } + return builder.listCallbackSingleChoice.onSelection(this, v, builder.selectedIndex, text); + } + + private boolean sendMultiChoiceCallback() { + if (builder.listCallbackMultiChoice == null) { + return false; + } + Collections.sort(selectedIndicesList); // make sure the indices are in order + List selectedTitles = new ArrayList<>(); + for (Integer i : selectedIndicesList) { + if (i < 0 || i > builder.items.size() - 1) { + continue; + } + selectedTitles.add(builder.items.get(i)); + } + return builder.listCallbackMultiChoice.onSelection(this, + selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]), + selectedTitles.toArray(new CharSequence[selectedTitles.size()])); + } + + @Override + public final void onClick(View v) { + DialogAction tag = (DialogAction) v.getTag(); + switch (tag) { + case POSITIVE: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onPositive(this); + } + if (builder.onPositiveCallback != null) { + builder.onPositiveCallback.onClick(this, tag); + } + if (!builder.alwaysCallSingleChoiceCallback) { + sendSingleChoiceCallback(v); + } + if (!builder.alwaysCallMultiChoiceCallback) { + sendMultiChoiceCallback(); + } + if (builder.inputCallback != null && input != null && + !builder.alwaysCallInputCallback) { + builder.inputCallback.onInput(this, input.getText()); + } + if (builder.autoDismiss) { + dismiss(); + } + break; + } + case NEGATIVE: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onNegative(this); + } + if (builder.onNegativeCallback != null) { + builder.onNegativeCallback.onClick(this, tag); + } + if (builder.autoDismiss) { + cancel(); + } + break; + } + case NEUTRAL: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onNeutral(this); + } + if (builder.onNeutralCallback != null) { + builder.onNeutralCallback.onClick(this, tag); + } + if (builder.autoDismiss) { + dismiss(); + } + break; + } + } + if (builder.onAnyCallback != null) { + builder.onAnyCallback.onClick(this, tag); + } + } + + @Override + @UiThread + public void show() { + try { + super.show(); + } catch (WindowManager.BadTokenException e) { + throw new DialogException("Bad window token, you cannot show a dialog " + + "before an Activity is created or after it's hidden."); + } + } + + /** + * Retrieves the view of an action button, allowing you to modify properties such as whether or + * not it's enabled. Use {@link #setActionButton(DialogAction, int)} to change text, since the + * view returned here is not the view that displays text. + * + * @param which The action button of which to get the view for. + * @return The view from the dialog's layout representing this action button. + */ + public final MDButton getActionButton(@NonNull DialogAction which) { + switch (which) { + default: + return positiveButton; + case NEUTRAL: + return neutralButton; + case NEGATIVE: + return negativeButton; + } + } + + /** + * Retrieves the view representing the dialog as a whole. Be careful with this. + */ + public final View getView() { + return view; + } + + @Nullable + public final EditText getInputEditText() { + return input; + } + + /** + * Retrieves the TextView that contains the dialog title. If you want to update the title, use + * #{@link #setTitle(CharSequence)} instead. + */ + @SuppressWarnings("unused") + public final TextView getTitleView() { + return title; + } + + /** + * Retrieves the ImageView that contains the dialog icon. + */ + @SuppressWarnings("unused") + public ImageView getIconView() { + return icon; + } + + /** + * Retrieves the TextView that contains the dialog content. If you want to update the content + * (message), use #{@link #setContent(CharSequence)} instead. + */ + @Nullable + @SuppressWarnings("unused") + public final TextView getContentView() { + return content; + } + + /** + * Retrieves the custom view that was inflated or set to the MaterialDialog during building. + * + * @return The custom view that was passed into the Builder. + */ + @Nullable + public final View getCustomView() { + return builder.customView; + } + + /** + * Updates an action button's title, causing invalidation to check if the action buttons should + * be stacked. Setting an action button's text to null is a shortcut for hiding it, too. + * + * @param which The action button to update. + * @param title The new title of the action button. + */ + @SuppressWarnings("WeakerAccess") + @UiThread + public final void setActionButton(@NonNull final DialogAction which, final CharSequence title) { + switch (which) { + default: + builder.positiveText = title; + positiveButton.setText(title); + positiveButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + case NEUTRAL: + builder.neutralText = title; + neutralButton.setText(title); + neutralButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + case NEGATIVE: + builder.negativeText = title; + negativeButton.setText(title); + negativeButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + } + } + + /** + * Updates an action button's title, causing invalidation to check if the action buttons should + * be stacked. + * + * @param which The action button to update. + * @param titleRes The string resource of the new title of the action button. + */ + public final void setActionButton(DialogAction which, @StringRes int titleRes) { + setActionButton(which, getContext().getText(titleRes)); + } + + /** + * Gets whether or not the positive, neutral, or negative action button is visible. + * + * @return Whether or not 1 or more action buttons is visible. + */ + public final boolean hasActionButtons() { + return numberOfActionButtons() > 0; + } + + /** + * Gets the number of visible action buttons. + * + * @return 0 through 3, depending on how many should be or are visible. + */ + @SuppressWarnings("WeakerAccess") + public final int numberOfActionButtons() { + int number = 0; + if (builder.positiveText != null && positiveButton.getVisibility() == View.VISIBLE) { + number++; + } + if (builder.neutralText != null && neutralButton.getVisibility() == View.VISIBLE) { + number++; + } + if (builder.negativeText != null && negativeButton.getVisibility() == View.VISIBLE) { + number++; + } + return number; + } + + @UiThread + @Override + public final void setTitle(CharSequence newTitle) { + title.setText(newTitle); + } + + @UiThread + @Override + public final void setTitle(@StringRes int newTitleRes) { + setTitle(builder.context.getString(newTitleRes)); + } + + @SuppressWarnings("unused") + @UiThread + public final void setTitle(@StringRes int newTitleRes, @Nullable Object... formatArgs) { + setTitle(builder.context.getString(newTitleRes, formatArgs)); + } + + @UiThread + public void setIcon(@DrawableRes final int resId) { + icon.setImageResource(resId); + icon.setVisibility(resId != 0 ? View.VISIBLE : View.GONE); + } + + @UiThread + public void setIcon(final Drawable d) { + icon.setImageDrawable(d); + icon.setVisibility(d != null ? View.VISIBLE : View.GONE); + } + + @SuppressWarnings("unused") + @UiThread + public void setIconAttribute(@AttrRes int attrId) { + Drawable d = DialogUtils.resolveDrawable(builder.context, attrId); + setIcon(d); + } + + @UiThread + public void setDigitsListener(DigitsKeyListener digitsListener){ + input.setKeyListener(digitsListener); + } + + @UiThread + public final void setContent(CharSequence newContent) { + content.setText(newContent); + content.setVisibility(TextUtils.isEmpty(newContent) ? View.GONE : View.VISIBLE); + } + + @UiThread + public final void setContent(@StringRes int newContentRes) { + setContent(builder.context.getString(newContentRes)); + } + + @SuppressWarnings("unused") + @UiThread + public final void setContent(@StringRes int newContentRes, @Nullable Object... formatArgs) { + setContent(builder.context.getString(newContentRes, formatArgs)); + } + + @Nullable + public final ArrayList getItems() { + return builder.items; + } + + @UiThread + public final void setItems(CharSequence... items) { + if (builder.adapter == null) { + throw new IllegalStateException("This MaterialDialog instance does not " + + "yet have an adapter set to it. You cannot use setItems()."); + } + if (items != null) { + builder.items = new ArrayList<>(items.length); + Collections.addAll(builder.items, items); + } else { + builder.items = null; + } + if (!(builder.adapter instanceof DefaultRvAdapter)) { + throw new IllegalStateException("When using a custom adapter, setItems() " + + "cannot be used. Set items through the adapter instead."); + } + notifyItemsChanged(); + } + + @UiThread + public final void notifyItemInserted( + @IntRange(from = 0, to = Integer.MAX_VALUE) int index) { + builder.adapter.notifyItemInserted(index); + } + + @SuppressWarnings("unused") + @UiThread + public final void notifyItemChanged( + @IntRange(from = 0, to = Integer.MAX_VALUE) int index) { + builder.adapter.notifyItemChanged(index); + } + + @UiThread + public final void notifyItemsChanged() { + builder.adapter.notifyDataSetChanged(); + } + + public final int getCurrentProgress() { + if (progressBar == null) { + return -1; + } + return progressBar.getProgress(); + } + + @SuppressWarnings("unused") + public ProgressBar getProgressBar() { + return progressBar; + } + + public final void incrementProgress(final int by) { + setProgress(getCurrentProgress() + by); + } + + public final void setProgress(final int progress) { + if (builder.progress <= -2) { + Log.w("MaterialDialog", "Calling setProgress(int) on an " + + "indeterminate progress dialog has no effect!"); + return; + } + progressBar.setProgress(progress); + handler.post(new Runnable() { + @Override + public void run() { + if (progressLabel != null) { + progressLabel.setText(builder.progressPercentFormat.format( + (float) getCurrentProgress() / (float) getMaxProgress())); + } + if (progressMinMax != null) { + progressMinMax.setText(String.format(builder.progressNumberFormat, + getCurrentProgress(), getMaxProgress())); + } + } + }); + } + + @SuppressWarnings("unused") + public final boolean isIndeterminateProgress() { + return builder.indeterminateProgress; + } + + public final int getMaxProgress() { + if (progressBar == null) { + return -1; + } + return progressBar.getMax(); + } + + @SuppressWarnings("unused") + public final void setMaxProgress(final int max) { + if (builder.progress <= -2) { + throw new IllegalStateException("Cannot use setMaxProgress() on this dialog."); + } + progressBar.setMax(max); + } + + /** + * Change the format of the small text showing the percentage of progress. The default is + * NumberFormat.getPercentageInstance(). + */ + @SuppressWarnings("unused") + public final void setProgressPercentFormat(NumberFormat format) { + builder.progressPercentFormat = format; + setProgress(getCurrentProgress()); // invalidates display + } + + /** + * Change the format of the small text showing current and maximum units of progress. The + * default is "%1d/%2d". + */ + @SuppressWarnings("unused") + public final void setProgressNumberFormat(String format) { + builder.progressNumberFormat = format; + setProgress(getCurrentProgress()); // invalidates display + } + + public final boolean isCancelled() { + return !isShowing(); + } + + /** + * Convenience method for getting the currently selected index of a single choice list. + * + * @return Currently selected index of a single choice list, or -1 if not showing a single choice + * list + */ + @SuppressWarnings("unused") + public int getSelectedIndex() { + if (builder.listCallbackSingleChoice != null) { + return builder.selectedIndex; + } else { + return -1; + } + } + + /** + * Convenience method for setting the currently selected index of a single choice list. This + * only works if you are not using a custom adapter; if you're using a custom adapter, an + * IllegalStateException is thrown. Note that this does not call the respective single choice + * callback. + * + * @param index The index of the list item to check. + */ + @UiThread + @SuppressWarnings("unused") + public void setSelectedIndex(int index) { + builder.selectedIndex = index; + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + builder.adapter.notifyDataSetChanged(); + } else { + throw new IllegalStateException("You can only use setSelectedIndex() " + + "with the default adapter implementation."); + } + } + + /** + * Convenience method for getting the currently selected indices of a multi choice list + * + * @return Currently selected index of a multi choice list, or null if not showing a multi choice + * list + */ + @Nullable + @SuppressWarnings("unused") + public Integer[] getSelectedIndices() { + if (builder.listCallbackMultiChoice != null) { + return selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]); + } else { + return null; + } + } + + /** + * Convenience method for setting the currently selected indices of a multi choice list. This + * only works if you are not using a custom adapter; if you're using a custom adapter, an + * IllegalStateException is thrown. Note that this does not call the respective multi choice + * callback. + * + * @param indices The indices of the list items to check. + */ + @UiThread + @SuppressWarnings("unused") + public void setSelectedIndices(@NonNull Integer[] indices) { + selectedIndicesList = new ArrayList<>(Arrays.asList(indices)); + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + builder.adapter.notifyDataSetChanged(); + } else { + throw new IllegalStateException("You can only use setSelectedIndices() " + + "with the default adapter implementation."); + } + } + + /** + * Clears all selected checkboxes from multi choice list dialogs. + */ + public void clearSelectedIndices() { + clearSelectedIndices(true); + } + + /** + * Clears all selected checkboxes from multi choice list dialogs. + * + * @param sendCallback Defaults to true. True will notify the multi-choice callback, if any. + */ + @SuppressWarnings("WeakerAccess") + public void clearSelectedIndices(boolean sendCallback) { + if (listType == null || listType != ListType.MULTI) { + throw new IllegalStateException("You can only use clearSelectedIndices() " + + "with multi choice list dialogs."); + } + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + if (selectedIndicesList != null) { + selectedIndicesList.clear(); + } + builder.adapter.notifyDataSetChanged(); + if (sendCallback && builder.listCallbackMultiChoice != null) { + sendMultiChoiceCallback(); + } + } else { + throw new IllegalStateException("You can only use clearSelectedIndices() " + + "with the default adapter implementation."); + } + } + + /** + * Selects all checkboxes in multi choice list dialogs. + */ + @SuppressWarnings("unused") + public void selectAllIndices() { + selectAllIndices(true); + } + + /** + * Selects all checkboxes in multi choice list dialogs. + * + * @param sendCallback Defaults to true. True will notify the multi-choice callback, if any. + */ + @SuppressWarnings("WeakerAccess") + public void selectAllIndices(boolean sendCallback) { + if (listType == null || listType != ListType.MULTI) { + throw new IllegalStateException("You can only use selectAllIndices() with " + + "multi choice list dialogs."); + } + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + if (selectedIndicesList == null) { + selectedIndicesList = new ArrayList<>(); + } + for (int i = 0; i < builder.adapter.getItemCount(); i++) { + if (!selectedIndicesList.contains(i)) { + selectedIndicesList.add(i); + } + } + builder.adapter.notifyDataSetChanged(); + if (sendCallback && builder.listCallbackMultiChoice != null) { + sendMultiChoiceCallback(); + } + } else { + throw new IllegalStateException("You can only use selectAllIndices() with the " + + "default adapter implementation."); + } + } + + @Override + public final void onShow(DialogInterface dialog) { + if (input != null) { + DialogUtils.showKeyboard(this, builder); + if (input.getText().length() > 0) { + input.setSelection(input.getText().length()); + } + } + super.onShow(dialog); + } + + void setInternalInputCallback() { + if (input == null) { + return; + } + input.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final int length = s.toString().length(); + boolean emptyDisabled = false; + if (!builder.inputAllowEmpty) { + emptyDisabled = length == 0; + final View positiveAb = getActionButton(DialogAction.POSITIVE); + positiveAb.setEnabled(!emptyDisabled); + } + invalidateInputMinMaxIndicator(length, emptyDisabled); + if (builder.alwaysCallInputCallback) { + builder.inputCallback.onInput(MaterialDialog.this, s); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + } + + void invalidateInputMinMaxIndicator(int currentLength, boolean emptyDisabled) { + if (inputMinMax != null) { + if (builder.inputMaxLength > 0) { + inputMinMax.setText( + String.format(Locale.getDefault(), "%d/%d", currentLength, builder.inputMaxLength)); + inputMinMax.setVisibility(View.VISIBLE); + } else { + inputMinMax.setVisibility(View.GONE); + } + final boolean isDisabled = (emptyDisabled && currentLength == 0) || + (builder.inputMaxLength > 0 && currentLength > builder.inputMaxLength) || + currentLength < builder.inputMinLength; + final int colorText = isDisabled ? builder.inputRangeErrorColor : builder.contentColor; + final int colorWidget = isDisabled ? builder.inputRangeErrorColor : builder.widgetColor; + if (builder.inputMaxLength > 0) { + inputMinMax.setTextColor(colorText); + } + MDTintHelper.setTint(input, colorWidget); + final View positiveAb = getActionButton(DialogAction.POSITIVE); + positiveAb.setEnabled(!isDisabled); + } + } + + @Override + public void dismiss() { + if (input != null) { + DialogUtils.hideKeyboard(this, builder); + } + super.dismiss(); + } + + enum ListType { + REGULAR, + SINGLE, + MULTI; + + public static int getLayoutForType(ListType type) { + switch (type) { + case REGULAR: + return R.layout.md_listitem; + case SINGLE: + return R.layout.md_listitem_singlechoice; + case MULTI: + return R.layout.md_listitem_multichoice; + default: + throw new IllegalArgumentException("Not a valid list type"); + } + } + } + + /** + * A callback used for regular list dialogs. + */ + public interface ListCallback { + + void onSelection(MaterialDialog dialog, View itemView, int position, CharSequence text); + } + + /** + * A callback used for regular list dialogs. + */ + public interface ListLongCallback { + + boolean onLongSelection(MaterialDialog dialog, View itemView, int position, CharSequence text); + } + + /** + * A callback used for multi choice (check box) list dialogs. + */ + public interface ListCallbackSingleChoice { + + /** + * Return true to allow the radio button to be checked, if the alwaysCallSingleChoice() + * option is used. + * + * @param dialog The dialog of which a list item was selected. + * @param which The index of the item that was selected. + * @param text The text of the item that was selected. + * @return True to allow the radio button to be selected. + */ + boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text); + } + + /** + * A callback used for multi choice (check box) list dialogs. + */ + public interface ListCallbackMultiChoice { + + /** + * Return true to allow the check box to be checked, if the alwaysCallSingleChoice() option + * is used. + * + * @param dialog The dialog of which a list item was selected. + * @param which The indices of the items that were selected. + * @param text The text of the items that were selected. + * @return True to allow the checkbox to be selected. + */ + boolean onSelection(MaterialDialog dialog, Integer[] which, CharSequence[] text); + } + + /** + * An alternate way to define a single callback. + */ + public interface SingleButtonCallback { + + void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which); + } + + public interface InputCallback { + + void onInput(@NonNull MaterialDialog dialog, CharSequence input); + } + + private static class DialogException extends WindowManager.BadTokenException { + + DialogException(@SuppressWarnings("SameParameterValue") String message) { + super(message); + } + } + + /** + * The class used to construct a MaterialDialog. + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public static class Builder { + + protected final Context context; + protected CharSequence title; + protected GravityEnum titleGravity = GravityEnum.START; + protected GravityEnum contentGravity = GravityEnum.START; + protected GravityEnum btnStackedGravity = GravityEnum.END; + protected GravityEnum itemsGravity = GravityEnum.START; + protected GravityEnum buttonsGravity = GravityEnum.START; + protected int buttonRippleColor = 0; + protected int titleColor = -1; + protected int contentColor = -1; + protected CharSequence content; + protected ArrayList items; + protected CharSequence positiveText; + protected CharSequence neutralText; + protected CharSequence negativeText; + protected boolean positiveFocus; + protected boolean neutralFocus; + protected boolean negativeFocus; + protected View customView; + protected int widgetColor; + protected ColorStateList choiceWidgetColor; + protected ColorStateList positiveColor; + protected ColorStateList negativeColor; + protected ColorStateList neutralColor; + protected ColorStateList linkColor; + protected ButtonCallback callback; + protected SingleButtonCallback onPositiveCallback; + protected SingleButtonCallback onNegativeCallback; + protected SingleButtonCallback onNeutralCallback; + protected SingleButtonCallback onAnyCallback; + protected ListCallback listCallback; + protected ListLongCallback listLongCallback; + protected ListCallbackSingleChoice listCallbackSingleChoice; + protected ListCallbackMultiChoice listCallbackMultiChoice; + protected boolean alwaysCallMultiChoiceCallback = false; + protected boolean alwaysCallSingleChoiceCallback = false; + protected Theme theme = Theme.LIGHT; + protected boolean cancelable = true; + protected boolean canceledOnTouchOutside = true; + protected float contentLineSpacingMultiplier = 1.2f; + protected int selectedIndex = -1; + protected Integer[] selectedIndices = null; + protected Integer[] disabledIndices = null; + protected boolean autoDismiss = true; + protected Typeface regularFont; + protected Typeface mediumFont; + protected Drawable icon; + protected boolean limitIconToDefaultSize; + protected int maxIconSize = -1; + protected RecyclerView.Adapter adapter; + protected RecyclerView.LayoutManager layoutManager; + protected OnDismissListener dismissListener; + protected OnCancelListener cancelListener; + protected OnKeyListener keyListener; + protected DigitsKeyListener digitsKeyListener; + protected OnShowListener showListener; + protected StackingBehavior stackingBehavior; + protected boolean wrapCustomViewInScroll; + protected int dividerColor; + protected int backgroundColor; + protected int itemColor; + protected boolean indeterminateProgress; + protected boolean showMinMax; + protected int progress = -2; + protected int progressMax = 0; + protected CharSequence inputPrefill; + protected CharSequence inputHint; + protected InputCallback inputCallback; + protected boolean inputAllowEmpty; + protected int inputType = -1; + protected boolean alwaysCallInputCallback; + protected int inputMinLength = -1; + protected int inputMaxLength = -1; + protected int inputRangeErrorColor = 0; + protected int[] itemIds; + protected CharSequence checkBoxPrompt; + protected boolean checkBoxPromptInitiallyChecked; + protected CheckBox.OnCheckedChangeListener checkBoxPromptListener; + + protected String progressNumberFormat; + protected NumberFormat progressPercentFormat; + protected boolean indeterminateIsHorizontalProgress; + + protected boolean titleColorSet = false; + protected boolean contentColorSet = false; + protected boolean itemColorSet = false; + protected boolean positiveColorSet = false; + protected boolean neutralColorSet = false; + protected boolean negativeColorSet = false; + protected boolean widgetColorSet = false; + protected boolean dividerColorSet = false; + + @DrawableRes + protected int listSelector; + @DrawableRes + protected int btnSelectorStacked; + @DrawableRes + protected int btnSelectorPositive; + @DrawableRes + protected int btnSelectorNeutral; + @DrawableRes + protected int btnSelectorNegative; + + protected Object tag; + + public Builder(@NonNull Context context) { + this.context = context; + final int materialBlue = DialogUtils.getColor(context, R.color.md_material_blue_600); + + // Retrieve default accent colors, which are used on the action buttons and progress bars + this.widgetColor = DialogUtils.resolveColor(context, + R.attr.colorAccent, materialBlue); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.widgetColor = DialogUtils.resolveColor(context, + android.R.attr.colorAccent, this.widgetColor); + } + + this.positiveColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.negativeColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.neutralColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.linkColor = DialogUtils.getActionTextStateList(context, + DialogUtils.resolveColor(context, R.attr.md_link_color, this.widgetColor)); + + int fallback = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + fallback = DialogUtils.resolveColor(context, android.R.attr.colorControlHighlight); + } + this.buttonRippleColor = DialogUtils.resolveColor(context, + R.attr.md_btn_ripple_color, + DialogUtils.resolveColor(context, + R.attr.colorControlHighlight, fallback)); + + this.progressPercentFormat = NumberFormat.getPercentInstance(); + this.progressNumberFormat = "%1d/%2d"; + + // Set the default theme based on the Activity theme's primary color darkness (more white or more black) + final int primaryTextColor = DialogUtils.resolveColor(context, + android.R.attr.textColorPrimary); + this.theme = DialogUtils.isColorDark(primaryTextColor) ? Theme.LIGHT : Theme.DARK; + + // Load theme values from the ThemeSingleton if needed + checkSingleton(); + + // Retrieve gravity settings from global theme attributes if needed + this.titleGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_title_gravity, this.titleGravity); + this.contentGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_content_gravity, this.contentGravity); + this.btnStackedGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_btnstacked_gravity, this.btnStackedGravity); + this.itemsGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_items_gravity, this.itemsGravity); + this.buttonsGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_buttons_gravity, this.buttonsGravity); + + final String mediumFont = DialogUtils.resolveString(context, + R.attr.md_medium_font); + final String regularFont = DialogUtils.resolveString(context, + R.attr.md_regular_font); + typeface(mediumFont, regularFont); + + if (this.mediumFont == null) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.mediumFont = Typeface.create("sans-serif-medium", Typeface.NORMAL); + } else { + this.mediumFont = Typeface.create("sans-serif", Typeface.BOLD); + } + } catch (Exception ignored) { + this.mediumFont = Typeface.DEFAULT_BOLD; + } + } + if (this.regularFont == null) { + try { + this.regularFont = Typeface.create("sans-serif", Typeface.NORMAL); + } catch (Exception ignored) { + this.regularFont = Typeface.SANS_SERIF; + if (this.regularFont == null) { + this.regularFont = Typeface.DEFAULT; + } + } + } + } + + public final Context getContext() { + return context; + } + + public final int getItemColor() { + return itemColor; + } + + public final Typeface getRegularFont() { + return regularFont; + } + + @SuppressWarnings("ConstantConditions") + private void checkSingleton() { + if (ThemeSingleton.get(false) == null) { + return; + } + ThemeSingleton s = ThemeSingleton.get(); + if (s.darkTheme) { + this.theme = Theme.DARK; + } + if (s.titleColor != 0) { + this.titleColor = s.titleColor; + } + if (s.contentColor != 0) { + this.contentColor = s.contentColor; + } + if (s.positiveColor != null) { + this.positiveColor = s.positiveColor; + } + if (s.neutralColor != null) { + this.neutralColor = s.neutralColor; + } + if (s.negativeColor != null) { + this.negativeColor = s.negativeColor; + } + if (s.itemColor != 0) { + this.itemColor = s.itemColor; + } + if (s.icon != null) { + this.icon = s.icon; + } + if (s.backgroundColor != 0) { + this.backgroundColor = s.backgroundColor; + } + if (s.dividerColor != 0) { + this.dividerColor = s.dividerColor; + } + if (s.btnSelectorStacked != 0) { + this.btnSelectorStacked = s.btnSelectorStacked; + } + if (s.listSelector != 0) { + this.listSelector = s.listSelector; + } + if (s.btnSelectorPositive != 0) { + this.btnSelectorPositive = s.btnSelectorPositive; + } + if (s.btnSelectorNeutral != 0) { + this.btnSelectorNeutral = s.btnSelectorNeutral; + } + if (s.btnSelectorNegative != 0) { + this.btnSelectorNegative = s.btnSelectorNegative; + } + if (s.widgetColor != 0) { + this.widgetColor = s.widgetColor; + } + if (s.linkColor != null) { + this.linkColor = s.linkColor; + } + this.titleGravity = s.titleGravity; + this.contentGravity = s.contentGravity; + this.btnStackedGravity = s.btnStackedGravity; + this.itemsGravity = s.itemsGravity; + this.buttonsGravity = s.buttonsGravity; + } + + public Builder title(@StringRes int titleRes) { + title(this.context.getText(titleRes)); + return this; + } + + public Builder title(@NonNull CharSequence title) { + this.title = title; + return this; + } + + public Builder titleGravity(@NonNull GravityEnum gravity) { + this.titleGravity = gravity; + return this; + } + + public Builder buttonRippleColor(@ColorInt int color) { + this.buttonRippleColor = color; + return this; + } + + public Builder buttonRippleColorRes(@ColorRes int colorRes) { + return buttonRippleColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder buttonRippleColorAttr(@AttrRes int colorAttr) { + return buttonRippleColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder titleColor(@ColorInt int color) { + this.titleColor = color; + this.titleColorSet = true; + return this; + } + + public Builder titleColorRes(@ColorRes int colorRes) { + return titleColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder titleColorAttr(@AttrRes int colorAttr) { + return titleColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + /** + * Sets the fonts used in the dialog. It's recommended that you use {@link #typeface(String, + * String)} instead, to avoid duplicate Typeface allocations and high memory usage. + * + * @param medium The font used on titles and action buttons. Null uses device default. + * @param regular The font used everywhere else, like on the content and list items. Null uses + * device default. + * @return The Builder instance so you can chain calls to it. + */ + public Builder typeface(@Nullable Typeface medium, @Nullable Typeface regular) { + this.mediumFont = medium; + this.regularFont = regular; + return this; + } + + /** + * Sets the fonts used in the dialog, by file names. This also uses TypefaceHelper in order + * to avoid any un-needed allocations (it recycles typefaces for you). + * + * @param medium The name of font in assets/fonts used on titles and action buttons (null uses + * device default). E.g. [your-project]/app/main/assets/fonts/[medium] + * @param regular The name of font in assets/fonts used everywhere else, like content and list + * items (null uses device default). E.g. [your-project]/app/main/assets/fonts/[regular] + * @return The Builder instance so you can chain calls to it. + */ + public Builder typeface(@Nullable String medium, @Nullable String regular) { + if (medium != null) { + this.mediumFont = TypefaceHelper.get(this.context, medium); + if (this.mediumFont == null) { + throw new IllegalArgumentException("No font asset found for " + medium); + } + } + if (regular != null) { + this.regularFont = TypefaceHelper.get(this.context, regular); + if (this.regularFont == null) { + throw new IllegalArgumentException("No font asset found for " + regular); + } + } + return this; + } + + public Builder icon(@NonNull Drawable icon) { + this.icon = icon; + return this; + } + + public Builder iconRes(@DrawableRes int icon) { + this.icon = ResourcesCompat.getDrawable(context.getResources(), icon, null); + return this; + } + + public Builder iconAttr(@AttrRes int iconAttr) { + this.icon = DialogUtils.resolveDrawable(context, iconAttr); + return this; + } + + public Builder content(@StringRes int contentRes) { + return content(contentRes, false); + } + + public Builder content(@StringRes int contentRes, boolean html) { + CharSequence text = this.context.getText(contentRes); + if (html) { + text = Html.fromHtml(text.toString().replace("\n", "
")); + } + return content(text); + } + + public Builder content(@NonNull CharSequence content) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set content() " + + "when you're using a custom view."); + } + this.content = content; + return this; + } + + public Builder content(@StringRes int contentRes, Object... formatArgs) { + String str = String.format(this.context.getString(contentRes), formatArgs) + .replace("\n", "
"); + //noinspection deprecation + return content(Html.fromHtml(str)); + } + + public Builder contentColor(@ColorInt int color) { + this.contentColor = color; + this.contentColorSet = true; + return this; + } + + public Builder contentColorRes(@ColorRes int colorRes) { + contentColor(DialogUtils.getColor(this.context, colorRes)); + return this; + } + + public Builder contentColorAttr(@AttrRes int colorAttr) { + contentColor(DialogUtils.resolveColor(this.context, colorAttr)); + return this; + } + + public Builder contentGravity(@NonNull GravityEnum gravity) { + this.contentGravity = gravity; + return this; + } + + public Builder contentLineSpacing(float multiplier) { + this.contentLineSpacingMultiplier = multiplier; + return this; + } + + public Builder items(@NonNull Collection collection) { + if (collection.size() > 0) { + final CharSequence[] array = new CharSequence[collection.size()]; + int i = 0; + for (Object obj : collection) { + array[i] = obj.toString(); + i++; + } + items(array); + } else if (collection.size() == 0) { + items = new ArrayList<>(); + } + return this; + } + + public Builder items(@ArrayRes int itemsRes) { + items(this.context.getResources().getTextArray(itemsRes)); + return this; + } + + public Builder items(@NonNull CharSequence... items) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set items()" + + " when you're using a custom view."); + } + this.items = new ArrayList<>(); + Collections.addAll(this.items, items); + return this; + } + + public Builder itemsCallback(@NonNull ListCallback callback) { + this.listCallback = callback; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = null; + return this; + } + + public Builder itemsLongCallback(@NonNull ListLongCallback callback) { + this.listLongCallback = callback; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = null; + return this; + } + + public Builder itemsColor(@ColorInt int color) { + this.itemColor = color; + this.itemColorSet = true; + return this; + } + + public Builder itemsColorRes(@ColorRes int colorRes) { + return itemsColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder itemsColorAttr(@AttrRes int colorAttr) { + return itemsColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder itemsGravity(@NonNull GravityEnum gravity) { + this.itemsGravity = gravity; + return this; + } + + public Builder itemsIds(@NonNull int[] idsArray) { + this.itemIds = idsArray; + return this; + } + + public Builder itemsIds(@ArrayRes int idsArrayRes) { + return itemsIds(context.getResources().getIntArray(idsArrayRes)); + } + + public Builder buttonsGravity(@NonNull GravityEnum gravity) { + this.buttonsGravity = gravity; + return this; + } + + /** + * Pass anything below 0 (such as -1) for the selected index to leave all options unselected + * initially. Otherwise pass the index of an item that will be selected initially. + * + * @param selectedIndex The checkbox index that will be selected initially. + * @param callback The callback that will be called when the presses the positive button. + * @return The Builder instance so you can chain calls to it. + */ + public Builder itemsCallbackSingleChoice(int selectedIndex, + @NonNull ListCallbackSingleChoice callback) { + this.selectedIndex = selectedIndex; + this.listCallback = null; + this.listCallbackSingleChoice = callback; + this.listCallbackMultiChoice = null; + return this; + } + + /** + * By default, the single choice callback is only called when the user clicks the positive + * button or if there are no buttons. Call this to force it to always call on item clicks + * even if the positive button exists. + * + * @return The Builder instance so you can chain calls to it. + */ + public Builder alwaysCallSingleChoiceCallback() { + this.alwaysCallSingleChoiceCallback = true; + return this; + } + + /** + * Pass null for the selected indices to leave all options unselected initially. Otherwise + * pass an array of indices that will be selected initially. + * + * @param selectedIndices The radio button indices that will be selected initially. + * @param callback The callback that will be called when the presses the positive button. + * @return The Builder instance so you can chain calls to it. + */ + public Builder itemsCallbackMultiChoice(@Nullable Integer[] selectedIndices, + @NonNull ListCallbackMultiChoice callback) { + this.selectedIndices = selectedIndices; + this.listCallback = null; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = callback; + return this; + } + + /** + * Sets indices of items that are not clickable. If they are checkboxes or radio buttons, + * they will not be toggleable. + * + * @param disabledIndices The item indices that will be disabled from selection. + * @return The Builder instance so you can chain calls to it. + */ + public Builder itemsDisabledIndices(@Nullable Integer... disabledIndices) { + this.disabledIndices = disabledIndices; + return this; + } + + /** + * By default, the multi choice callback is only called when the user clicks the positive + * button or if there are no buttons. Call this to force it to always call on item clicks + * even if the positive button exists. + * + * @return The Builder instance so you can chain calls to it. + */ + public Builder alwaysCallMultiChoiceCallback() { + this.alwaysCallMultiChoiceCallback = true; + return this; + } + + public Builder positiveText(@StringRes int positiveRes) { + if (positiveRes == 0) { + return this; + } + positiveText(this.context.getText(positiveRes)); + return this; + } + + public Builder positiveText(@NonNull CharSequence message) { + this.positiveText = message; + return this; + } + + public Builder positiveColor(@ColorInt int color) { + return positiveColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder positiveColorRes(@ColorRes int colorRes) { + return positiveColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder positiveColorAttr(@AttrRes int colorAttr) { + return positiveColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder positiveColor(@NonNull ColorStateList colorStateList) { + this.positiveColor = colorStateList; + this.positiveColorSet = true; + return this; + } + + public Builder positiveFocus(boolean isFocusedDefault) { + this.positiveFocus = isFocusedDefault; + return this; + } + + public Builder neutralText(@StringRes int neutralRes) { + if (neutralRes == 0) { + return this; + } + return neutralText(this.context.getText(neutralRes)); + } + + public Builder neutralText(@NonNull CharSequence message) { + this.neutralText = message; + return this; + } + + public Builder negativeColor(@ColorInt int color) { + return negativeColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder negativeColorRes(@ColorRes int colorRes) { + return negativeColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder negativeColorAttr(@AttrRes int colorAttr) { + return negativeColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder negativeColor(@NonNull ColorStateList colorStateList) { + this.negativeColor = colorStateList; + this.negativeColorSet = true; + return this; + } + + public Builder negativeText(@StringRes int negativeRes) { + if (negativeRes == 0) { + return this; + } + return negativeText(this.context.getText(negativeRes)); + } + + public Builder negativeText(@NonNull CharSequence message) { + this.negativeText = message; + return this; + } + + public Builder negativeFocus(boolean isFocusedDefault) { + this.negativeFocus = isFocusedDefault; + return this; + } + + public Builder neutralColor(@ColorInt int color) { + return neutralColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder neutralColorRes(@ColorRes int colorRes) { + return neutralColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder neutralColorAttr(@AttrRes int colorAttr) { + return neutralColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder neutralColor(@NonNull ColorStateList colorStateList) { + this.neutralColor = colorStateList; + this.neutralColorSet = true; + return this; + } + + public Builder neutralFocus(boolean isFocusedDefault) { + this.neutralFocus = isFocusedDefault; + return this; + } + + public Builder linkColor(@ColorInt int color) { + return linkColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder linkColorRes(@ColorRes int colorRes) { + return linkColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder linkColorAttr(@AttrRes int colorAttr) { + return linkColor(DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder linkColor(@NonNull ColorStateList colorStateList) { + this.linkColor = colorStateList; + return this; + } + + public Builder listSelector(@DrawableRes int selectorRes) { + this.listSelector = selectorRes; + return this; + } + + public Builder btnSelectorStacked(@DrawableRes int selectorRes) { + this.btnSelectorStacked = selectorRes; + return this; + } + + public Builder btnSelector(@DrawableRes int selectorRes) { + this.btnSelectorPositive = selectorRes; + this.btnSelectorNeutral = selectorRes; + this.btnSelectorNegative = selectorRes; + return this; + } + + public Builder btnSelector(@DrawableRes int selectorRes, @NonNull DialogAction which) { + switch (which) { + default: + this.btnSelectorPositive = selectorRes; + break; + case NEUTRAL: + this.btnSelectorNeutral = selectorRes; + break; + case NEGATIVE: + this.btnSelectorNegative = selectorRes; + break; + } + return this; + } + + /** + * Sets the gravity used for the text in stacked action buttons. By default, it's #{@link + * GravityEnum#END}. + * + * @param gravity The gravity to use. + * @return The Builder instance so calls can be chained. + */ + public Builder btnStackedGravity(@NonNull GravityEnum gravity) { + this.btnStackedGravity = gravity; + return this; + } + + public Builder checkBoxPrompt(@NonNull CharSequence prompt, boolean initiallyChecked, + @Nullable CheckBox.OnCheckedChangeListener checkListener) { + this.checkBoxPrompt = prompt; + this.checkBoxPromptInitiallyChecked = initiallyChecked; + this.checkBoxPromptListener = checkListener; + return this; + } + + public Builder checkBoxPromptRes(@StringRes int prompt, boolean initiallyChecked, + @Nullable CheckBox.OnCheckedChangeListener checkListener) { + return checkBoxPrompt(context.getResources().getText(prompt), + initiallyChecked, checkListener); + } + + public Builder customView(@LayoutRes int layoutRes, boolean wrapInScrollView) { + LayoutInflater li = LayoutInflater.from(this.context); + return customView(li.inflate(layoutRes, null), wrapInScrollView); + } + + public Builder customView(@NonNull View view, boolean wrapInScrollView) { + if (this.content != null) { + throw new IllegalStateException("You cannot use customView() when you have content set."); + } else if (this.items != null) { + throw new IllegalStateException("You cannot use customView() when you have items set."); + } else if (this.inputCallback != null) { + throw new IllegalStateException("You cannot use customView() with an input dialog"); + } else if (this.progress > -2 || this.indeterminateProgress) { + throw new IllegalStateException("You cannot use customView() with a progress dialog"); + } + if (view.getParent() != null && view.getParent() instanceof ViewGroup) { + ((ViewGroup) view.getParent()).removeView(view); + } + this.customView = view; + this.wrapCustomViewInScroll = wrapInScrollView; + return this; + } + + /** + * Makes this dialog a progress dialog. + * + * @param indeterminate If true, an infinite circular spinner is shown. If false, a horizontal + * progress bar is shown that is incremented or set via the built MaterialDialog instance. + * @param max When indeterminate is false, the max value the horizontal progress bar can get + * to. + * @return An instance of the Builder so calls can be chained. + */ + public Builder progress(boolean indeterminate, int max) { + if (this.customView != null) { + throw new IllegalStateException( + "You cannot set progress() when you're using a custom view."); + } + if (indeterminate) { + this.indeterminateProgress = true; + this.progress = -2; + } else { + this.indeterminateIsHorizontalProgress = false; + this.indeterminateProgress = false; + this.progress = -1; + this.progressMax = max; + } + return this; + } + + /** + * Makes this dialog a progress dialog. + * + * @param indeterminate If true, an infinite circular spinner is shown. If false, a horizontal + * progress bar is shown that is incremented or set via the built MaterialDialog instance. + * @param max When indeterminate is false, the max value the horizontal progress bar can get + * to. + * @param showMinMax For determinate dialogs, the min and max will be displayed to the left + * (start) of the progress bar, e.g. 50/100. + * @return An instance of the Builder so calls can be chained. + */ + public Builder progress(boolean indeterminate, int max, boolean showMinMax) { + this.showMinMax = showMinMax; + return progress(indeterminate, max); + } + + /** + * hange the format of the small text showing current and maximum units of progress. The + * default is "%1d/%2d". + */ + public Builder progressNumberFormat(@NonNull String format) { + this.progressNumberFormat = format; + return this; + } + + /** + * Change the format of the small text showing the percentage of progress. The default is + * NumberFormat.getPercentageInstance(). + */ + public Builder progressPercentFormat(@NonNull NumberFormat format) { + this.progressPercentFormat = format; + return this; + } + + /** + * By default, indeterminate progress dialogs will use a circular indicator. You can change + * it to use a horizontal progress indicator. + */ + public Builder progressIndeterminateStyle(boolean horizontal) { + this.indeterminateIsHorizontalProgress = horizontal; + return this; + } + + public Builder widgetColor(@ColorInt int color) { + this.widgetColor = color; + this.widgetColorSet = true; + return this; + } + + public Builder widgetColorRes(@ColorRes int colorRes) { + return widgetColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder widgetColorAttr(@AttrRes int colorAttr) { + return widgetColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder choiceWidgetColor(@Nullable ColorStateList colorStateList) { + this.choiceWidgetColor = colorStateList; + return this; + } + + public Builder dividerColor(@ColorInt int color) { + this.dividerColor = color; + this.dividerColorSet = true; + return this; + } + + public Builder dividerColorRes(@ColorRes int colorRes) { + return dividerColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder dividerColorAttr(@AttrRes int colorAttr) { + return dividerColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder backgroundColor(@ColorInt int color) { + this.backgroundColor = color; + return this; + } + + public Builder backgroundColorRes(@ColorRes int colorRes) { + return backgroundColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder backgroundColorAttr(@AttrRes int colorAttr) { + return backgroundColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder callback(@NonNull ButtonCallback callback) { + this.callback = callback; + return this; + } + + public Builder onPositive(@NonNull SingleButtonCallback callback) { + this.onPositiveCallback = callback; + return this; + } + + public Builder onNegative(@NonNull SingleButtonCallback callback) { + this.onNegativeCallback = callback; + return this; + } + + public Builder onNeutral(@NonNull SingleButtonCallback callback) { + this.onNeutralCallback = callback; + return this; + } + + public Builder onAny(@NonNull SingleButtonCallback callback) { + this.onAnyCallback = callback; + return this; + } + + public Builder theme(@NonNull Theme theme) { + this.theme = theme; + return this; + } + + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + this.canceledOnTouchOutside = cancelable; + return this; + } + + public Builder canceledOnTouchOutside(boolean canceledOnTouchOutside) { + this.canceledOnTouchOutside = canceledOnTouchOutside; + return this; + } + + /** + * This defaults to true. If set to false, the dialog will not automatically be dismissed + * when an action button is pressed, and not automatically dismissed when the user selects a + * list item. + * + * @param dismiss Whether or not to dismiss the dialog automatically. + * @return The Builder instance so you can chain calls to it. + */ + public Builder autoDismiss(boolean dismiss) { + this.autoDismiss = dismiss; + return this; + } + + /** + * Sets a custom {@link RecyclerView.Adapter} for the dialog's + * list + * + * @param adapter The adapter to set to the list. + * @param layoutManager The layout manager to use in the RecyclerView. Pass null to use the + * default linear manager. + * @return This Builder object to allow for chaining of calls to set methods + */ + @SuppressWarnings("ConstantConditions") + public Builder adapter( + @NonNull RecyclerView.Adapter adapter, + @Nullable RecyclerView.LayoutManager layoutManager) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set adapter() when " + + "you're using a custom view."); + } + if (layoutManager != null && !(layoutManager instanceof LinearLayoutManager) && + !(layoutManager instanceof GridLayoutManager)) { + throw new IllegalStateException("You can currently only use LinearLayoutManager" + + " and GridLayoutManager with this library."); + } + this.adapter = adapter; + this.layoutManager = layoutManager; + return this; + } + + /** + * Limits the display size of a set icon to 48dp. + */ + public Builder limitIconToDefaultSize() { + this.limitIconToDefaultSize = true; + return this; + } + + public Builder maxIconSize(int maxIconSize) { + this.maxIconSize = maxIconSize; + return this; + } + + public Builder maxIconSizeRes(@DimenRes int maxIconSizeRes) { + return maxIconSize((int) this.context.getResources().getDimension(maxIconSizeRes)); + } + + public Builder showListener(@NonNull OnShowListener listener) { + this.showListener = listener; + return this; + } + + public Builder dismissListener(@NonNull OnDismissListener listener) { + this.dismissListener = listener; + return this; + } + + public Builder cancelListener(@NonNull OnCancelListener listener) { + this.cancelListener = listener; + return this; + } + + public Builder keyListener(@NonNull OnKeyListener listener) { + this.keyListener = listener; + return this; + } + + public Builder DigitsKeyListener(@NonNull DigitsKeyListener digitsKeyListener) { + this.digitsKeyListener = digitsKeyListener; + return this; + } + + /** + * Sets action button stacking behavior. + * + * @param behavior The behavior of the action button stacking logic. + * @return The Builder instance so you can chain calls to it. + */ + public Builder stackingBehavior(@NonNull StackingBehavior behavior) { + this.stackingBehavior = behavior; + return this; + } + + public Builder input(@Nullable CharSequence hint, @Nullable CharSequence prefill, + boolean allowEmptyInput, @NonNull InputCallback callback) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set content() when " + + "you're using a custom view."); + } + this.inputCallback = callback; + this.inputHint = hint; + this.inputPrefill = prefill; + this.inputAllowEmpty = allowEmptyInput; + return this; + } + + public Builder input(@Nullable CharSequence hint, @Nullable CharSequence prefill, + @NonNull InputCallback callback) { + return input(hint, prefill, true, callback); + } + + public Builder input(@StringRes int hint, @StringRes int prefill, + boolean allowEmptyInput, @NonNull InputCallback callback) { + return input(hint == 0 ? null : context.getText(hint), prefill == 0 ? null : + context.getText(prefill), allowEmptyInput, callback); + } + + public Builder input(@StringRes int hint, @StringRes int prefill, + @NonNull InputCallback callback) { + return input(hint, prefill, true, callback); + } + + public Builder inputType(int type) { + this.inputType = type; + return this; + } + + public Builder inputRange(@IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength) { + return inputRange(minLength, maxLength, 0); + } + + /** + * @param errorColor Pass in 0 for the default red error color (as specified in guidelines). + */ + public Builder inputRange(@IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength, + @ColorInt int errorColor) { + if (minLength < 0) { + throw new IllegalArgumentException("Min length for input dialogs " + + "cannot be less than 0."); + } + this.inputMinLength = minLength; + this.inputMaxLength = maxLength; + if (errorColor == 0) { + this.inputRangeErrorColor = DialogUtils.getColor(context, + R.color.md_edittext_error); + } else { + this.inputRangeErrorColor = errorColor; + } + if (this.inputMinLength > 0) { + this.inputAllowEmpty = false; + } + return this; + } + + /** + * Same as #{@link #inputRange(int, int, int)}, but it takes a color resource ID for the + * error color. + */ + public Builder inputRangeRes(@IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength, + @ColorRes int errorColor) { + return inputRange(minLength, maxLength, + DialogUtils.getColor(context, errorColor)); + } + + public Builder alwaysCallInputCallback() { + this.alwaysCallInputCallback = true; + return this; + } + + public Builder tag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + @UiThread + public MaterialDialog build() { + return new MaterialDialog(this); + } + + @UiThread + public MaterialDialog show() { + MaterialDialog dialog = build(); + dialog.show(); + return dialog; + } + } + + /** + * Override these as needed, so no needing to sub empty methods from an interface + * + * @deprecated Use the individual onPositive, onNegative, onNeutral, or onAny Builder methods + * instead. + */ + @SuppressWarnings({"WeakerAccess", "UnusedParameters"}) + @Deprecated + public static abstract class ButtonCallback { + + public ButtonCallback() { + super(); + } + + @Deprecated + public void onAny(MaterialDialog dialog) { + } + + @Deprecated + public void onPositive(MaterialDialog dialog) { + } + + @Deprecated + public void onNegative(MaterialDialog dialog) { + } + + // The overidden methods below prevent Android Studio from suggesting that they are overidden by developers + + @Deprecated + public void onNeutral(MaterialDialog dialog) { + } + + @Override + protected final Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + @Override + public final boolean equals(Object o) { + return super.equals(o); + } + + @Override + protected final void finalize() throws Throwable { + super.finalize(); + } + + @Override + public final int hashCode() { + return super.hashCode(); + } + + @Override + public final String toString() { + return super.toString(); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/StackingBehavior.java b/widget/src/main/java/com/hzecool/widget/materialdialog/StackingBehavior.java new file mode 100644 index 0000000..112a86a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/StackingBehavior.java @@ -0,0 +1,19 @@ +package com.hzecool.widget.materialdialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public enum StackingBehavior { + /** + * The action buttons are always stacked vertically. + */ + ALWAYS, + /** + * The action buttons are stacked vertically IF it is necessary for them to fit in the dialog. + */ + ADAPTIVE, + /** + * The action buttons are never stacked, even if they should be. + */ + NEVER +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/Theme.java b/widget/src/main/java/com/hzecool/widget/materialdialog/Theme.java new file mode 100644 index 0000000..5f2cf77 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/Theme.java @@ -0,0 +1,8 @@ +package com.hzecool.widget.materialdialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public enum Theme { + LIGHT, DARK +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDAdapter.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDAdapter.java new file mode 100644 index 0000000..7ca27d1 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDAdapter.java @@ -0,0 +1,12 @@ +package com.hzecool.widget.materialdialog.internal; + + +import com.hzecool.widget.materialdialog.MaterialDialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public interface MDAdapter { + + void setDialog(MaterialDialog dialog); +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDButton.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDButton.java new file mode 100644 index 0000000..5ba7d82 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDButton.java @@ -0,0 +1,97 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.v7.text.AllCapsTransformationMethod; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; +import android.view.Gravity; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.GravityEnum; +import com.hzecool.widget.materialdialog.util.DialogUtils; + + +/** + * @author Kevin Barry (teslacoil) 4/02/2015 + */ +public class MDButton extends AppCompatTextView { + + private boolean stacked = false; + private GravityEnum stackedGravity; + + private int stackedEndPadding; + private Drawable stackedBackground; + private Drawable defaultBackground; + + public MDButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public MDButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + stackedEndPadding = context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_frame_margin); + stackedGravity = GravityEnum.END; + } + + /** + * Set if the button should be displayed in stacked mode. + * This should only be called from MDRootLayout's onMeasure, and we must be measured + * after calling this. + */ + /* package */ void setStacked(boolean stacked, boolean force) { + if (this.stacked != stacked || force) { + + setGravity( + stacked ? (Gravity.CENTER_VERTICAL | stackedGravity.getGravityInt()) : Gravity.CENTER); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + //noinspection ResourceType + setTextAlignment(stacked ? stackedGravity.getTextAlignment() : TEXT_ALIGNMENT_CENTER); + } + + DialogUtils.setBackgroundCompat(this, stacked ? stackedBackground : defaultBackground); + if (stacked) { + setPadding(stackedEndPadding, getPaddingTop(), stackedEndPadding, getPaddingBottom()); + } /* Else the padding was properly reset by the drawable */ + + this.stacked = stacked; + } + } + + public void setStackedGravity(GravityEnum gravity) { + stackedGravity = gravity; + } + + public void setStackedSelector(Drawable d) { + stackedBackground = d; + if (stacked) { + setStacked(true, true); + } + } + + public void setDefaultSelector(Drawable d) { + defaultBackground = d; + if (!stacked) { + setStacked(false, true); + } + } + + public void setAllCapsCompat(boolean allCaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + setAllCaps(allCaps); + } else { + if (allCaps) { + setTransformationMethod(new AllCapsTransformationMethod(getContext())); + } else { + setTransformationMethod(null); + } + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDRootLayout.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDRootLayout.java new file mode 100644 index 0000000..2f3a128 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDRootLayout.java @@ -0,0 +1,631 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.webkit.WebView; +import android.widget.AdapterView; +import android.widget.ScrollView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.GravityEnum; +import com.hzecool.widget.materialdialog.StackingBehavior; +import com.hzecool.widget.materialdialog.util.DialogUtils; + +/** + * @author Kevin Barry (teslacoil) 4/02/2015 This is the top level view for all MaterialDialogs It + * handles the layout of: titleFrame (md_stub_titleframe) content (text, custom view, + * listview, etc) buttonDefault... (either stacked or horizontal) + */ +public class MDRootLayout extends ViewGroup { + + private static final int INDEX_NEUTRAL = 0; + private static final int INDEX_NEGATIVE = 1; + private static final int INDEX_POSITIVE = 2; + private final MDButton[] buttons = new MDButton[3]; + private int maxHeight; + private View titleBar; + private View content; + private boolean drawTopDivider = false; + private boolean drawBottomDivider = false; + private StackingBehavior stackBehavior = StackingBehavior.ADAPTIVE; + private boolean isStacked = false; + private boolean useFullPadding = true; + private boolean reducePaddingNoTitleNoButtons; + private boolean noTitleNoPadding; + + private int noTitlePaddingFull; + private int buttonPaddingFull; + private int buttonBarHeight; + + private GravityEnum buttonGravity = GravityEnum.START; + + /* Margin from dialog frame to first button */ + private int buttonHorizontalEdgeMargin; + + private Paint dividerPaint; + + private ViewTreeObserver.OnScrollChangedListener topOnScrollChangedListener; + private ViewTreeObserver.OnScrollChangedListener bottomOnScrollChangedListener; + private int dividerWidth; + + public MDRootLayout(Context context) { + super(context); + init(context, null, 0); + } + + public MDRootLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + private static boolean isVisible(View v) { + boolean visible = v != null && v.getVisibility() != View.GONE; + if (visible && v instanceof MDButton) { + visible = ((MDButton) v).getText().toString().trim().length() > 0; + } + return visible; + } + + public static boolean canRecyclerViewScroll(RecyclerView view) { + return view != null && view.getLayoutManager() != null && view.getLayoutManager() + .canScrollVertically(); + } + + private static boolean canScrollViewScroll(ScrollView sv) { + if (sv.getChildCount() == 0) { + return false; + } + final int childHeight = sv.getChildAt(0).getMeasuredHeight(); + return sv.getMeasuredHeight() - sv.getPaddingTop() - sv.getPaddingBottom() < childHeight; + } + + private static boolean canWebViewScroll(WebView view) { + //noinspection deprecation + return view.getMeasuredHeight() < view.getContentHeight() * view.getScale(); + } + + private static boolean canAdapterViewScroll(AdapterView lv) { + /* Force it to layout it's children */ + if (lv.getLastVisiblePosition() == -1) { + return false; + } + + /* We can scroll if the first or last item is not visible */ + boolean firstItemVisible = lv.getFirstVisiblePosition() == 0; + boolean lastItemVisible = lv.getLastVisiblePosition() == lv.getCount() - 1; + + if (firstItemVisible && lastItemVisible && lv.getChildCount() > 0) { + /* Or the first item's top is above or own top */ + if (lv.getChildAt(0).getTop() < lv.getPaddingTop()) { + return true; + } + /* or the last item's bottom is beyond our own bottom */ + return lv.getChildAt(lv.getChildCount() - 1).getBottom() > + lv.getHeight() - lv.getPaddingBottom(); + } + + return true; + } + + /** + * Find the view touching the bottom of this ViewGroup. Non visible children are ignored, + * however getChildDrawingOrder is not taking into account for simplicity and because it behaves + * inconsistently across platform versions. + * + * @return View touching the bottom of this ViewGroup or null + */ + @Nullable + private static View getBottomView(ViewGroup viewGroup) { + if (viewGroup == null || viewGroup.getChildCount() == 0) { + return null; + } + View bottomView = null; + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + View child = viewGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && child.getBottom() == viewGroup + .getMeasuredHeight()) { + bottomView = child; + break; + } + } + return bottomView; + } + + @Nullable + private static View getTopView(ViewGroup viewGroup) { + if (viewGroup == null || viewGroup.getChildCount() == 0) { + return null; + } + View topView = null; + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + View child = viewGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && child.getTop() == 0) { + topView = child; + break; + } + } + return topView; + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + Resources r = context.getResources(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MDRootLayout, defStyleAttr, 0); + reducePaddingNoTitleNoButtons = a + .getBoolean(R.styleable.MDRootLayout_md_reduce_padding_no_title_no_buttons, true); + a.recycle(); + + noTitlePaddingFull = r.getDimensionPixelSize(R.dimen.md_notitle_vertical_padding); + buttonPaddingFull = r.getDimensionPixelSize(R.dimen.md_button_frame_vertical_padding); + + buttonHorizontalEdgeMargin = r.getDimensionPixelSize(R.dimen.md_button_padding_frame_side); + buttonBarHeight = r.getDimensionPixelSize(R.dimen.md_button_height); + + dividerPaint = new Paint(); + dividerWidth = r.getDimensionPixelSize(R.dimen.md_divider_height); + dividerPaint.setColor(DialogUtils.resolveColor(context, R.attr.md_divider_color)); + setWillNotDraw(false); + } + + public void setMaxHeight(int maxHeight) { + this.maxHeight = maxHeight; + } + + public void noTitleNoPadding() { + noTitleNoPadding = true; + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + if (v.getId() == R.id.md_titleFrame) { + titleBar = v; + } else if (v.getId() == R.id.md_buttonDefaultNeutral) { + buttons[INDEX_NEUTRAL] = (MDButton) v; + } else if (v.getId() == R.id.md_buttonDefaultNegative) { + buttons[INDEX_NEGATIVE] = (MDButton) v; + } else if (v.getId() == R.id.md_buttonDefaultPositive) { + buttons[INDEX_POSITIVE] = (MDButton) v; + } else { + content = v; + } + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + if (height > maxHeight) { + height = maxHeight; + } + + useFullPadding = true; + boolean hasButtons = false; + + final boolean stacked; + if (stackBehavior == StackingBehavior.ALWAYS) { + stacked = true; + } else if (stackBehavior == StackingBehavior.NEVER) { + stacked = false; + } else { + int buttonsWidth = 0; + for (MDButton button : buttons) { + if (button != null && isVisible(button)) { + button.setStacked(false, false); + measureChild(button, widthMeasureSpec, heightMeasureSpec); + buttonsWidth += button.getMeasuredWidth(); + hasButtons = true; + } + } + + int buttonBarPadding = getContext().getResources() + .getDimensionPixelSize(R.dimen.md_neutral_button_margin); + final int buttonFrameWidth = width - 2 * buttonBarPadding; + stacked = buttonsWidth > buttonFrameWidth; + } + + int stackedHeight = 0; + isStacked = stacked; + if (stacked) { + for (MDButton button : buttons) { + if (button != null && isVisible(button)) { + button.setStacked(true, false); + measureChild(button, widthMeasureSpec, heightMeasureSpec); + stackedHeight += button.getMeasuredHeight(); + hasButtons = true; + } + } + } + + int availableHeight = height; + int fullPadding = 0; + int minPadding = 0; + if (hasButtons) { + if (isStacked) { + availableHeight -= stackedHeight; + fullPadding += 2 * buttonPaddingFull; + minPadding += 2 * buttonPaddingFull; + } else { + availableHeight -= buttonBarHeight; + fullPadding += 2 * buttonPaddingFull; + /* No minPadding */ + } + } else { + /* Content has 8dp, we add 16dp and get 24dp, the frame margin */ + fullPadding += 2 * buttonPaddingFull; + } + + if (isVisible(titleBar)) { + titleBar.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED); + availableHeight -= titleBar.getMeasuredHeight(); + } else if (!noTitleNoPadding) { + fullPadding += noTitlePaddingFull; + } + + if (isVisible(content)) { + content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(availableHeight - minPadding, MeasureSpec.AT_MOST)); + + if (content.getMeasuredHeight() <= availableHeight - fullPadding) { + if (!reducePaddingNoTitleNoButtons || isVisible(titleBar) || hasButtons) { + useFullPadding = true; + availableHeight -= content.getMeasuredHeight() + fullPadding; + } else { + useFullPadding = false; + availableHeight -= content.getMeasuredHeight() + minPadding; + } + } else { + useFullPadding = false; + availableHeight = 0; + } + + } + + setMeasuredDimension(width, height - availableHeight); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (content != null) { + if (drawTopDivider) { + int y = content.getTop(); + canvas.drawRect(0, y - dividerWidth, getMeasuredWidth(), y, dividerPaint); + } + + if (drawBottomDivider) { + int y = content.getBottom(); + canvas.drawRect(0, y, getMeasuredWidth(), y + dividerWidth, dividerPaint); + } + } + } + + @Override + protected void onLayout(boolean changed, final int l, int t, final int r, int b) { + if (isVisible(titleBar)) { + int height = titleBar.getMeasuredHeight(); + titleBar.layout(l, t, r, t + height); + t += height; + } else if (!noTitleNoPadding && useFullPadding) { + t += noTitlePaddingFull; + } + + if (isVisible(content)) { + content.layout(l, t, r, t + content.getMeasuredHeight()); + } + + if (isStacked) { + b -= buttonPaddingFull; + for (MDButton mButton : buttons) { + if (isVisible(mButton)) { + mButton.layout(l, b - mButton.getMeasuredHeight(), r, b); + b -= mButton.getMeasuredHeight(); + } + } + } else { + int barTop; + int barBottom = b; + if (useFullPadding) { + barBottom -= buttonPaddingFull; + } + barTop = barBottom - buttonBarHeight; + /* START: + Neutral Negative Positive + + CENTER: + Negative Neutral Positive + + END: + Positive Negative Neutral + + (With no Positive, Negative takes it's place except for CENTER) + */ + int offset = buttonHorizontalEdgeMargin; + + /* Used with CENTER gravity */ + int neutralLeft = -1; + int neutralRight = -1; + + if (isVisible(buttons[INDEX_POSITIVE])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + bl = l + offset; + br = bl + buttons[INDEX_POSITIVE].getMeasuredWidth(); + } else { /* START || CENTER */ + br = r - offset; + bl = br - buttons[INDEX_POSITIVE].getMeasuredWidth(); + neutralRight = bl; + } + buttons[INDEX_POSITIVE].layout(bl, barTop, br, barBottom); + offset += buttons[INDEX_POSITIVE].getMeasuredWidth(); + } + + if (isVisible(buttons[INDEX_NEGATIVE])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + bl = l + offset; + br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth(); + } else if (buttonGravity == GravityEnum.START) { + br = r - offset; + bl = br - buttons[INDEX_NEGATIVE].getMeasuredWidth(); + } else { /* CENTER */ + bl = l + buttonHorizontalEdgeMargin; + br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth(); + neutralLeft = br; + } + buttons[INDEX_NEGATIVE].layout(bl, barTop, br, barBottom); + } + + if (isVisible(buttons[INDEX_NEUTRAL])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + br = r - buttonHorizontalEdgeMargin; + bl = br - buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (buttonGravity == GravityEnum.START) { + bl = l + buttonHorizontalEdgeMargin; + br = bl + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else { /* CENTER */ + if (neutralLeft == -1 && neutralRight != -1) { + neutralLeft = neutralRight - buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (neutralRight == -1 && neutralLeft != -1) { + neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (neutralRight == -1) { + neutralLeft = (r - l) / 2 - buttons[INDEX_NEUTRAL].getMeasuredWidth() / 2; + neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } + bl = neutralLeft; + br = neutralRight; + } + + buttons[INDEX_NEUTRAL].layout(bl, barTop, br, barBottom); + } + } + + setUpDividersVisibility(content, true, true); + } + + public void setStackingBehavior(StackingBehavior behavior) { + stackBehavior = behavior; + invalidate(); + } + + public void setDividerColor(int color) { + dividerPaint.setColor(color); + invalidate(); + } + + public void setButtonGravity(GravityEnum gravity) { + buttonGravity = gravity; + invertGravityIfNecessary(); + } + + private void invertGravityIfNecessary() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return; + } + Configuration config = getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + switch (buttonGravity) { + case START: + buttonGravity = GravityEnum.END; + break; + case END: + buttonGravity = GravityEnum.START; + break; + } + } + } + + public void setButtonStackedGravity(GravityEnum gravity) { + for (MDButton mButton : buttons) { + if (mButton != null) { + mButton.setStackedGravity(gravity); + } + } + } + + private void setUpDividersVisibility(final View view, final boolean setForTop, + final boolean setForBottom) { + if (view == null) { + return; + } + if (view instanceof ScrollView) { + final ScrollView sv = (ScrollView) view; + if (canScrollViewScroll(sv)) { + addScrollListener(sv, setForTop, setForBottom); + } else { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } + } else if (view instanceof AdapterView) { + final AdapterView sv = (AdapterView) view; + if (canAdapterViewScroll(sv)) { + addScrollListener(sv, setForTop, setForBottom); + } else { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } + } else if (view instanceof WebView) { + view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (view.getMeasuredHeight() != 0) { + if (!canWebViewScroll((WebView) view)) { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } else { + addScrollListener((ViewGroup) view, setForTop, setForBottom); + } + view.getViewTreeObserver().removeOnPreDrawListener(this); + } + return true; + } + }); + } else if (view instanceof RecyclerView) { + boolean canScroll = canRecyclerViewScroll((RecyclerView) view); + if (setForTop) { + drawTopDivider = canScroll; + } + if (setForBottom) { + drawBottomDivider = canScroll; + } + if (canScroll) { + addScrollListener((ViewGroup) view, setForTop, setForBottom); + } + } else if (view instanceof ViewGroup) { + View topView = getTopView((ViewGroup) view); + setUpDividersVisibility(topView, setForTop, setForBottom); + View bottomView = getBottomView((ViewGroup) view); + if (bottomView != topView) { + setUpDividersVisibility(bottomView, false, true); + } + } + } + + private void addScrollListener(final ViewGroup vg, final boolean setForTop, + final boolean setForBottom) { + if ((!setForBottom && topOnScrollChangedListener == null + || (setForBottom && bottomOnScrollChangedListener == null))) { + if (vg instanceof RecyclerView) { + RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + boolean hasButtons = false; + for (MDButton button : buttons) { + if (button != null && button.getVisibility() != View.GONE) { + hasButtons = true; + break; + } + } + invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons); + invalidate(); + } + }; + ((RecyclerView) vg).addOnScrollListener(scrollListener); + scrollListener.onScrolled((RecyclerView) vg, 0, 0); + } else { + ViewTreeObserver.OnScrollChangedListener onScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() { + @Override + public void onScrollChanged() { + boolean hasButtons = false; + for (MDButton button : buttons) { + if (button != null && button.getVisibility() != View.GONE) { + hasButtons = true; + break; + } + } + if (vg instanceof WebView) { + invalidateDividersForWebView((WebView) vg, setForTop, setForBottom, hasButtons); + } else { + invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons); + } + invalidate(); + } + }; + if (!setForBottom) { + topOnScrollChangedListener = onScrollChangedListener; + vg.getViewTreeObserver().addOnScrollChangedListener(topOnScrollChangedListener); + } else { + bottomOnScrollChangedListener = onScrollChangedListener; + vg.getViewTreeObserver().addOnScrollChangedListener(bottomOnScrollChangedListener); + } + onScrollChangedListener.onScrollChanged(); + } + } + } + + private void invalidateDividersForScrollingView(ViewGroup view, final boolean setForTop, + boolean setForBottom, boolean hasButtons) { + if (setForTop && view.getChildCount() > 0) { + drawTopDivider = titleBar != null && + titleBar.getVisibility() != View.GONE && + //Not scrolled to the top. + view.getScrollY() + view.getPaddingTop() > view.getChildAt(0).getTop(); + + } + if (setForBottom && view.getChildCount() > 0) { + drawBottomDivider = hasButtons && + view.getScrollY() + view.getHeight() - view.getPaddingBottom() < view + .getChildAt(view.getChildCount() - 1).getBottom(); + } + } + + private void invalidateDividersForWebView(WebView view, final boolean setForTop, + boolean setForBottom, boolean hasButtons) { + if (setForTop) { + drawTopDivider = titleBar != null && + titleBar.getVisibility() != View.GONE && + //Not scrolled to the top. + view.getScrollY() + view.getPaddingTop() > 0; + } + if (setForBottom) { + //noinspection deprecation + drawBottomDivider = hasButtons && + view.getScrollY() + view.getMeasuredHeight() - view.getPaddingBottom() + < view.getContentHeight() * view.getScale(); + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDTintHelper.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDTintHelper.java new file mode 100644 index 0000000..3410332 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDTintHelper.java @@ -0,0 +1,194 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.AppCompatEditText; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.util.DialogUtils; + +import java.lang.reflect.Field; + +/** + * Tints widgets + */ +public class MDTintHelper { + + public static void setTint(@NonNull RadioButton radioButton, + @NonNull ColorStateList colors) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + radioButton.setButtonTintList(colors); + } else { + Drawable radioDrawable = ContextCompat.getDrawable(radioButton.getContext(), + R.drawable.abc_btn_radio_material); + Drawable d = DrawableCompat.wrap(radioDrawable); + DrawableCompat.setTintList(d, colors); + radioButton.setButtonDrawable(d); + } + } + + public static void setTint(@NonNull RadioButton radioButton, + @ColorInt int color) { + final int disabledColor = DialogUtils.getDisabledColor(radioButton.getContext()); + ColorStateList sl = new ColorStateList(new int[][]{ + new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked} + }, new int[]{ + DialogUtils.resolveColor(radioButton.getContext(), R.attr.colorControlNormal), + color, + disabledColor, + disabledColor + }); + setTint(radioButton, sl); + } + + public static void setTint(@NonNull CheckBox box, + @NonNull ColorStateList colors) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + box.setButtonTintList(colors); + } else { + Drawable checkDrawable = ContextCompat.getDrawable(box.getContext(), + R.drawable.abc_btn_check_material); + Drawable drawable = DrawableCompat.wrap(checkDrawable); + DrawableCompat.setTintList(drawable, colors); + box.setButtonDrawable(drawable); + } + } + + public static void setTint(@NonNull CheckBox box, @ColorInt int color) { + final int disabledColor = DialogUtils.getDisabledColor(box.getContext()); + ColorStateList sl = new ColorStateList(new int[][]{ + new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked} + }, new int[]{ + DialogUtils.resolveColor(box.getContext(), R.attr.colorControlNormal), + color, + disabledColor, + disabledColor + }); + setTint(box, sl); + } + + public static void setTint(@NonNull SeekBar seekBar, @ColorInt int color) { + ColorStateList s1 = ColorStateList.valueOf(color); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + seekBar.setThumbTintList(s1); + seekBar.setProgressTintList(s1); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) { + Drawable progressDrawable = DrawableCompat.wrap(seekBar.getProgressDrawable()); + seekBar.setProgressDrawable(progressDrawable); + DrawableCompat.setTintList(progressDrawable, s1); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + Drawable thumbDrawable = DrawableCompat.wrap(seekBar.getThumb()); + DrawableCompat.setTintList(thumbDrawable, s1); + seekBar.setThumb(thumbDrawable); + } + } else { + PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { + mode = PorterDuff.Mode.MULTIPLY; + } + if (seekBar.getIndeterminateDrawable() != null) { + seekBar.getIndeterminateDrawable().setColorFilter(color, mode); + } + if (seekBar.getProgressDrawable() != null) { + seekBar.getProgressDrawable().setColorFilter(color, mode); + } + } + } + + public static void setTint(@NonNull ProgressBar progressBar, + @ColorInt int color) { + setTint(progressBar, color, false); + } + + private static void setTint(@NonNull ProgressBar progressBar, + @ColorInt int color, + boolean skipIndeterminate) { + ColorStateList sl = ColorStateList.valueOf(color); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + progressBar.setProgressTintList(sl); + progressBar.setSecondaryProgressTintList(sl); + if (!skipIndeterminate) { + progressBar.setIndeterminateTintList(sl); + } + } else { + PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { + mode = PorterDuff.Mode.MULTIPLY; + } + if (!skipIndeterminate && progressBar.getIndeterminateDrawable() != null) { + progressBar.getIndeterminateDrawable().setColorFilter(color, mode); + } + if (progressBar.getProgressDrawable() != null) { + progressBar.getProgressDrawable().setColorFilter(color, mode); + } + } + } + + private static ColorStateList createEditTextColorStateList( + @NonNull Context context, @ColorInt int color) { + int[][] states = new int[3][]; + int[] colors = new int[3]; + int i = 0; + states[i] = new int[]{-android.R.attr.state_enabled}; + colors[i] = DialogUtils.resolveColor(context, R.attr.colorControlNormal); + i++; + states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused}; + colors[i] = DialogUtils.resolveColor(context, R.attr.colorControlNormal); + i++; + states[i] = new int[]{}; + colors[i] = color; + return new ColorStateList(states, colors); + } + + public static void setTint(@NonNull EditText editText, @ColorInt int color) { + ColorStateList editTextColorStateList = createEditTextColorStateList(editText.getContext(), + color); + if (editText instanceof AppCompatEditText) { + ((AppCompatEditText) editText).setSupportBackgroundTintList(editTextColorStateList); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + editText.setBackgroundTintList(editTextColorStateList); + } + setCursorTint(editText, color); + } + + private static void setCursorTint(@NonNull EditText editText, @ColorInt int color) { + try { + Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); + fCursorDrawableRes.setAccessible(true); + int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); + Field fEditor = TextView.class.getDeclaredField("mEditor"); + fEditor.setAccessible(true); + Object editor = fEditor.get(editText); + Class clazz = editor.getClass(); + Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); + fCursorDrawable.setAccessible(true); + Drawable[] drawables = new Drawable[2]; + drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); + drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); + fCursorDrawable.set(editor, drawables); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/ThemeSingleton.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/ThemeSingleton.java new file mode 100644 index 0000000..631d695 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/ThemeSingleton.java @@ -0,0 +1,62 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.DrawableRes; + +import com.hzecool.widget.materialdialog.GravityEnum; + + +/** + * Use of this is discouraged for now; for internal use only. See the Global Theming section of the + * README. + */ +public class ThemeSingleton { + + private static ThemeSingleton singleton; + public boolean darkTheme = false; + @ColorInt + public int titleColor = 0; + @ColorInt + public int contentColor = 0; + public ColorStateList positiveColor = null; + public ColorStateList neutralColor = null; + public ColorStateList negativeColor = null; + @ColorInt + public int widgetColor = 0; + @ColorInt + public int itemColor = 0; + public Drawable icon = null; + @ColorInt + public int backgroundColor = 0; + @ColorInt + public int dividerColor = 0; + public ColorStateList linkColor = null; + @DrawableRes + public int listSelector = 0; + @DrawableRes + public int btnSelectorStacked = 0; + @DrawableRes + public int btnSelectorPositive = 0; + @DrawableRes + public int btnSelectorNeutral = 0; + @DrawableRes + public int btnSelectorNegative = 0; + public GravityEnum titleGravity = GravityEnum.START; + public GravityEnum contentGravity = GravityEnum.START; + public GravityEnum btnStackedGravity = GravityEnum.END; + public GravityEnum itemsGravity = GravityEnum.START; + public GravityEnum buttonsGravity = GravityEnum.START; + + public static ThemeSingleton get(boolean createIfNull) { + if (singleton == null && createIfNull) { + singleton = new ThemeSingleton(); + } + return singleton; + } + + public static ThemeSingleton get() { + return get(true); + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/util/DialogUtils.java b/widget/src/main/java/com/hzecool/widget/materialdialog/util/DialogUtils.java new file mode 100644 index 0000000..3627b55 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/util/DialogUtils.java @@ -0,0 +1,305 @@ +package com.hzecool.widget.materialdialog.util; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.IBinder; +import android.support.annotation.ArrayRes; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.util.TypedValue; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import com.hzecool.widget.materialdialog.GravityEnum; +import com.hzecool.widget.materialdialog.MaterialDialog; + + +/** + * @author Aidan Follestad (afollestad) + */ +public class DialogUtils { + +// @SuppressWarnings("ConstantConditions") +// public static float resolveFloat(Context context, int attr) { +// TypedArray a = context.obtainStyledAttributes(null, new int[]{attr}); +// try { +// return a.getFloat(0, 0); +// } finally { +// a.recycle(); +// } +// } + + @ColorInt + public static int getDisabledColor(Context context) { + final int primaryColor = resolveColor(context, android.R.attr.textColorPrimary); + final int disabledColor = isColorDark(primaryColor) ? Color.BLACK : Color.WHITE; + return adjustAlpha(disabledColor, 0.3f); + } + + @ColorInt + public static int adjustAlpha(@ColorInt int color, + @SuppressWarnings("SameParameterValue") float factor) { + int alpha = Math.round(Color.alpha(color) * factor); + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + return Color.argb(alpha, red, green, blue); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr) { + return resolveColor(context, attr, 0); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getColor(0, fallback); + } finally { + a.recycle(); + } + } + + // Try to resolve the colorAttr attribute. + public static ColorStateList resolveActionTextColorStateList( + Context context, @AttrRes int colorAttr, ColorStateList fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{colorAttr}); + try { + final TypedValue value = a.peekValue(0); + if (value == null) { + return fallback; + } + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + final ColorStateList stateList = a.getColorStateList(0); + if (stateList != null) { + return stateList; + } else { + return fallback; + } + } + } finally { + a.recycle(); + } + } + + // Get the specified color resource, creating a ColorStateList if the resource + // points to a color value. + public static ColorStateList getActionTextColorStateList(Context context, @ColorRes int colorId) { + final TypedValue value = new TypedValue(); + context.getResources().getValue(colorId, value, true); + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + //noinspection deprecation + return context.getResources().getColorStateList(colorId); + } else { + return context.getColorStateList(colorId); + } + } + } + + /** + * Returns a color associated with a particular resource ID + * Starting in {@link Build.VERSION_CODES#M}, the returned + * color will be styled for the specified Context's theme. + * + * @param colorId The desired resource identifier, as generated by the aapt tool. This integer + * encodes the package, type, and resource entry. The value 0 is an invalid identifier. + * @return A single color value in the form 0xAARRGGBB. + */ + @ColorInt + public static int getColor(Context context, @ColorRes int colorId) { + return ContextCompat.getColor(context, colorId); + } + + public static String resolveString(Context context, @AttrRes int attr) { + TypedValue v = new TypedValue(); + context.getTheme().resolveAttribute(attr, v, true); + return (String) v.string; + } + + private static int gravityEnumToAttrInt(GravityEnum value) { + switch (value) { + case CENTER: + return 1; + case END: + return 2; + default: + return 0; + } + } + + public static GravityEnum resolveGravityEnum(Context context, + @AttrRes int attr, + GravityEnum defaultGravity) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + switch (a.getInt(0, gravityEnumToAttrInt(defaultGravity))) { + case 1: + return GravityEnum.CENTER; + case 2: + return GravityEnum.END; + default: + return GravityEnum.START; + } + } finally { + a.recycle(); + } + } + + public static Drawable resolveDrawable(Context context, @AttrRes int attr) { + return resolveDrawable(context, attr, null); + } + + private static Drawable resolveDrawable(Context context, + @AttrRes int attr, + @SuppressWarnings( + "SameParameterValue") Drawable fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + Drawable d = a.getDrawable(0); + if (d == null && fallback != null) { + d = fallback; + } + return d; + } finally { + a.recycle(); + } + } + + public static int resolveDimension(Context context, @AttrRes int attr) { + return resolveDimension(context, attr, -1); + } + + private static int resolveDimension(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getDimensionPixelSize(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr, boolean fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getBoolean(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr) { + return resolveBoolean(context, attr, false); + } + + public static boolean isColorDark(@ColorInt int color) { + double darkness = 1 + - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255; + return darkness >= 0.5; + } + + public static void setBackgroundCompat(View view, Drawable d) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + //noinspection deprecation + view.setBackgroundDrawable(d); + } else { + view.setBackground(d); + } + } + + public static void showKeyboard(@NonNull final DialogInterface di, + @NonNull final MaterialDialog.Builder builder) { + final MaterialDialog dialog = (MaterialDialog) di; + if (dialog.getInputEditText() == null) { + return; + } + dialog.getInputEditText().post(new Runnable() { + @Override + public void run() { + dialog.getInputEditText().requestFocus(); + InputMethodManager imm = (InputMethodManager) builder.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(dialog.getInputEditText(), InputMethodManager.SHOW_IMPLICIT); + } + } + }); + } + + public static void hideKeyboard(@NonNull final DialogInterface di, + @NonNull final MaterialDialog.Builder builder) { + final MaterialDialog dialog = (MaterialDialog) di; + if (dialog.getInputEditText() == null) { + return; + } + InputMethodManager imm = (InputMethodManager) builder.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + final View currentFocus = dialog.getCurrentFocus(); + final IBinder windowToken = currentFocus != null ? + currentFocus.getWindowToken() : dialog.getView().getWindowToken(); + if (windowToken != null) { + imm.hideSoftInputFromWindow(windowToken, 0); + } + } + } + + public static ColorStateList getActionTextStateList(Context context, int newPrimaryColor) { + final int fallBackButtonColor = DialogUtils + .resolveColor(context, android.R.attr.textColorPrimary); + if (newPrimaryColor == 0) { + newPrimaryColor = fallBackButtonColor; + } + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_enabled}, // disabled + new int[]{} // enabled + }; + int[] colors = new int[]{ + DialogUtils.adjustAlpha(newPrimaryColor, 0.4f), + newPrimaryColor + }; + return new ColorStateList(states, colors); + } + + public static int[] getColorArray(@NonNull Context context, @ArrayRes int array) { + if (array == 0) { + return null; + } + TypedArray ta = context.getResources().obtainTypedArray(array); + int[] colors = new int[ta.length()]; + for (int i = 0; i < ta.length(); i++) { + colors[i] = ta.getColor(i, 0); + } + ta.recycle(); + return colors; + } + + public static boolean isIn(@NonNull T find, @Nullable T[] ary) { + if (ary == null || ary.length == 0) { + return false; + } + for (T item : ary) { + if (item.equals(find)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/util/RippleHelper.java b/widget/src/main/java/com/hzecool/widget/materialdialog/util/RippleHelper.java new file mode 100644 index 0000000..6fa42db --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/util/RippleHelper.java @@ -0,0 +1,21 @@ +package com.hzecool.widget.materialdialog.util; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import android.support.annotation.ColorInt; + +/** + * @author Aidan Follestad (afollestad) + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class RippleHelper { + + public static void applyColor(Drawable d, @ColorInt int color) { + if (d instanceof RippleDrawable) { + ((RippleDrawable) d).setColor(ColorStateList.valueOf(color)); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/util/TypefaceHelper.java b/widget/src/main/java/com/hzecool/widget/materialdialog/util/TypefaceHelper.java new file mode 100644 index 0000000..c521f48 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/util/TypefaceHelper.java @@ -0,0 +1,47 @@ +package com.hzecool.widget.materialdialog.util; + +import android.content.Context; +import android.graphics.Typeface; +import android.support.v4.util.SimpleArrayMap; + +/* + Each call to Typeface.createFromAsset will load a new instance of the typeface into memory, + and this memory is not consistently get garbage collected + http://code.google.com/p/android/issues/detail?id=9904 + (It states released but even on Lollipop you can see the typefaces accumulate even after + multiple GC passes) + + You can detect this by running: + adb shell dumpsys meminfo com.your.packagenage + + You will see output like: + + Asset Allocations + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Regular.ttf: 123K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + +*/ +public class TypefaceHelper { + + private static final SimpleArrayMap cache = new SimpleArrayMap<>(); + + public static Typeface get(Context c, String name) { + synchronized (cache) { + if (!cache.containsKey(name)) { + try { + Typeface t = Typeface.createFromAsset( + c.getAssets(), String.format("fonts/%s", name)); + cache.put(name, t); + return t; + } catch (RuntimeException e) { + return null; + } + } + return cache.get(name); + } + } +} + diff --git a/widget/src/main/java/com/hzecool/widget/ninegridview/ColorFilterImageView.java b/widget/src/main/java/com/hzecool/widget/ninegridview/ColorFilterImageView.java new file mode 100644 index 0000000..6909c68 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ninegridview/ColorFilterImageView.java @@ -0,0 +1,51 @@ +package com.hzecool.widget.ninegridview; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff.Mode; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.ImageView; + +/** + * @author hnclca + * @ClassName: ColorFilterImageView + * 实现图像根据按下抬起动作变化颜色 + * @date 2016-02-26 + */ +public class ColorFilterImageView extends android.support.v7.widget.AppCompatImageView implements OnTouchListener { + public ColorFilterImageView(Context context) { + this(context, null, 0); + } + + public ColorFilterImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorFilterImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: // 按下时图像变灰 + setColorFilter(Color.GRAY, Mode.MULTIPLY); + break; + case MotionEvent.ACTION_UP: // 手指离开或取消操作时恢复原色 + case MotionEvent.ACTION_CANCEL: + setColorFilter(Color.TRANSPARENT); + break; + default: + break; + } + return false; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/ninegridview/DisplayUtils.java b/widget/src/main/java/com/hzecool/widget/ninegridview/DisplayUtils.java new file mode 100644 index 0000000..0f3a2d0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ninegridview/DisplayUtils.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2016 venshine.cn@gmail.com + * + * 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. + */ +package com.hzecool.widget.ninegridview; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.view.Window; + +/** + * 屏幕显示相关信息 + * + * @author venshine + */ +public class DisplayUtils { + + /** + * 是否横屏 + * + * @param context + * @return + */ + public static boolean isLandscape(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 是否竖屏 + * + * @param context + * @return + */ + public static boolean isPortrait(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } + + /** + * Get screen width, in pixels + * + * @param context + * @return + */ + public static int getScreenWidth(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.widthPixels; + } + + /** + * Get screen height, in pixels + * + * @param context + * @return + */ + public static int getScreenHeight(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.heightPixels; + } + + /** + * Get screen density, the logical density of the display + * + * @param context + * @return + */ + public static float getScreenDensity(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.density; + } + + /** + * Get screen density dpi, the screen density expressed as dots-per-inch + * + * @param context + * @return + */ + public static int getScreenDensityDPI(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.densityDpi; + } + + /** + * Get titlebar height, this method cannot be used in onCreate(),onStart(),onResume(), unless it is called in the + * post(Runnable). + * + * @param activity + * @return + */ + public static int getTitleBarHeight(Activity activity) { + int statusBarHeight = getStatusBarHeight(activity); + int contentViewTop = activity.getWindow().findViewById(Window.ID_ANDROID_CONTENT).getTop(); + int titleBarHeight = contentViewTop - statusBarHeight; + return titleBarHeight < 0 ? 0 : titleBarHeight; + } + + /** + * Get statusbar height, this method cannot be used in onCreate(),onStart(),onResume(), unless it is called in the + * post(Runnable). + * + * @param activity + * @return + */ + public static int getStatusBarHeight(Activity activity) { + Rect rect = new Rect(); + activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); + return rect.top; + } + + /** + * Get statusbar height + * + * @param activity + * @return + */ + public static int getStatusBarHeight2(Activity activity) { + int statusBarHeight = getStatusBarHeight(activity); + if (0 == statusBarHeight) { + Class localClass; + try { + localClass = Class.forName("com.android.internal.R$dimen"); + Object localObject = localClass.newInstance(); + int id = Integer.parseInt(localClass.getField("status_bar_height").get(localObject).toString()); + statusBarHeight = activity.getResources().getDimensionPixelSize(id); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (NumberFormatException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + } + return statusBarHeight; + } + + /** + * Convert dp to px by the density of phone + * + * @param context + * @param dp + * @return + */ + public static int dip2px(Context context, float dp) { + if (context == null) { + return -1; + } + return (int) (dipToPx(context, dp) + 0.5f); + } + + /** + * Convert dp to px + * + * @param context + * @param dp + * @return + */ + private static float dipToPx(Context context, float dp) { + if (context == null) { + return -1; + } + float scale = context.getResources().getDisplayMetrics().density; + return dp * scale; + } + + /** + * Convert px to dp by the density of phone + * + * @param context + * @param px + * @return + */ + public static int px2dip(Context context, float px) { + if (context == null) { + return -1; + } + return (int) (pxToDip(context, px) + 0.5f); + } + + /** + * Convert px to dp + * + * @param context + * @param px + * @return + */ + private static float pxToDip(Context context, float px) { + if (context == null) { + return -1; + } + float scale = context.getResources().getDisplayMetrics().density; + return px / scale; + } + + /** + * Convert px to sp + * + * @param context + * @param px + * @return + */ + public static int px2sp(Context context, float px) { + return (int) (pxToSp(context, px) + 0.5f); + } + + /** + * Convert px to sp + * + * @param context + * @param px + * @return + */ + private static float pxToSp(Context context, float px) { + float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return px / fontScale; + } + + /** + * Convert sp to px + * + * @param context + * @param sp + * @return + */ + public static int sp2px(Context context, float sp) { + return (int) (spToPx(context, sp) + 0.5f); + } + + /** + * Convert sp to px + * + * @param context + * @param sp + * @return + */ + private static float spToPx(Context context, float sp) { + float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return sp * fontScale; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/ninegridview/MultiImageView.java b/widget/src/main/java/com/hzecool/widget/ninegridview/MultiImageView.java new file mode 100644 index 0000000..98e9619 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ninegridview/MultiImageView.java @@ -0,0 +1,214 @@ +package com.hzecool.widget.ninegridview; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.hzecool.widget.utils.GlideSetting; + +import java.util.List; + +/** + * Created by song on 2017/8/7. + */ + +public class MultiImageView extends LinearLayout { + public static int MAX_WIDTH = 0; + + // 照片的Url列表 + private List imagesList; + + /** + * 长度 单位为Pixel + **/ + private int pxOneMaxWandH; // 单张图最大允许宽高 + private int pxMoreWandH = 0;// 多张图的宽高 + private int pxImagePadding = DisplayUtils.dip2px(getContext(), 3);// 图片间的间距 + + private int MAX_PER_ROW_COUNT = 3;// 每行显示最大数 + + private LayoutParams onePicPara; + private LayoutParams morePara, moreParaColumnFirst; + private LayoutParams rowPara; + + private OnItemClickListener mOnItemClickListener; + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + mOnItemClickListener = onItemClickListener; + } + + public MultiImageView(Context context) { + super(context); + } + + public MultiImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setList(List lists) throws IllegalArgumentException { + if (lists == null) { + throw new IllegalArgumentException("imageList is null..."); + } + imagesList = lists; + + if (MAX_WIDTH > 0) { + pxMoreWandH = (MAX_WIDTH - pxImagePadding * 2) / 3; //解决右侧图片和内容对不齐问题 + pxOneMaxWandH = MAX_WIDTH * 2 / 3; + initImageLayoutParams(); + } + + initView(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (MAX_WIDTH == 0) { + int width = measureWidth(widthMeasureSpec); + if (width > 0) { + MAX_WIDTH = width; + if (imagesList != null && imagesList.size() > 0) { + setList(imagesList); + } + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + /** + * Determines the width of this view + * + * @param measureSpec A measureSpec packed into an int + * @return The width of the view, honoring constraints from measureSpec + */ + private int measureWidth(int measureSpec) { + int result = 0; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + // We were told how big to be + result = specSize; + } else { + // Measure the text + // result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + // + getPaddingRight(); + if (specMode == MeasureSpec.AT_MOST) { + // Respect AT_MOST value if that was what is called for by + // measureSpec + result = Math.min(result, specSize); + } + } + return result; + } + + private void initImageLayoutParams() { + int wrap = LayoutParams.WRAP_CONTENT; + int match = LayoutParams.MATCH_PARENT; + + onePicPara = new LayoutParams(wrap, wrap); + + moreParaColumnFirst = new LayoutParams(pxMoreWandH, pxMoreWandH); + morePara = new LayoutParams(pxMoreWandH, pxMoreWandH); + morePara.setMargins(pxImagePadding, 0, 0, 0); + + rowPara = new LayoutParams(match, wrap); + } + + // 根据imageView的数量初始化不同的View布局,还要为每一个View作点击效果 + private void initView() { + this.setOrientation(VERTICAL); + this.removeAllViews(); + if (MAX_WIDTH == 0) { + //为了触发onMeasure()来测量MultiImageView的最大宽度,MultiImageView的宽设置为match_parent + addView(new View(getContext())); + return; + } + + if (imagesList == null || imagesList.size() == 0) { + return; + } + + if (imagesList.size() == 1) { + addView(createImageView(0, false)); + } else { + int allCount = imagesList.size(); + if (allCount == 4) { + MAX_PER_ROW_COUNT = 2; + } else { + MAX_PER_ROW_COUNT = 3; + } + int rowCount = allCount / MAX_PER_ROW_COUNT + (allCount % MAX_PER_ROW_COUNT > 0 ? 1 : 0);// 行数 + for (int rowCursor = 0; rowCursor < rowCount; rowCursor++) { + LinearLayout rowLayout = new LinearLayout(getContext()); + rowLayout.setOrientation(LinearLayout.HORIZONTAL); + + rowLayout.setLayoutParams(rowPara); + if (rowCursor != 0) { + rowLayout.setPadding(0, pxImagePadding, 0, 0); + } + + int columnCount = allCount % MAX_PER_ROW_COUNT == 0 ? MAX_PER_ROW_COUNT : allCount % + MAX_PER_ROW_COUNT;//每行的列数 + if (rowCursor != rowCount - 1) { + columnCount = MAX_PER_ROW_COUNT; + } + addView(rowLayout); + + int rowOffset = rowCursor * MAX_PER_ROW_COUNT;// 行偏移 + for (int columnCursor = 0; columnCursor < columnCount; columnCursor++) { + int position = columnCursor + rowOffset; + rowLayout.addView(createImageView(position, true)); + } + } + } + } + + private ImageView createImageView(int position, final boolean isMultiImage) { + String url = imagesList.get(position); + ImageView imageView = new ColorFilterImageView(getContext()); + if (isMultiImage) { + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + imageView.setLayoutParams(position % MAX_PER_ROW_COUNT == 0 ? moreParaColumnFirst : morePara); + } else { + imageView.setAdjustViewBounds(true); + imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + imageView.setMaxHeight(pxOneMaxWandH); + imageView.setLayoutParams(onePicPara); + } + + imageView.setId(url.hashCode()); + imageView.setOnClickListener(new ImageOnClickListener(position)); + Glide.with(getContext()) + .load(url) + .apply(GlideSetting.getGlideSetting() + .diskCacheStrategy(DiskCacheStrategy.ALL) + ) + + .into(imageView); + return imageView; + } + + private class ImageOnClickListener implements View.OnClickListener { + + private int position; + + public ImageOnClickListener(int position) { + this.position = position; + } + + @Override + public void onClick(View view) { + if (mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(view, position); + } + } + } + + public interface OnItemClickListener { + public void onItemClick(View view, int position); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/popwindowlist/PopAdapter.java b/widget/src/main/java/com/hzecool/widget/popwindowlist/PopAdapter.java new file mode 100644 index 0000000..d09bd92 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/popwindowlist/PopAdapter.java @@ -0,0 +1,27 @@ +package com.hzecool.widget.popwindowlist; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.BaseViewHolder; +import com.hzecool.widget.R; + +import java.util.List; + +/** + * Created by tutu on 2017/4/17. + */ + +public class PopAdapter extends BaseQuickAdapter { + + + public PopAdapter(int resId, List data) { + + super(resId, data); + + + } + + @Override + protected void convert(BaseViewHolder helper, String item) { + helper.setText(R.id.tv, item); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/popwindowlist/RecyclerViewPop.java b/widget/src/main/java/com/hzecool/widget/popwindowlist/RecyclerViewPop.java new file mode 100644 index 0000000..5259f3c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/popwindowlist/RecyclerViewPop.java @@ -0,0 +1,81 @@ +package com.hzecool.widget.popwindowlist; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.hzecool.widget.R; +import com.hzecool.widget.RecycleViewDivider; +import com.hzecool.widget.utils.SizeUtils; + +import java.util.List; + +/** + * Created by tutu on 2017/4/17. + */ + +public class RecyclerViewPop { + + private PopupWindow popupWindow; + private List datas; + private View target; + private View content; + private RecyclerView recyclerView; + private PopAdapter popAdapter; + private Context context; + private int itemViewResId = -1; + + public void setItemView(int resId) { + itemViewResId = resId; + } + + + public RecyclerViewPop(List datas, View target, Context context, BaseQuickAdapter.OnItemClickListener onItemClickListener) { + this.datas = datas; + this.target = target; + this.context = context; + + content = LayoutInflater.from(context).inflate(R.layout.recyclerview_pop, null); + + recyclerView = (RecyclerView) content.findViewById(R.id.rv); + + if (itemViewResId == -1) { + popAdapter = new PopAdapter(R.layout.rv_pop_item, datas); + } else { + popAdapter = new PopAdapter(itemViewResId, datas); + } + + RecycleViewDivider recycleViewDivider = new RecycleViewDivider(context, LinearLayout.HORIZONTAL, SizeUtils.dp2px(context,0.5),context.getResources().getColor(R.color.white)); + recycleViewDivider.setMarginLeft(SizeUtils.dp2px(context,7)); + recycleViewDivider.setMarginRight(SizeUtils.dp2px(context,7)); + recyclerView.addItemDecoration(recycleViewDivider); + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + recyclerView.setAdapter(popAdapter); + + popupWindow = new PopupWindow(content, + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true); + + popupWindow.setBackgroundDrawable(new BitmapDrawable()); + + popAdapter.setOnItemClickListener(onItemClickListener); + } + + + public void show() { + popupWindow.showAsDropDown(target); + } + + public void dissmiss() { + if (popupWindow != null && popupWindow.isShowing()) { + popupWindow.dismiss(); + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/AppUtil.java b/widget/src/main/java/com/hzecool/widget/puzzle/AppUtil.java new file mode 100644 index 0000000..3460512 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/AppUtil.java @@ -0,0 +1,23 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; +import android.util.DisplayMetrics; + +/** + * Created by dd on 16/1/13. + */ +public class AppUtil { + + public static int getScreenWidth(Context context) { + + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.widthPixels; + } + + public static int getScreenHeight(Context context) { + + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.heightPixels; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/Coordinates.java b/widget/src/main/java/com/hzecool/widget/puzzle/Coordinates.java new file mode 100644 index 0000000..68fa118 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/Coordinates.java @@ -0,0 +1,32 @@ +package com.hzecool.widget.puzzle; + +/** + * Created by dd on 16/1/13. + * 坐标实体 + */ +public class Coordinates { + + private float x; + private float y; + + public Coordinates(float x, float y){ + this.x = x; + this.y = y; + } + + public float getX() { + return x; + } + + public void setX(float x) { + this.x = x; + } + + public float getY() { + return y; + } + + public void setY(float y) { + this.y = y; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/DensityUtil.java b/widget/src/main/java/com/hzecool/widget/puzzle/DensityUtil.java new file mode 100644 index 0000000..f7128c0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/DensityUtil.java @@ -0,0 +1,41 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; + +/** + * Created by dd on 16/1/13. + */ +public class DensityUtil { + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + */ + public static int px2dip(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + public static int dip2px(Context context, float dipValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + 0.5f); + } + + /** + * 将px值转换为sp值,保证文字大小不变 + */ + public static int px2sp(Context context, float pxValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 将sp值转换为px值,保证文字大小不变 + */ + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/ImageBean.java b/widget/src/main/java/com/hzecool/widget/puzzle/ImageBean.java new file mode 100644 index 0000000..f2ab1b4 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/ImageBean.java @@ -0,0 +1,186 @@ +package com.hzecool.widget.puzzle; + +import java.io.Serializable; +import java.util.Date; + +/** + * Created by dd on 16/1/11. + */ +public class ImageBean implements Serializable { + + private static final long serialVersionUID = 5700379542385619938L; + + private long pictureId = -1; + + private int id; + public String parentName; + public long size; + public String displayName; + + public String path; + public boolean isChecked = false; + private String uploadTaskId; + private String pictureUrl; + private int photoYear; + private int photoMonth; + private int monthlyNum; + + private Long photoId; + private Date photoTakenDate; + private String memo; + private Boolean monthlyCover; + + private long photoDate; + + public ImageBean() { + super(); + } + + public ImageBean(String parentName, long size, String displayName, String path, boolean isChecked) { + super(); + this.parentName = parentName; + this.size = size; + this.displayName = displayName; + this.path = path; + this.isChecked = isChecked; + } + + public long getPictureId() { + return pictureId; + } + + public void setPictureId(long pictureId) { + this.pictureId = pictureId; + } + + public Date getPhotoTakenDate() { + return photoTakenDate; + } + + public void setPhotoTakenDate(Date photoTakenDate) { + this.photoTakenDate = photoTakenDate; + } + + + public long getPhotoDate() { + return photoDate; + } + + public void setPhotoDate(long photoDate) { + this.photoDate = photoDate; + } + + public String getPictureUrl() { + return pictureUrl; + } + + public void setPictureUrl(String pictureUrl) { + this.pictureUrl = pictureUrl; + } + + public int getPhotoYear() { + return photoYear; + } + + public void setPhotoYear(int photoYear) { + this.photoYear = photoYear; + } + + public int getPhotoMonth() { + return photoMonth; + } + + public void setPhotoMonth(int photoMonth) { + this.photoMonth = photoMonth; + } + + public int getMonthlyNum() { + return monthlyNum; + } + + public void setMonthlyNum(int monthlyNum) { + this.monthlyNum = monthlyNum; + } + + public Long getPhotoId() { + return photoId; + } + + public void setPhotoId(Long photoId) { + this.photoId = photoId; + } + + + public String getMemo() { + return memo; + } + + public void setMemo(String memo) { + this.memo = memo; + } + + public Boolean getMonthlyCover() { + return monthlyCover; + } + + public void setMonthlyCover(Boolean monthlyCover) { + this.monthlyCover = monthlyCover; + } + + + public String getUploadTaskId() { + return uploadTaskId; + } + + public void setUploadTaskId(String uploadTaskId) { + this.uploadTaskId = uploadTaskId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getParentName() { + return parentName; + } + + public void setParentName(String parentName) { + this.parentName = parentName; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isChecked() { + return isChecked; + } + + public void setChecked(boolean isChecked) { + this.isChecked = isChecked; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/ImageItem.java b/widget/src/main/java/com/hzecool/widget/puzzle/ImageItem.java new file mode 100644 index 0000000..f1c2da5 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/ImageItem.java @@ -0,0 +1,20 @@ +package com.hzecool.widget.puzzle; + +import java.util.List; + +/** + * Created by dd on 16/1/13. + * 单张图片实体 + */ +public class ImageItem { + + private List coordinates; + + public List getCoordinates() { + return coordinates; + } + + public void setCoordinates(List coordinates) { + this.coordinates = coordinates; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/PuzzleView.java b/widget/src/main/java/com/hzecool/widget/puzzle/PuzzleView.java new file mode 100644 index 0000000..d9304c0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/PuzzleView.java @@ -0,0 +1,347 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by dd on 16/1/13. + * 拼图重要部分 + */ +public class PuzzleView extends View { + + private Context context; + private Path[] path; + private Bitmap[] bitmaps; + private boolean[] bitmapsFlag; + private float[][] pathLT; + private float[][] pathOffset; + private int pathNum; + private int viewWdh, viewHgt; + private int leftMargin; + private List pics; + private final static int MARGIN_HEIGHT = 100; + private List coordinateSetList; + + + public PuzzleView(Context context) { + super(context); + this.context = context; + } + + public PuzzleView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + this.context = context; + initPath(); + } + + public PuzzleView(Context context, AttributeSet attributeSet, int defStyle) { + super(context, attributeSet, defStyle); + this.context = context; + initPath(); + } + + public void setPathCoordinate(List pathCoordinate) { + this.coordinateSetList = pathCoordinate; + initPath(); + } + + public void setPics(List imageBeans) { + + leftMargin = (AppUtil.getScreenWidth(context) - dp2px(320)) / 2; + viewWdh = dp2px(320); + viewHgt = dp2px(450); + pics = new ArrayList<>(); + if (imageBeans != null) { + pics.addAll(imageBeans); + } + pathNum = pics.size(); + } + + private void initPath() { + path = new Path[pathNum]; + for (int i = 0; i < pathNum; i++) { + path[i] = new Path(); + } + bitmapsFlag = new boolean[pathNum]; + + pathLT = new float[pathNum][2]; + pathOffset = new float[pathNum][2]; + for (int i = 0; i < pathNum; i++) { + bitmapsFlag[i] = false; + pathLT[i][0] = 0f; + pathLT[i][1] = 0f; + pathOffset[i][0] = 0f; + pathOffset[i][1] = 0f; + } + + for (int i = 0; i < pathNum; i++) { + for (int j = 0; j < coordinateSetList.get(i).getCoordinates().size(); j++) { + float x = coordinateSetList.get(i).getCoordinates().get(j).getX(); + float y = coordinateSetList.get(i).getCoordinates().get(j).getY(); + if (j == 0) { + path[i].moveTo(dp2px(x), dp2px(y)); + } else { + path[i].lineTo(dp2px(x), dp2px(y)); + } + } + path[i].close(); + } + + // get bitmap + bitmaps = new Bitmap[pathNum]; + for (int i = 0; i < pathNum; i++) { + BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inJustDecodeBounds = true; + BitmapFactory.decodeFile(pics.get(i).getAbsolutePath(), opt); + + int bmpWdh = opt.outWidth; + int bmpHgt = opt.outHeight; + + Coordinates coordinate = caculateViewSize(coordinateSetList.get(i).getCoordinates()); + int size = caculateSampleSize(bmpWdh, bmpHgt, dp2px(coordinate.getX()), dp2px(coordinate.getY())); + opt.inJustDecodeBounds = false; + opt.inSampleSize = size; + + bitmaps[i] = scaleImage(BitmapFactory.decodeFile(pics.get(i).getAbsolutePath(), opt), dp2px(coordinate.getX()), dp2px(coordinate.getY())); + } + } + + private Coordinates caculateViewSize(List list) { + + float viewWidth; + float viewHeight; + + viewWidth = caculateMaxCoordinateX(list) - caculateMinCoordinateX(list); + viewHeight = caculateMaxCoordinateY(list) - caculateMinCoordinateY(list); + + return new Coordinates(viewWidth, viewHeight); + } + + + private int caculateSampleSize(int picWdh, int picHgt, int showWdh, + int showHgt) { + // 如果此时显示区域比图片大,直接返回 + if ((showWdh < picWdh) && (showHgt < picHgt)) { + int wdhSample = picWdh / showWdh; + int hgtSample = picHgt / showHgt; + // 利用小的来处理 + int sample = wdhSample > hgtSample ? hgtSample : wdhSample; + int minSample = 2; + while (sample > minSample) { + minSample *= 2; + } + return minSample >> 1; + } else { + return 0; + } + } + + private float caculateMinCoordinateX(List list) { + + float minX; + minX = list.get(0).getX(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getX() < minX) { + minX = list.get(i).getX(); + } + } + return minX; + } + + private float caculateMaxCoordinateX(List list) { + + float maxX; + maxX = list.get(0).getX(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getX() > maxX) { + maxX = list.get(i).getX(); + } + } + return maxX; + } + + private float caculateMinCoordinateY(List list) { + + float minY; + minY = list.get(0).getY(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getY() < minY) { + minY = list.get(i).getY(); + } + } + return minY; + } + + private float caculateMaxCoordinateY(List list) { + + float maxY; + maxY = list.get(0).getY(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getY() > maxY) { + maxY = list.get(i).getY(); + } + } + return maxY; + } + + //图片缩放 + private static Bitmap scaleImage(Bitmap bm, int newWidth, int newHeight) { + if (bm == null) { + return null; + } + int width = bm.getWidth(); + int height = bm.getHeight(); + float scaleWidth = ((float) newWidth) / width; + float scaleHeight = ((float) newHeight) / height; + float scale = 1; + if (scaleWidth >= scaleHeight) { + scale = scaleWidth; + } else { + scale = scaleHeight; + } + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, + true); + return newbm; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // TODO Auto-generated method stub + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(viewWdh, viewHgt); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawColor(Color.TRANSPARENT);// 显示背景颜色 + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(Color.WHITE); +// canvas.drawPaint(paint); + // draw1(canvas); + startDraw(canvas, paint); + } + + + private void startDraw(Canvas canvas, Paint paint) { + for (int i = 0; i < pathNum; i++) { + canvas.save(); + drawScene(canvas, paint, i); + canvas.restore(); + } + } + + private void drawScene(Canvas canvas, Paint paint, int idx) { + canvas.clipPath(path[idx]); + canvas.drawColor(Color.GRAY); + if (bitmapsFlag[idx]) { + canvas.drawBitmap(bitmaps[idx], dp2px(caculateMinCoordinateX(coordinateSetList.get(idx).getCoordinates())) + pathOffsetX + pathOffset[idx][0], + dp2px(caculateMinCoordinateY(coordinateSetList.get(idx).getCoordinates())) + pathOffsetY + pathOffset[idx][1], paint); + } else { + canvas.drawBitmap(bitmaps[idx], dp2px(caculateMinCoordinateX(coordinateSetList.get(idx).getCoordinates())) + pathOffset[idx][0], + dp2px(caculateMinCoordinateY(coordinateSetList.get(idx).getCoordinates())) + pathOffset[idx][1], paint); + } + } + + private int dp2px(float point) { + return DensityUtil.dip2px(getContext(), point); + } + + float ptx, pty; + float pathOffsetX, pathOffsetY; + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + for (int i = 0; i < pathNum; i++) { + bitmapsFlag[i] = false; + } + ptx = event.getRawX() - dp2px(leftMargin); + pty = event.getRawY() - dp2px(MARGIN_HEIGHT); + pathOffsetX = 0; + pathOffsetY = 0; + int cflag = 0; + for (cflag = 0; cflag < pathNum; cflag++) { + if (contains(path[cflag], ptx, pty)) { + bitmapsFlag[cflag] = true; + break; + } + } + break; + case MotionEvent.ACTION_MOVE: + pathOffsetX = event.getRawX() - dp2px(leftMargin) - ptx; + pathOffsetY = event.getRawY() - dp2px(MARGIN_HEIGHT) - pty; + invalidate(); + break; + case MotionEvent.ACTION_POINTER_DOWN: + break; + case MotionEvent.ACTION_UP: + for (int i = 0; i < pathNum; i++) { + if (bitmapsFlag[i]) { + pathOffset[i][0] += event.getRawX() - dp2px(leftMargin) - ptx; + pathOffset[i][1] += event.getRawY() - dp2px(MARGIN_HEIGHT) - pty; + + if (pathOffset[i][0] > 0) { + pathOffset[i][0] = 0; + } + if (pathOffset[i][0] < -(bitmaps[i].getWidth() - getViewWidth(coordinateSetList.get(i).getCoordinates()))) { + pathOffset[i][0] = -(bitmaps[i].getWidth() - getViewWidth(coordinateSetList.get(i).getCoordinates())); + } + if (pathOffset[i][1] > 0) { + pathOffset[i][1] = 0; + } + if (pathOffset[i][1] < -(bitmaps[i].getHeight() - getViewHeight(coordinateSetList.get(i).getCoordinates()))) { + pathOffset[i][1] = -(bitmaps[i].getHeight() - getViewHeight(coordinateSetList.get(i).getCoordinates())); + } + bitmapsFlag[i] = false; + break; + } + } + invalidate(); + break; + default: + break; + } + + return true; + } + + private boolean contains(Path parapath, float pointx, float pointy) { + RectF localRectF = new RectF(); + parapath.computeBounds(localRectF, true); + Region localRegion = new Region(); + localRegion.setPath(parapath, new Region((int) localRectF.left, + (int) localRectF.top, (int) localRectF.right, + (int) localRectF.bottom)); + return localRegion.contains((int) pointx, (int) pointy); + } + + private float getViewWidth(List list) { + + return dp2px(caculateMaxCoordinateX(list) - caculateMinCoordinateX(list)); + } + + private float getViewHeight(List list) { + + return dp2px(caculateMaxCoordinateY(list) - caculateMinCoordinateY(list)); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/SelectItem.java b/widget/src/main/java/com/hzecool/widget/puzzle/SelectItem.java new file mode 100644 index 0000000..16477cf --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/SelectItem.java @@ -0,0 +1,29 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +/** + * Created by dd on 16/1/13. + */ +public class SelectItem extends RelativeLayout { + + + private ImageView mImageView; + + public SelectItem(Context context) { + super(context); + mImageView = new ImageView(context); + mImageView.setAdjustViewBounds(true); + mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + LayoutParams mImageViewRLP = new LayoutParams(DensityUtil.dip2px(context, 60), DensityUtil.dip2px(context, 60)); + mImageViewRLP.addRule(RelativeLayout.CENTER_IN_PARENT); + mImageViewRLP.setMargins(0, DensityUtil.dip2px(context, 30), DensityUtil.dip2px(context, 20), 0); + addView(mImageView, mImageViewRLP); + } + + public ImageView getmImageView() { + return mImageView; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditText.java b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditText.java new file mode 100644 index 0000000..19d907a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditText.java @@ -0,0 +1,161 @@ +package com.hzecool.widget.riseedittext; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.hzecool.widget.R; +import com.hzecool.widget.utils.SizeUtils; + +/** + * 可以随着传入的字符串的长度增加或减少显示的方框形状的edittext,讲字符串分开显示 + * Created by song on 2017/6/19. + */ + +public class RiseEditText extends LinearLayout { + + private LinearLayout mLl_editext; + + public RiseEditText(Context context) { + this(context, null); + } + + public RiseEditText(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public RiseEditText(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + View.inflate(getContext(), R.layout.edittext_layout, this); + mLl_editext = (LinearLayout) findViewById(R.id.ll_edittext); + } + + private String content; + + public void setContent(String content) { + this.content = content; + //初始化edittext的数量 + initEditextNum(); + } + + private void initEditextNum() { + mLl_editext.removeAllViews(); + char[] chars = content.toCharArray(); + for (int i = 0; i < content.length(); i++) { + EditText editText = new EditText(getContext()); + // MaterialEditText materialEditText = new MaterialEditText(getContext()); + editText.setBackgroundResource(R.drawable.bg_square_voice); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); + editText.setPadding(SizeUtils.dp2px(getContext(), 10), 0, SizeUtils.dp2px(getContext(), 10), 0); + params.topMargin = SizeUtils.dp2px(getContext(), 4); + params.bottomMargin = SizeUtils.dp2px(getContext(), 4); + params.leftMargin = SizeUtils.dp2px(getContext(), 10); + editText.setTextSize(SizeUtils.sp2px(getContext(), 8)); + //限制输入内容的长度 + editText.setGravity(Gravity.CENTER); + editText.setText(String.valueOf(chars[i])); + //去除滑动内容弹出复制粘贴 + editText.setCustomSelectionActionModeCallback(mCallback); + //去除长按复制功能 + editText.setLongClickable(false); + //默认不全部选中内容 + editText.setSelectAllOnFocus(false); + //清除焦点 + editText.clearFocus(); + //默认不选中内容 + editText.setSelected(false); + //不显示光标 + editText.setCursorVisible(false); + //添加view + mLl_editext.addView(editText, params); + editText.setOnClickListener(mPointClickListener); + /*editText.setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + ((EditText) v).setText(null); + } + });*/ + } + } + + + /** + * 限制输入类型,1仅输入数字 0任意字符 默认是任意字符. + * + * @param type + */ + public void setInputType(int type) { + int childCount = mLl_editext.getChildCount(); + for (int i = 0; i < childCount; i++) { + EditText editText = (EditText) mLl_editext.getChildAt(i); + if (type == 0) { + //输入任意字符 + editText.setInputType(InputType.TYPE_CLASS_TEXT); + } else if (type == 1) { + //仅仅输入数字 + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + } + } + } + + /** + * 去掉复制粘贴的功能 + */ + private ActionMode.Callback mCallback = new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + + } + }; + + private OnClickListener mPointClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + ((EditText) v).setText(null); + } + }; + + /** + * 实现editText的getText()功能 + * + * @return + */ + public String getText() { + String str = ""; + int count = mLl_editext.getChildCount(); + for (int i = 0; i < count; i++) { + EditText child = (EditText) mLl_editext.getChildAt(i); + str = str + child.getText(); + } + return str; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditTextNum.java b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditTextNum.java new file mode 100644 index 0000000..f62c839 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditTextNum.java @@ -0,0 +1,144 @@ +package com.hzecool.widget.riseedittext; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.hzecool.widget.R; +import com.hzecool.widget.utils.SizeUtils; + +/** + * 仅支持输入数字的自增的edittext + * Created by song on 2017/7/3. + */ + +public class RiseEditTextNum extends LinearLayout { + private LinearLayout mLl_editext; + + public RiseEditTextNum(Context context) { + this(context,null); + } + + public RiseEditTextNum(Context context, @Nullable AttributeSet attrs) { + this(context, attrs,0); + } + + public RiseEditTextNum(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + View.inflate(getContext(), R.layout.edittext_layout, this); + mLl_editext = (LinearLayout) findViewById(R.id.ll_edittext); + } + + private String content; + + public void setContent(String content) { + this.content = content; + //初始化edittext的数量 + initEditextNum(); + } + + private void initEditextNum() { + mLl_editext.removeAllViews(); + char[] chars = content.toCharArray(); + for (int i = 0; i < content.length(); i++) { + EditText editText = new EditText(getContext()); + // MaterialEditText materialEditText = new MaterialEditText(getContext()); + editText.setBackgroundResource(R.drawable.bg_square_voice); + LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); + editText.setPadding(SizeUtils.dp2px(getContext(), 10), 0, SizeUtils.dp2px(getContext(), 10), 0); + params.topMargin = SizeUtils.dp2px(getContext(), 4); + params.bottomMargin = SizeUtils.dp2px(getContext(), 4); + params.leftMargin = SizeUtils.dp2px(getContext(), 10); + editText.setTextSize(SizeUtils.sp2px(getContext(), 8)); + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + //限制输入内容的长度 + // editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1)}); + editText.setGravity(Gravity.CENTER); + editText.setText(String.valueOf(chars[i])); + //去除滑动内容弹出复制粘贴 + editText.setCustomSelectionActionModeCallback(mCallback); + //去除长按复制功能 + editText.setLongClickable(false); + //默认不全部选中内容 + editText.setSelectAllOnFocus(false); + //清除焦点 + editText.clearFocus(); + //默认不选中内容 + editText.setSelected(false); + //不显示光标 + editText.setCursorVisible(false); + //添加view + mLl_editext.addView(editText, params); + editText.setOnClickListener(mPointClickListener); + /*editText.setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + ((EditText) v).setText(null); + } + });*/ + } + } + + + + /** + * 去掉复制粘贴的功能 + */ + private ActionMode.Callback mCallback = new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + + } + }; + + private OnClickListener mPointClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + ((EditText) v).setText(null); + } + }; + + /** + * 实现editText的getText()功能 + * + * @return + */ + public String getText() { + String str = ""; + int count = mLl_editext.getChildCount(); + for (int i = 0; i < count; i++) { + EditText child = (EditText) mLl_editext.getChildAt(i); + str = str + child.getText(); + } + return str; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/risenumber/IRiseNumber.java b/widget/src/main/java/com/hzecool/widget/risenumber/IRiseNumber.java new file mode 100644 index 0000000..d9bc4fd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/risenumber/IRiseNumber.java @@ -0,0 +1,43 @@ +package com.hzecool.widget.risenumber; + +/** + * Created by song on 2017/5/16. + */ + +public interface IRiseNumber { + /** + * 开始播放动画的方法 + */ + public void start(); + + /** + * 设置小数 + * + * @param number + * @return + */ + public void withNumber(float number); + + /** + * 设置整数 + * + * @param number + * @return + */ + public void withNumber(int number); + + /** + * 设置动画播放时长 + * + * @param duration + * @return + */ + public void setDuration(long duration); + + /** + * 设置动画结束监听器 + * + * @param callback + */ + public void setOnEndListener(RiseNumberTextView.EndListener callback); +} diff --git a/widget/src/main/java/com/hzecool/widget/risenumber/RiseNumberTextView.java b/widget/src/main/java/com/hzecool/widget/risenumber/RiseNumberTextView.java new file mode 100644 index 0000000..3d88587 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/risenumber/RiseNumberTextView.java @@ -0,0 +1,219 @@ +package com.hzecool.widget.risenumber; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.text.DecimalFormat; + +/** + * Created by song on 2017/5/16. + */ + +public class RiseNumberTextView extends TextView implements IRiseNumber{ + private static final int STOPPED = 0; + + private static final int RUNNING = 1; + + private int mPlayingState = STOPPED; + + private float number; + + private float fromNumber; + + /** + * 动画播放时长 + */ + private long duration = 1500; + /** + * 1.int 2.float + */ + private int numberType = 2; + + private DecimalFormat fnum; + + private EndListener mEndListener = null; + + final static int[] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999, + 99999999, 999999999, Integer.MAX_VALUE }; + + /** + * 构造方法 + * + * @param context + */ + public RiseNumberTextView(Context context) { + super(context); + } + + /** + * 使用xml布局文件默认的被调用的构造方法 + * + * @param context + * @param attr + */ + public RiseNumberTextView(Context context, AttributeSet attr) { + super(context, attr); + setTextColor(Color.RED); + setTextSize(30); + } + + public RiseNumberTextView(Context context, AttributeSet attr, int defStyle) { + super(context, attr, defStyle); + } + + /** + * 判断动画是否正在播放 + * + * @return + */ + public boolean isRunning() { + return (mPlayingState == RUNNING); + } + + /** + * 跑小数动画 + */ + private void runFloat() { + ValueAnimator valueAnimator = ValueAnimator.ofFloat(fromNumber, number); + valueAnimator.setDuration(duration); + + valueAnimator + .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + + setText(fnum.format(Float.parseFloat(valueAnimator + .getAnimatedValue().toString()))); + if (valueAnimator.getAnimatedFraction() >= 1) { + mPlayingState = STOPPED; + if (mEndListener != null) + mEndListener.onEndFinish(); + } + } + + + }); + + valueAnimator.start(); + } + + /** + * 跑整数动画 + */ + private void runInt() { + + ValueAnimator valueAnimator = ValueAnimator.ofInt((int) fromNumber, + (int) number); + valueAnimator.setDuration(duration); + + valueAnimator + .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + //设置瞬时的数据值到界面上 + setText(valueAnimator.getAnimatedValue().toString()); + if (valueAnimator.getAnimatedFraction() >= 1) { + //设置状态为停止 + mPlayingState = STOPPED; + if (mEndListener != null) + //通知监听器,动画结束事件 + mEndListener.onEndFinish(); + } + } + }); + valueAnimator.start(); + } + + static int sizeOfInt(int x) { + for (int i = 0;; i++){ + if (x <= sizeTable[i]) + return i + 1; + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + fnum = new DecimalFormat("##0.00"); + } + + /** + * 开始播放动画 + */ + @Override + public void start() { + + if (!isRunning()) { + mPlayingState = RUNNING; + if (numberType == 1) + runInt(); + else + runFloat(); + } + } + + /** + * 设置一个小数进来 + */ + @Override + public void withNumber(float number) { + + this.number = number; + numberType = 2; + /*if (number > 1000) { + fromNumber = number + - (float) Math.pow(10, sizeOfInt((int) number) - 1); + } else { + fromNumber = number / 2; + }*/ + fromNumber = number / 2; + + } + + /** + * 设置一个整数进来 + */ + @Override + public void withNumber(int number) { + this.number = number; + numberType = 1; + /* if (number > 1000) { + fromNumber = number + - (float) Math.pow(10, sizeOfInt((int) number) - 2); + } else { + fromNumber = number / 2; + }*/ + fromNumber = number / 2; + } + + /** + * 设置动画播放时间 + */ + @Override + public void setDuration(long duration) { + this.duration = duration; + } + + /** + * 设置动画结束监听器 + */ + @Override + public void setOnEndListener(EndListener callback) { + mEndListener = callback; + } + + /** + * 定义动画结束接口 + * + * + */ + public interface EndListener { + /** + * 当动画播放结束时的回调方法 + */ + public void onEndFinish(); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUD.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUD.java new file mode 100644 index 0000000..c3a0b0b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUD.java @@ -0,0 +1,348 @@ +package com.hzecool.widget.svprogresshud; + +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; + +import com.hzecool.widget.R; +import com.hzecool.widget.svprogresshud.listener.OnDismissListener; +import com.hzecool.widget.svprogresshud.view.SVCircleProgressBar; +import com.hzecool.widget.svprogresshud.view.SVProgressDefaultView; + +import java.lang.ref.WeakReference; + +/** + * Created by Sai on 15/8/15. + */ +public class SVProgressHUD { + private WeakReference contextWeak; + private static final long DISMISSDELAYED = 1000; + private SVProgressHUDMaskType mSVProgressHUDMaskType; + private boolean isShowing; + private boolean isDismissing; + + public enum SVProgressHUDMaskType { + None, // 允许遮罩下面控件点击 + Clear, // 不允许遮罩下面控件点击 + Black, // 不允许遮罩下面控件点击,背景黑色半透明 + Gradient, // 不允许遮罩下面控件点击,背景渐变半透明 + ClearCancel, // 不允许遮罩下面控件点击,点击遮罩消失 + BlackCancel, // 不允许遮罩下面控件点击,背景黑色半透明,点击遮罩消失 + GradientCancel // 不允许遮罩下面控件点击,背景渐变半透明,点击遮罩消失 + ; + } + + private final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM + ); + private ViewGroup decorView;//activity的根View + private ViewGroup rootView;// mSharedView 的 根View + private SVProgressDefaultView mSharedView; + + private Animation outAnim; + private Animation inAnim; + private int gravity = Gravity.CENTER; + private OnDismissListener onDismissListener; + + + public SVProgressHUD(Context context){ + this.contextWeak = new WeakReference<>(context); + gravity = Gravity.CENTER; + initViews(); + initDefaultView(); + initAnimation(); + } + + protected void initViews() { + Context context = contextWeak.get(); + if(context == null) return; + + LayoutInflater layoutInflater = LayoutInflater.from(context); + decorView = (ViewGroup) ((Activity) context).getWindow().getDecorView().findViewById(android.R.id.content); + rootView = (ViewGroup) layoutInflater.inflate(R.layout.layout_svprogresshud, null, false); + rootView.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + )); + } + protected void initDefaultView(){ + Context context = contextWeak.get(); + if(context == null) return; + + mSharedView = new SVProgressDefaultView(context); + params.gravity = gravity; + mSharedView.setLayoutParams(params); + } + + protected void initAnimation() { + if(inAnim == null) + inAnim = getInAnimation(); + if(outAnim == null) + outAnim = getOutAnimation(); + } + + /** + * show的时候调用 + */ + private void onAttached() { + isShowing = true; + decorView.addView(rootView); + if(mSharedView.getParent()!=null)((ViewGroup)mSharedView.getParent()).removeView(mSharedView); + rootView.addView(mSharedView); + } + + /** + * 添加这个View到Activity的根视图 + */ + private void svShow() { + + mHandler.removeCallbacksAndMessages(null); + onAttached(); + + mSharedView.startAnimation(inAnim); + + } + + public void show() { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.show(); + svShow(); + } + + public void showWithMaskType(SVProgressHUDMaskType maskType) { + if(isShowing())return; + //判断maskType + setMaskType(maskType); + mSharedView.show(); + svShow(); + } + + public void showWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showWithStatus(string); + svShow(); + } + + public void showWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showWithStatus(string); + svShow(); + } + + public void showInfoWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showInfoWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showInfoWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showInfoWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showSuccessWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showSuccessWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showSuccessWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showSuccessWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showErrorWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showErrorWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showErrorWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showErrorWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showWithProgress(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showWithProgress(string); + svShow(); + } + + public SVCircleProgressBar getProgressBar(){ + return mSharedView.getCircleProgressBar(); + } + public void setText(String string){ + mSharedView.setText(string); + } + + private void setMaskType(SVProgressHUDMaskType maskType) { + mSVProgressHUDMaskType = maskType; + switch (mSVProgressHUDMaskType) { + case None: + configMaskType(android.R.color.transparent, false, false); + break; + case Clear: + configMaskType(android.R.color.transparent, true, false); + break; + case ClearCancel: + configMaskType(android.R.color.transparent, true, true); + break; + case Black: + configMaskType(R.color.bgColor_overlay, true, false); + break; + case BlackCancel: + configMaskType(R.color.bgColor_overlay, true, true); + break; + case Gradient: + configMaskType(R.drawable.bg_overlay_gradient, true, false); + break; + case GradientCancel: + configMaskType(R.drawable.bg_overlay_gradient, true, true); + break; + default: + break; + } + } + + private void configMaskType(int bg, boolean clickable, boolean cancelable) { + rootView.setBackgroundResource(bg); + rootView.setClickable(clickable); + setCancelable(cancelable); + } + + /** + * 检测该View是不是已经添加到根视图 + * + * @return 如果视图已经存在该View返回true + */ + public boolean isShowing() { + return rootView.getParent() != null || isShowing; + } + + public void dismiss() { + if(isDismissing)return; + isDismissing = true; + //消失动画 + outAnim.setAnimationListener(outAnimListener); + mSharedView.dismiss(); + mSharedView.startAnimation(outAnim); + } + + public void dismissImmediately() { + mSharedView.dismiss(); + rootView.removeView(mSharedView); + decorView.removeView(rootView); + isShowing = false; + isDismissing = false; + if(onDismissListener != null){ + onDismissListener.onDismiss(this); + } + + } + + public Animation getInAnimation() { + Context context = contextWeak.get(); + if(context == null) return null; + + int res = SVProgressHUDAnimateUtil.getAnimationResource(this.gravity, true); + return AnimationUtils.loadAnimation(context, res); + } + + public Animation getOutAnimation() { + Context context = contextWeak.get(); + if(context == null) return null; + + int res = SVProgressHUDAnimateUtil.getAnimationResource(this.gravity, false); + return AnimationUtils.loadAnimation(context, res); + } + + private void setCancelable(boolean isCancelable) { + View view = rootView.findViewById(R.id.sv_outmost_container); + + if (isCancelable) { + view.setOnTouchListener(onCancelableTouchListener); + } else { + view.setOnTouchListener(null); + } + } + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + dismiss(); + } + }; + + private void scheduleDismiss() { + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessageDelayed(0, DISMISSDELAYED); + } + + /** + * Called when the user touch on black overlay in order to dismiss the dialog + */ + private final View.OnTouchListener onCancelableTouchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + dismiss(); + setCancelable(false); + } + return false; + } + }; + + private Animation.AnimationListener outAnimListener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + + } + + @Override + public void onAnimationEnd(Animation animation) { + dismissImmediately(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }; + + public void setOnDismissListener(OnDismissListener listener){ + this.onDismissListener = listener; + } + + public OnDismissListener getOnDismissListener(){ + return onDismissListener; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUDAnimateUtil.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUDAnimateUtil.java new file mode 100644 index 0000000..734acf3 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUDAnimateUtil.java @@ -0,0 +1,25 @@ +package com.hzecool.widget.svprogresshud; + +import android.view.Gravity; + +import com.hzecool.widget.R; + +/** + * Created by Sai on 15/8/16. + */ +public class SVProgressHUDAnimateUtil { + private static final int INVALID = -1; + static int getAnimationResource(int gravity, boolean isInAnimation) { + switch (gravity) { + case Gravity.TOP: + return isInAnimation ? R.anim.svslide_in_top : R.anim.svslide_out_top; + case Gravity.BOTTOM: + return isInAnimation ? R.anim.svslide_in_bottom : R.anim.svslide_out_bottom; + case Gravity.CENTER: + return isInAnimation ? R.anim.svfade_in_center : R.anim.svfade_out_center; + default: + // This case is not implemented because we don't expect any other gravity at the moment + } + return INVALID; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/listener/OnDismissListener.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/listener/OnDismissListener.java new file mode 100644 index 0000000..50f2530 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/listener/OnDismissListener.java @@ -0,0 +1,10 @@ +package com.hzecool.widget.svprogresshud.listener; + +import com.hzecool.widget.svprogresshud.SVProgressHUD; + +/** + * Created by Sai on 16/7/31. + */ +public interface OnDismissListener { + void onDismiss(SVProgressHUD hud); +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVCircleProgressBar.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVCircleProgressBar.java new file mode 100644 index 0000000..c651c69 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVCircleProgressBar.java @@ -0,0 +1,195 @@ +package com.hzecool.widget.svprogresshud.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import com.hzecool.widget.R; + + +/** + * Created by Sai on 15/9/1. + */ +public class SVCircleProgressBar extends View { + /** + * 画笔对象的引用 + */ + private Paint paint; + + /** + * 圆环的颜色 + */ + private int roundColor; + + /** + * 圆环进度的颜色 + */ + private int roundProgressColor; + + /** + * 圆环的宽度 + */ + private float roundWidth; + + /** + * 最大进度 + */ + private int max; + + /** + * 当前进度 + */ + private int progress; + + /** + * 进度的风格,实心或者空心 + */ + private int style; + + public static final int STROKE = 0; + public static final int FILL = 1; + + public SVCircleProgressBar(Context context) { + this(context, null); + } + + public SVCircleProgressBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SVCircleProgressBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + paint = new Paint(); + + TypedArray mTypedArray = context.obtainStyledAttributes(attrs, + R.styleable.SVCircleProgressBar); + + // 获取自定义属性和默认值 + roundColor = mTypedArray.getColor(R.styleable.SVCircleProgressBar_svprogress_roundColor, Color.BLUE); + roundProgressColor = mTypedArray.getColor(R.styleable.SVCircleProgressBar_svprogress_roundProgressColor, + Color.GRAY); + roundWidth = mTypedArray.getDimension(R.styleable.SVCircleProgressBar_svprogress_roundWidth, 5); + max = mTypedArray.getInteger(R.styleable.SVCircleProgressBar_svprogress_max, 100); + style = mTypedArray.getInt(R.styleable.SVCircleProgressBar_svprogress_style, 0); + + mTypedArray.recycle(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + /** + * 画最外层的大圆环 + */ + int centre = getWidth() / 2; // 获取圆心的x坐标 + int radius = (int) (centre - roundWidth / 2); // 圆环的半径 + paint.setAntiAlias(true); // 消除锯齿 + paint.setColor(roundColor); // 设置圆环的颜色 + paint.setStyle(Paint.Style.STROKE); // 设置空心 + paint.setStrokeWidth(roundWidth); // 设置圆环的宽度 + canvas.drawCircle(centre, centre, radius, paint); // 画出圆环 + + + /** + * 画圆弧 ,画圆环的进度 + */ + + // 设置进度是实心还是空心 + paint.setStrokeWidth(roundWidth); // 设置圆环的宽度 + paint.setColor(roundProgressColor); // 设置进度的颜色 + RectF oval = new RectF(centre - radius, centre - radius, centre + + radius, centre + radius); // 用于定义的圆弧的形状和大小的界限 + + switch (style) { + case STROKE: { + paint.setStyle(Paint.Style.STROKE); + canvas.drawArc(oval, 270, 360 * progress / max, false, paint); // 根据进度画圆弧 + break; + } + case FILL: { + paint.setStyle(Paint.Style.FILL_AND_STROKE); + if (progress != 0) + canvas.drawArc(oval, 270, 360 * progress / max, true, paint); // 根据进度画圆弧 + break; + } + } + + } + + public synchronized int getMax() { + return max; + } + + /** + * 设置进度的最大值 + * + * @param max + */ + public synchronized void setMax(int max) { + if (max < 0) { + throw new IllegalArgumentException("max not less than 0"); + } + this.max = max; + } + + /** + * 获取进度.需要同步 + * + * @return + */ + public synchronized int getProgress() { + return progress; + } + + /** + * 设置进度,此为线程安全控件,由于考虑多线的问题,需要同步 + * 刷新界面调用postInvalidate()能在非UI线程刷新 + * + * @param progress + */ + public synchronized void setProgress(int progress) { + if (progress < 0) { + throw new IllegalArgumentException("progress not less than 0"); + } + if (progress > max) { + progress = max; + } + if (progress <= max) { + this.progress = progress; + postInvalidate(); + } + + } + + public int getCircleColor() { + return roundColor; + } + + public void setCircleColor(int circleColor) { + this.roundColor = circleColor; + } + + public int getCircleProgressColor() { + return roundProgressColor; + } + + public void setCircleProgressColor(int circleProgressColor) { + this.roundProgressColor = circleProgressColor; + } + + public float getRoundWidth() { + return roundWidth; + } + + public void setRoundWidth(float roundWidth) { + this.roundWidth = roundWidth; + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVProgressDefaultView.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVProgressDefaultView.java new file mode 100644 index 0000000..de6ced6 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVProgressDefaultView.java @@ -0,0 +1,126 @@ +package com.hzecool.widget.svprogresshud.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.hzecool.widget.R; + + +/** + * Created by Sai on 15/8/15. + * 默认的SVProgress效果 + */ +public class SVProgressDefaultView extends LinearLayout { + private int resBigLoading = R.mipmap.ic_svstatus_loading; + private int resInfo = R.mipmap.ic_svstatus_info; + private int resSuccess = R.mipmap.ic_svstatus_success; + private int resError = R.mipmap.ic_svstatus_error; + private ImageView ivBigLoading, ivSmallLoading; + private SVCircleProgressBar circleProgressBar; + private TextView tvMsg; + + private RotateAnimation mRotateAnimation; + + public SVProgressDefaultView(Context context) { + super(context); + initViews(); + init(); + } + + private void initViews() { + LayoutInflater.from(getContext()).inflate(R.layout.view_svprogressdefault, this, true); + ivBigLoading = (ImageView) findViewById(R.id.ivBigLoading); + ivSmallLoading = (ImageView) findViewById(R.id.ivSmallLoading); + circleProgressBar = (SVCircleProgressBar) findViewById(R.id.circleProgressBar); + tvMsg = (TextView) findViewById(R.id.tvMsg); + } + + private void init() { + mRotateAnimation = new RotateAnimation(0f, 359f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + mRotateAnimation.setDuration(1000L); + mRotateAnimation.setInterpolator(new LinearInterpolator()); + mRotateAnimation.setRepeatCount(-1); + mRotateAnimation.setRepeatMode(Animation.RESTART); + } + + public void show() { + clearAnimations(); + ivBigLoading.setImageResource(resBigLoading); + ivBigLoading.setVisibility(View.VISIBLE); + ivSmallLoading.setVisibility(View.GONE); + circleProgressBar.setVisibility(View.GONE); + tvMsg.setVisibility(View.GONE); + //开启旋转动画 + ivBigLoading.startAnimation(mRotateAnimation); + } + + public void showWithStatus(String string) { + if (string == null) { + show(); + return; + } + showBaseStatus(resBigLoading, string); + //开启旋转动画 + ivSmallLoading.startAnimation(mRotateAnimation); + } + + public void showInfoWithStatus(String string) { + showBaseStatus(resInfo, string); + } + + public void showSuccessWithStatus(String string) { + showBaseStatus(resSuccess, string); + } + + public void showErrorWithStatus(String string) { + showBaseStatus(resError, string); + } + public void showWithProgress(String string) { + showProgress(string); + } + + public SVCircleProgressBar getCircleProgressBar() { + return circleProgressBar; + } + + public void setText(String string){ + tvMsg.setText(string); + } + + public void showProgress(String string) { + clearAnimations(); + tvMsg.setText(string); + ivBigLoading.setVisibility(View.GONE); + ivSmallLoading.setVisibility(View.GONE); + circleProgressBar.setVisibility(View.VISIBLE); + tvMsg.setVisibility(View.VISIBLE); + } + + public void showBaseStatus(int res, String string) { + clearAnimations(); + ivSmallLoading.setImageResource(res); + tvMsg.setText(string); + ivBigLoading.setVisibility(View.GONE); + circleProgressBar.setVisibility(View.GONE); + ivSmallLoading.setVisibility(View.VISIBLE); + tvMsg.setVisibility(View.VISIBLE); + } + + public void dismiss() { + clearAnimations(); + } + + private void clearAnimations() { + ivBigLoading.clearAnimation(); + ivSmallLoading.clearAnimation(); + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/swipdelete/SwipeMenuLayout.java b/widget/src/main/java/com/hzecool/widget/swipdelete/SwipeMenuLayout.java new file mode 100644 index 0000000..0b4f5ce --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/swipdelete/SwipeMenuLayout.java @@ -0,0 +1,621 @@ +package com.hzecool.widget.swipdelete; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.PointF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.OvershootInterpolator; + +import com.hzecool.widget.R; + +/** + * Created by tutu on 2017/5/17. + */ + +public class SwipeMenuLayout extends ViewGroup { + private static final String TAG = "zxt/SwipeMenuLayout"; + + private int mScaleTouchSlop;//为了处理单击事件的冲突 + private int mMaxVelocity;//计算滑动速度用 + private int mPointerId;//多点触摸只算第一根手指的速度 + private int mHeight;//自己的高度 + //右侧菜单宽度总和(最大滑动距离) + private int mRightMenuWidths; + + //滑动判定临界值(右侧菜单宽度的40%) 手指抬起时,超过了展开,没超过收起menu + private int mLimit; + + private View mContentView;//2016 11 13 add ,存储contentView(第一个View) + + //private Scroller mScroller;//以前item的滑动动画靠它做,现在用属性动画做 + //上一次的xy + private PointF mLastP = new PointF(); + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击除侧滑菜单之外的区域,关闭侧滑菜单。 + //增加一个布尔值变量,dispatch函数里,每次down时,为true,move时判断,如果是滑动动作,设为false。 + //在Intercept函数的up时,判断这个变量,如果仍为true 说明是点击事件,则关闭菜单。 + private boolean isUnMoved = true; + + //2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + //up-down的坐标,判断是否是滑动,如果是,则屏蔽一切点击事件 + private PointF mFirstP = new PointF(); + private boolean isUserSwiped; + + //存储的是当前正在展开的View + private static SwipeMenuLayout mViewCache; + + //防止多只手指一起滑我的flag 在每次down里判断, touch事件结束清空 + private static boolean isTouching; + + private VelocityTracker mVelocityTracker;//滑动速度变量 + private android.util.Log LogUtils; + + /** + * 右滑删除功能的开关,默认开 + */ + private boolean isSwipeEnable; + + /** + * IOS、QQ式交互,默认开 + */ + private boolean isIos; + + private boolean iosInterceptFlag;//IOS类型下,是否拦截事件的flag + + /** + * 20160929add 左滑右滑的开关,默认左滑打开菜单 + */ + private boolean isLeftSwipe; + + public SwipeMenuLayout(Context context) { + this(context, null); + } + + public SwipeMenuLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + public boolean isSwipeEnable() { + return isSwipeEnable; + } + + /** + * 设置侧滑功能开关 + * + * @param swipeEnable + */ + public void setSwipeEnable(boolean swipeEnable) { + isSwipeEnable = swipeEnable; + } + + + public boolean isIos() { + return isIos; + } + + /** + * 设置是否开启IOS阻塞式交互 + * + * @param ios + */ + public SwipeMenuLayout setIos(boolean ios) { + isIos = ios; + return this; + } + + public boolean isLeftSwipe() { + return isLeftSwipe; + } + + /** + * 设置是否开启左滑出菜单,设置false 为右滑出菜单 + * + * @param leftSwipe + * @return + */ + public SwipeMenuLayout setLeftSwipe(boolean leftSwipe) { + isLeftSwipe = leftSwipe; + return this; + } + + /** + * 返回ViewCache + * + * @return + */ + public static SwipeMenuLayout getViewCache() { + return mViewCache; + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + mScaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity(); + //初始化滑动帮助类对象 + //mScroller = new Scroller(context); + + //右滑删除功能的开关,默认开 + isSwipeEnable = true; + //IOS、QQ式交互,默认开 + isIos = true; + //左滑右滑的开关,默认左滑打开菜单 + isLeftSwipe = true; + TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout, defStyleAttr, 0); + int count = ta.getIndexCount(); + for (int i = 0; i < count; i++) { + int attr = ta.getIndex(i); + //如果引用成AndroidLib 资源都不是常量,无法使用switch case + if (attr == R.styleable.SwipeMenuLayout_swipeEnable) { + isSwipeEnable = ta.getBoolean(attr, true); + } else if (attr == R.styleable.SwipeMenuLayout_ios) { + isIos = ta.getBoolean(attr, true); + } else if (attr == R.styleable.SwipeMenuLayout_leftSwipe) { + isLeftSwipe = ta.getBoolean(attr, true); + } + } + ta.recycle(); + + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //Log.d(TAG, "onMeasure() called with: " + "widthMeasureSpec = [" + widthMeasureSpec + "], heightMeasureSpec = [" + heightMeasureSpec + "]"); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + setClickable(true);//令自己可点击,从而获取触摸事件 + + mRightMenuWidths = 0;//由于ViewHolder的复用机制,每次这里要手动恢复初始值 + mHeight = 0; + int contentWidth = 0;//2016 11 09 add,适配GridLayoutManager,将以第一个子Item(即ContentItem)的宽度为控件宽度 + int childCount = getChildCount(); + + //add by 2016 08 11 为了子View的高,可以matchParent(参考的FrameLayout 和LinearLayout的Horizontal) + final boolean measureMatchParentChildren = MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; + boolean isNeedMeasureChildHeight = false; + + for (int i = 0; i < childCount; i++) { + View childView = getChildAt(i); + //令每一个子View可点击,从而获取触摸事件 + childView.setClickable(true); + if (childView.getVisibility() != GONE) { + //后续计划加入上滑、下滑,则将不再支持Item的margin + measureChild(childView, widthMeasureSpec, heightMeasureSpec); + //measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0); + final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); + mHeight = Math.max(mHeight, childView.getMeasuredHeight()/* + lp.topMargin + lp.bottomMargin*/); + if (measureMatchParentChildren && lp.height == LayoutParams.MATCH_PARENT) { + isNeedMeasureChildHeight = true; + } + if (i > 0) {//第一个布局是Left item,从第二个开始才是RightMenu + mRightMenuWidths += childView.getMeasuredWidth(); + } else { + mContentView = childView; + contentWidth = childView.getMeasuredWidth(); + } + } + } + setMeasuredDimension(getPaddingLeft() + getPaddingRight() + contentWidth, + mHeight + getPaddingTop() + getPaddingBottom());//宽度取第一个Item(Content)的宽度 + mLimit = mRightMenuWidths * 4 / 10;//滑动判断的临界值 + //Log.d(TAG, "onMeasure() called with: " + "mRightMenuWidths = [" + mRightMenuWidths); + if (isNeedMeasureChildHeight) {//如果子View的height有MatchParent属性的,设置子View高度 + forceUniformHeight(childCount, widthMeasureSpec); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + /** + * 给MatchParent的子View设置高度 + * + * @param count + * @param widthMeasureSpec + * @see android.widget.LinearLayout# 同名方法 + */ + private void forceUniformHeight(int count, int widthMeasureSpec) { + // Pretend that the linear layout has an exact size. This is the measured height of + // ourselves. The measured height should be the max height of the children, changed + // to accommodate the heightMeasureSpec from the parent + int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), + MeasureSpec.EXACTLY);//以父布局高度构建一个Exactly的测量参数 + for (int i = 0; i < count; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + if (lp.height == LayoutParams.MATCH_PARENT) { + // Temporarily force children to reuse their old measured width + // FIXME: this may not be right for something like wrapping text? + int oldWidth = lp.width;//measureChildWithMargins 这个函数会用到宽,所以要保存一下 + lp.width = child.getMeasuredWidth(); + // Remeasure with new dimensions + measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0); + lp.width = oldWidth; + } + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + //LogUtils.e(TAG, "onLayout() called with: " + "changed = [" + changed + "], l = [" + l + "], t = [" + t + "], r = [" + r + "], b = [" + b + "]"); + int childCount = getChildCount(); + int left = 0 + getPaddingLeft(); + int right = 0 + getPaddingLeft(); + for (int i = 0; i < childCount; i++) { + View childView = getChildAt(i); + if (childView.getVisibility() != GONE) { + if (i == 0) {//第一个子View是内容 宽度设置为全屏 + childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight()); + left = left + childView.getMeasuredWidth(); + } else { + if (isLeftSwipe) { + childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight()); + left = left + childView.getMeasuredWidth(); + } else { + childView.layout(right - childView.getMeasuredWidth(), getPaddingTop(), right, getPaddingTop() + childView.getMeasuredHeight()); + right = right - childView.getMeasuredWidth(); + } + + } + } + } + //Log.d(TAG, "onLayout() called with: " + "maxScrollGap = [" + maxScrollGap + "], l = [" + l + "], t = [" + t + "], r = [" + r + "], b = [" + b + "]"); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + //LogUtils.d(TAG, "dispatchTouchEvent() called with: " + "ev = [" + ev + "]"); + if (isSwipeEnable) { + acquireVelocityTracker(ev); + final VelocityTracker verTracker = mVelocityTracker; + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + isUserSwiped = false;//2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + isUnMoved = true;//2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。 + iosInterceptFlag = false;//add by 2016 09 11 ,每次DOWN时,默认是不拦截的 + if (isTouching) {//如果有别的指头摸过了,那么就return false。这样后续的move..等事件也不会再来找这个View了。 + return false; + } else { + isTouching = true;//第一个摸的指头,赶紧改变标志,宣誓主权。 + } + mLastP.set(ev.getRawX(), ev.getRawY()); + mFirstP.set(ev.getRawX(), ev.getRawY());//2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + + //如果down,view和cacheview不一样,则立马让它还原。且把它置为null + if (mViewCache != null) { + if (mViewCache != this) { + mViewCache.smoothClose(); + + iosInterceptFlag = isIos;//add by 2016 09 11 ,IOS模式开启的话,且当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。 + } + //只要有一个侧滑菜单处于打开状态, 就不给外层布局上下滑动了 + getParent().requestDisallowInterceptTouchEvent(true); + } + //求第一个触点的id, 此时可能有多个触点,但至少一个,计算滑动速率用 + mPointerId = ev.getPointerId(0); + break; + case MotionEvent.ACTION_MOVE: + //add by 2016 09 11 ,IOS模式开启的话,且当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。滑动也不该出现 + if (iosInterceptFlag) { + break; + } + float gap = mLastP.x - ev.getRawX(); + //为了在水平滑动中禁止父类ListView等再竖直滑动 + if (Math.abs(gap) > 10 || Math.abs(getScrollX()) > 10) {//2016 09 29 修改此处,使屏蔽父布局滑动更加灵敏, + getParent().requestDisallowInterceptTouchEvent(true); + } + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。begin + if (Math.abs(gap) > mScaleTouchSlop) { + isUnMoved = false; + } + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。end + //如果scroller还没有滑动结束 停止滑动动画 +/* if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + }*/ + scrollBy((int) (gap), 0);//滑动使用scrollBy + //越界修正 + if (isLeftSwipe) {//左滑 + if (getScrollX() < 0) { + scrollTo(0, 0); + } + if (getScrollX() > mRightMenuWidths) { + scrollTo(mRightMenuWidths, 0); + } + } else {//右滑 + if (getScrollX() < -mRightMenuWidths) { + scrollTo(-mRightMenuWidths, 0); + } + if (getScrollX() > 0) { + scrollTo(0, 0); + } + } + + mLastP.set(ev.getRawX(), ev.getRawY()); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + //2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) { + isUserSwiped = true; + } + + //add by 2016 09 11 ,IOS模式开启的话,且当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。滑动也不该出现 + if (!iosInterceptFlag) {//且滑动了 才判断是否要收起、展开menu + //求伪瞬时速度 + verTracker.computeCurrentVelocity(1000, mMaxVelocity); + final float velocityX = verTracker.getXVelocity(mPointerId); + if (Math.abs(velocityX) > 1000) {//滑动速度超过阈值 + if (velocityX < -1000) { + if (isLeftSwipe) {//左滑 + //平滑展开Menu + smoothExpand(); + + } else { + //平滑关闭Menu + smoothClose(); + } + } else { + if (isLeftSwipe) {//左滑 + // 平滑关闭Menu + smoothClose(); + } else { + //平滑展开Menu + smoothExpand(); + + } + } + } else { + if (Math.abs(getScrollX()) > mLimit) {//否则就判断滑动距离 + //平滑展开Menu + smoothExpand(); + } else { + // 平滑关闭Menu + smoothClose(); + } + } + } + //释放 + releaseVelocityTracker(); + //LogUtils.i(TAG, "onTouch A ACTION_UP ACTION_CANCEL:velocityY:" + velocityX); + isTouching = false;//没有手指在摸我了 + break; + default: + break; + } + } + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + //add by zhangxutong 2016 12 07 begin: + //禁止侧滑时,点击事件不受干扰。 + if (isSwipeEnable) { + switch (ev.getAction()) { + //add by zhangxutong 2016 11 04 begin : + // fix 长按事件和侧滑的冲突。 + case MotionEvent.ACTION_MOVE: + //屏蔽滑动时的事件 + if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) { + return true; + } + break; + //add by zhangxutong 2016 11 04 end + case MotionEvent.ACTION_UP: + //为了在侧滑时,屏蔽子View的点击事件 + if (isLeftSwipe) { + if (getScrollX() > mScaleTouchSlop) { + //add by 2016 09 10 解决一个智障问题~ 居然不给点击侧滑菜单 我跪着谢罪 + //这里判断落点在内容区域屏蔽点击,内容区域外,允许传递事件继续向下的的。。。 + if (ev.getX() < getWidth() - getScrollX()) { + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。 + if (isUnMoved) { + smoothClose(); + } + return true;//true表示拦截 + } + } + } else { + if (-getScrollX() > mScaleTouchSlop) { + if (ev.getX() > -getScrollX()) {//点击范围在菜单外 屏蔽 + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。 + if (isUnMoved) { + smoothClose(); + } + return true; + } + } + } + //add by zhangxutong 2016 11 03 begin: + // 判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + if (isUserSwiped) { + return true; + } + //add by zhangxutong 2016 11 03 end + + break; + } + //模仿IOS 点击其他区域关闭: + if (iosInterceptFlag) { + //IOS模式开启,且当前有菜单的View,且不是自己的 拦截点击事件给子View + return true; + } + } + return super.onInterceptTouchEvent(ev); + } + + /** + * 平滑展开 + */ + private ValueAnimator mExpandAnim, mCloseAnim; + + private boolean isExpand;//代表当前是否是展开状态 2016 11 03 add + + public void smoothExpand() { + //Log.d(TAG, "smoothExpand() called" + this); + /*mScroller.startScroll(getScrollX(), 0, mRightMenuWidths - getScrollX(), 0); + invalidate();*/ + //展开就加入ViewCache: + mViewCache = SwipeMenuLayout.this; + + //2016 11 13 add 侧滑菜单展开,屏蔽content长按 + if (null != mContentView) { + mContentView.setLongClickable(false); + } + + cancelAnim(); + mExpandAnim = ValueAnimator.ofInt(getScrollX(), isLeftSwipe ? mRightMenuWidths : -mRightMenuWidths); + mExpandAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scrollTo((Integer) animation.getAnimatedValue(), 0); + } + }); + mExpandAnim.setInterpolator(new OvershootInterpolator()); + mExpandAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + isExpand = true; + } + }); + mExpandAnim.setDuration(300).start(); + } + + /** + * 每次执行动画之前都应该先取消之前的动画 + */ + private void cancelAnim() { + if (mCloseAnim != null && mCloseAnim.isRunning()) { + mCloseAnim.cancel(); + } + if (mExpandAnim != null && mExpandAnim.isRunning()) { + mExpandAnim.cancel(); + } + } + + /** + * 平滑关闭 + */ + public void smoothClose() { + //Log.d(TAG, "smoothClose() called" + this); +/* mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0); + invalidate();*/ + mViewCache = null; + + //2016 11 13 add 侧滑菜单展开,屏蔽content长按 + if (null != mContentView) { + mContentView.setLongClickable(true); + } + + cancelAnim(); + mCloseAnim = ValueAnimator.ofInt(getScrollX(), 0); + mCloseAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scrollTo((Integer) animation.getAnimatedValue(), 0); + } + }); + mCloseAnim.setInterpolator(new AccelerateInterpolator()); + mCloseAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + isExpand = false; + + } + }); + mCloseAnim.setDuration(300).start(); + //LogUtils.d(TAG, "smoothClose() called with:getScrollX() " + getScrollX()); + } + + + /** + * @param event 向VelocityTracker添加MotionEvent + * @see VelocityTracker#obtain() + * @see VelocityTracker#addMovement(MotionEvent) + */ + private void acquireVelocityTracker(final MotionEvent event) { + if (null == mVelocityTracker) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + } + + /** + * * 释放VelocityTracker + * + * @see VelocityTracker#clear() + * @see VelocityTracker#recycle() + */ + private void releaseVelocityTracker() { + if (null != mVelocityTracker) { + mVelocityTracker.clear(); + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + //每次ViewDetach的时候,判断一下 ViewCache是不是自己,如果是自己,关闭侧滑菜单,且ViewCache设置为null, + // 理由:1 防止内存泄漏(ViewCache是一个静态变量) + // 2 侧滑删除后自己后,这个View被Recycler回收,复用,下一个进入屏幕的View的状态应该是普通状态,而不是展开状态。 + @Override + protected void onDetachedFromWindow() { + if (this == mViewCache) { + mViewCache.smoothClose(); + mViewCache = null; + } + super.onDetachedFromWindow(); + } + + //展开时,禁止长按 + @Override + public boolean performLongClick() { + if (Math.abs(getScrollX()) > mScaleTouchSlop) { + return false; + } + return super.performLongClick(); + } + + //平滑滚动 弃用 改属性动画实现 +/* @Override + public void computeScroll() { + //判断Scroller是否执行完毕: + if (mScroller.computeScrollOffset()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + //通知View重绘-invalidate()->onDraw()->computeScroll() + invalidate(); + } + }*/ + + /** + * 快速关闭。 + * 用于 点击侧滑菜单上的选项,同时想让它快速关闭(删除 置顶)。 + * 这个方法在ListView里是必须调用的, + * 在RecyclerView里,视情况而定,如果是mAdapter.notifyItemRemoved(pos)方法不用调用。 + */ + public void quickClose() { + if (this == mViewCache) { + //先取消展开动画 + cancelAnim(); + mViewCache.scrollTo(0, 0);//关闭 + mViewCache = null; + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/holder/SimpleViewHolder.java b/widget/src/main/java/com/hzecool/widget/treeview/holder/SimpleViewHolder.java new file mode 100644 index 0000000..0e0f6fe --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/holder/SimpleViewHolder.java @@ -0,0 +1,29 @@ +package com.hzecool.widget.treeview.holder; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import com.hzecool.widget.treeview.model.TreeNode; + +/** + * Created by Bogdan Melnychuk on 2/11/15. + */ +public class SimpleViewHolder extends TreeNode.BaseNodeViewHolder { + + public SimpleViewHolder(Context context) { + super(context); + } + + @Override + public View createNodeView(TreeNode node, Object value) { + final TextView tv = new TextView(context); + tv.setText(String.valueOf(value)); + return tv; + } + + @Override + public void toggle(boolean active) { + + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/model/TreeNode.java b/widget/src/main/java/com/hzecool/widget/treeview/model/TreeNode.java new file mode 100644 index 0000000..b157662 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/model/TreeNode.java @@ -0,0 +1,284 @@ +package com.hzecool.widget.treeview.model; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import com.hzecool.widget.R; +import com.hzecool.widget.treeview.view.AndroidTreeView; +import com.hzecool.widget.treeview.view.TreeNodeWrapperView; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Created by Bogdan Melnychuk on 2/10/15. + */ +public class TreeNode { + public static final String NODES_ID_SEPARATOR = ":"; + + private int mId; + private int mLastId; + private TreeNode mParent; + private boolean mSelected; + private boolean mSelectable = true; + private final List children; + private BaseNodeViewHolder mViewHolder; + private TreeNodeClickListener mClickListener; + private TreeNodeLongClickListener mLongClickListener; + private Object mValue; + private boolean mExpanded; + + public static TreeNode root() { + TreeNode root = new TreeNode(null); + root.setSelectable(false); + return root; + } + + private int generateId() { + return ++mLastId; + } + + public TreeNode(Object value) { + children = new ArrayList<>(); + mValue = value; + } + + public TreeNode addChild(TreeNode childNode) { + childNode.mParent = this; + childNode.mId = generateId(); + children.add(childNode); + return this; + } + + public TreeNode addChildren(TreeNode... nodes) { + for (TreeNode n : nodes) { + addChild(n); + } + return this; + } + + public TreeNode addChildren(Collection nodes) { + for (TreeNode n : nodes) { + addChild(n); + } + return this; + } + + public int deleteChild(TreeNode child) { + for (int i = 0; i < children.size(); i++) { + if (child.mId == children.get(i).mId) { + children.remove(i); + return i; + } + } + return -1; + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public int size() { + return children.size(); + } + + public TreeNode getParent() { + return mParent; + } + + public int getId() { + return mId; + } + + public boolean isLeaf() { + return size() == 0; + } + + public Object getValue() { + return mValue; + } + + public boolean isExpanded() { + return mExpanded; + } + + public TreeNode setExpanded(boolean expanded) { + mExpanded = expanded; + return this; + } + + public void setSelected(boolean selected) { + mSelected = selected; + } + + public boolean isSelected() { + return mSelectable && mSelected; + } + + public void setSelectable(boolean selectable) { + mSelectable = selectable; + } + + public boolean isSelectable() { + return mSelectable; + } + + public String getPath() { + final StringBuilder path = new StringBuilder(); + TreeNode node = this; + while (node.mParent != null) { + path.append(node.getId()); + node = node.mParent; + if (node.mParent != null) { + path.append(NODES_ID_SEPARATOR); + } + } + return path.toString(); + } + + + public int getLevel() { + int level = 0; + TreeNode root = this; + while (root.mParent != null) { + root = root.mParent; + level++; + } + return level; + } + + public boolean isLastChild() { + if (!isRoot()) { + int parentSize = mParent.children.size(); + if (parentSize > 0) { + final List parentChildren = mParent.children; + return parentChildren.get(parentSize - 1).mId == mId; + } + } + return false; + } + + public TreeNode setViewHolder(BaseNodeViewHolder viewHolder) { + mViewHolder = viewHolder; + if (viewHolder != null) { + viewHolder.mNode = this; + } + return this; + } + + public TreeNode setClickListener(TreeNodeClickListener listener) { + mClickListener = listener; + return this; + } + + public TreeNodeClickListener getClickListener() { + return this.mClickListener; + } + + public TreeNode setLongClickListener(TreeNodeLongClickListener listener) { + mLongClickListener = listener; + return this; + } + + public TreeNodeLongClickListener getLongClickListener() { + return mLongClickListener; + } + + public BaseNodeViewHolder getViewHolder() { + return mViewHolder; + } + + public boolean isFirstChild() { + if (!isRoot()) { + List parentChildren = mParent.children; + return parentChildren.get(0).mId == mId; + } + return false; + } + + public boolean isRoot() { + return mParent == null; + } + + public TreeNode getRoot() { + TreeNode root = this; + while (root.mParent != null) { + root = root.mParent; + } + return root; + } + + public interface TreeNodeClickListener { + void onClick(TreeNode node, Object value); + } + + public interface TreeNodeLongClickListener { + boolean onLongClick(TreeNode node, Object value); + } + + public static abstract class BaseNodeViewHolder { + protected AndroidTreeView tView; + protected TreeNode mNode; + private View mView; + protected int containerStyle; + protected Context context; + + public BaseNodeViewHolder(Context context) { + this.context = context; + } + + public View getView() { + if (mView != null) { + return mView; + } + final View nodeView = getNodeView(); + final TreeNodeWrapperView nodeWrapperView = new TreeNodeWrapperView(nodeView.getContext(), getContainerStyle()); + nodeWrapperView.insertNodeView(nodeView); + mView = nodeWrapperView; + + return mView; + } + + public void setTreeViev(AndroidTreeView treeViev) { + this.tView = treeViev; + } + + public AndroidTreeView getTreeView() { + return tView; + } + + public void setContainerStyle(int style) { + containerStyle = style; + } + + public View getNodeView() { + return createNodeView(mNode, (E) mNode.getValue()); + } + + public ViewGroup getNodeItemsView() { + return (ViewGroup) getView().findViewById(R.id.node_items); + } + + public boolean isInitialized() { + return mView != null; + } + + public int getContainerStyle() { + return containerStyle; + } + + + public abstract View createNodeView(TreeNode node, E value); + + public void toggle(boolean active) { + // empty + } + + public void toggleSelectionMode(boolean editModeEnabled) { + // empty + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/view/AndroidTreeView.java b/widget/src/main/java/com/hzecool/widget/treeview/view/AndroidTreeView.java new file mode 100644 index 0000000..2bb33b9 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/view/AndroidTreeView.java @@ -0,0 +1,486 @@ +package com.hzecool.widget.treeview.view; + +import android.content.Context; +import android.text.TextUtils; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import com.hzecool.widget.R; +import com.hzecool.widget.treeview.holder.SimpleViewHolder; +import com.hzecool.widget.treeview.model.TreeNode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Created by wzg + */ +public class AndroidTreeView { + private static final String NODES_PATH_SEPARATOR = ";"; + + protected TreeNode mRoot; + private Context mContext; + private boolean applyForRoot; + private int containerStyle = 0; + private Class defaultViewHolderClass = SimpleViewHolder.class; + private TreeNode.TreeNodeClickListener nodeClickListener; + private TreeNode.TreeNodeLongClickListener nodeLongClickListener; + private boolean mSelectionModeEnabled; + private boolean mUseDefaultAnimation = false; + private boolean use2dScroll = false; + private boolean enableAutoToggle = true; + + public AndroidTreeView(Context context) { + mContext = context; + } + + public void setRoot(TreeNode mRoot) { + this.mRoot = mRoot; + } + + public AndroidTreeView(Context context, TreeNode root) { + mRoot = root; + mContext = context; + } + + public void setDefaultAnimation(boolean defaultAnimation) { + this.mUseDefaultAnimation = defaultAnimation; + } + + public void setDefaultContainerStyle(int style) { + setDefaultContainerStyle(style, false); + } + + public void setDefaultContainerStyle(int style, boolean applyForRoot) { + containerStyle = style; + this.applyForRoot = applyForRoot; + } + + public void setUse2dScroll(boolean use2dScroll) { + this.use2dScroll = use2dScroll; + } + + public boolean is2dScrollEnabled() { + return use2dScroll; + } + + public void setUseAutoToggle(boolean enableAutoToggle) { + this.enableAutoToggle = enableAutoToggle; + } + + public boolean isAutoToggleEnabled() { + return enableAutoToggle; + } + + public void setDefaultViewHolder(Class viewHolder) { + defaultViewHolderClass = viewHolder; + } + + public void setDefaultNodeClickListener(TreeNode.TreeNodeClickListener listener) { + nodeClickListener = listener; + } + + public void setDefaultNodeLongClickListener(TreeNode.TreeNodeLongClickListener listener) { + nodeLongClickListener = listener; + } + + public void expandAll() { + expandNode(mRoot, true); + } + + public void collapseAll() { + for (TreeNode n : mRoot.getChildren()) { + collapseNode(n, true); + } + } + + + public View getView(int style) { + final ViewGroup view; + if (style > 0) { + ContextThemeWrapper newContext = new ContextThemeWrapper(mContext, style); + view = use2dScroll ? new TwoDScrollView(newContext) : new ScrollView(newContext); + } else { + view = use2dScroll ? new TwoDScrollView(mContext) : new ScrollView(mContext); + } + + Context containerContext = mContext; + if (containerStyle != 0 && applyForRoot) { + containerContext = new ContextThemeWrapper(mContext, containerStyle); + } + final LinearLayout viewTreeItems = new LinearLayout(containerContext, null, containerStyle); + + viewTreeItems.setId(R.id.tree_items); + viewTreeItems.setOrientation(LinearLayout.VERTICAL); + view.addView(viewTreeItems); + + mRoot.setViewHolder(new TreeNode.BaseNodeViewHolder(mContext) { + @Override + public View createNodeView(TreeNode node, Object value) { + return null; + } + + @Override + public ViewGroup getNodeItemsView() { + return viewTreeItems; + } + }); + + expandNode(mRoot, false); + return view; + } + + public View getView() { + return getView(-1); + } + + + public void expandLevel(int level) { + for (TreeNode n : mRoot.getChildren()) { + expandLevel(n, level); + } + } + + private void expandLevel(TreeNode node, int level) { + if (node.getLevel() <= level) { + expandNode(node, false); + } + for (TreeNode n : node.getChildren()) { + expandLevel(n, level); + } + } + + public void expandNode(TreeNode node) { + expandNode(node, false); + } + + public void collapseNode(TreeNode node) { + collapseNode(node, false); + } + + public String getSaveState() { + final StringBuilder builder = new StringBuilder(); + getSaveState(mRoot, builder); + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } + return builder.toString(); + } + + public void restoreState(String saveState) { + if (!TextUtils.isEmpty(saveState)) { + collapseAll(); + final String[] openNodesArray = saveState.split(NODES_PATH_SEPARATOR); + final Set openNodes = new HashSet<>(Arrays.asList(openNodesArray)); + restoreNodeState(mRoot, openNodes); + } + } + + private void restoreNodeState(TreeNode node, Set openNodes) { + for (TreeNode n : node.getChildren()) { + if (openNodes.contains(n.getPath())) { + expandNode(n); + restoreNodeState(n, openNodes); + } + } + } + + private void getSaveState(TreeNode root, StringBuilder sBuilder) { + for (TreeNode node : root.getChildren()) { + if (node.isExpanded()) { + sBuilder.append(node.getPath()); + sBuilder.append(NODES_PATH_SEPARATOR); + getSaveState(node, sBuilder); + } + } + } + + public void toggleNode(TreeNode node) { + if (node.isExpanded()) { + collapseNode(node, false); + } else { + expandNode(node, false); + } + + } + + private void collapseNode(TreeNode node, final boolean includeSubnodes) { + node.setExpanded(false); + TreeNode.BaseNodeViewHolder nodeViewHolder = getViewHolderForNode(node); + + if (mUseDefaultAnimation) { + collapse(nodeViewHolder.getNodeItemsView()); + } else { + nodeViewHolder.getNodeItemsView().setVisibility(View.GONE); + } + nodeViewHolder.toggle(false); + if (includeSubnodes) { + for (TreeNode n : node.getChildren()) { + collapseNode(n, includeSubnodes); + } + } + } + + private void expandNode(final TreeNode node, boolean includeSubnodes) { + node.setExpanded(true); + final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(node); + parentViewHolder.getNodeItemsView().removeAllViews(); + + + parentViewHolder.toggle(true); + + for (final TreeNode n : node.getChildren()) { + addNode(parentViewHolder.getNodeItemsView(), n); + + if (n.isExpanded() || includeSubnodes) { + expandNode(n, includeSubnodes); + } + + } + if (mUseDefaultAnimation) { + expand(parentViewHolder.getNodeItemsView()); + } else { + parentViewHolder.getNodeItemsView().setVisibility(View.VISIBLE); + } + + } + + private void addNode(ViewGroup container, final TreeNode n) { + final TreeNode.BaseNodeViewHolder viewHolder = getViewHolderForNode(n); + final View nodeView = viewHolder.getView(); + container.addView(nodeView); + if (mSelectionModeEnabled) { + viewHolder.toggleSelectionMode(mSelectionModeEnabled); + } + + nodeView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (n.getClickListener() != null) { + n.getClickListener().onClick(n, n.getValue()); + } else if (nodeClickListener != null) { + nodeClickListener.onClick(n, n.getValue()); + } + if (enableAutoToggle) { + toggleNode(n); + } + } + }); + + nodeView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if (n.getLongClickListener() != null) { + return n.getLongClickListener().onLongClick(n, n.getValue()); + } else if (nodeLongClickListener != null) { + return nodeLongClickListener.onLongClick(n, n.getValue()); + } + if (enableAutoToggle) { + toggleNode(n); + } + return false; + } + }); + } + + //------------------------------------------------------------ + // Selection methods + + public void setSelectionModeEnabled(boolean selectionModeEnabled) { + if (!selectionModeEnabled) { + // TODO fix double iteration over tree + deselectAll(); + } + mSelectionModeEnabled = selectionModeEnabled; + + for (TreeNode node : mRoot.getChildren()) { + toggleSelectionMode(node, selectionModeEnabled); + } + + } + + public List getSelectedValues(Class clazz) { + List result = new ArrayList<>(); + List selected = getSelected(); + for (TreeNode n : selected) { + Object value = n.getValue(); + if (value != null && value.getClass().equals(clazz)) { + result.add((E) value); + } + } + return result; + } + + public boolean isSelectionModeEnabled() { + return mSelectionModeEnabled; + } + + private void toggleSelectionMode(TreeNode parent, boolean mSelectionModeEnabled) { + toogleSelectionForNode(parent, mSelectionModeEnabled); + if (parent.isExpanded()) { + for (TreeNode node : parent.getChildren()) { + toggleSelectionMode(node, mSelectionModeEnabled); + } + } + } + + public List getSelected() { + if (mSelectionModeEnabled) { + return getSelected(mRoot); + } else { + return new ArrayList<>(); + } + } + + // TODO Do we need to go through whole tree? Save references or consider collapsed nodes as not selected + private List getSelected(TreeNode parent) { + List result = new ArrayList<>(); + for (TreeNode n : parent.getChildren()) { + if (n.isSelected()) { + result.add(n); + } + result.addAll(getSelected(n)); + } + return result; + } + + public void selectAll(boolean skipCollapsed) { + makeAllSelection(true, skipCollapsed); + } + + public void deselectAll() { + makeAllSelection(false, false); + } + + private void makeAllSelection(boolean selected, boolean skipCollapsed) { + if (mSelectionModeEnabled) { + for (TreeNode node : mRoot.getChildren()) { + selectNode(node, selected, skipCollapsed); + } + } + } + + public void selectNode(TreeNode node, boolean selected) { + if (mSelectionModeEnabled) { + node.setSelected(selected); + toogleSelectionForNode(node, true); + } + } + + private void selectNode(TreeNode parent, boolean selected, boolean skipCollapsed) { + parent.setSelected(selected); + toogleSelectionForNode(parent, true); + boolean toContinue = skipCollapsed ? parent.isExpanded() : true; + if (toContinue) { + for (TreeNode node : parent.getChildren()) { + selectNode(node, selected, skipCollapsed); + } + } + } + + private void toogleSelectionForNode(TreeNode node, boolean makeSelectable) { + TreeNode.BaseNodeViewHolder holder = getViewHolderForNode(node); + if (holder.isInitialized()) { + getViewHolderForNode(node).toggleSelectionMode(makeSelectable); + } + } + + private TreeNode.BaseNodeViewHolder getViewHolderForNode(TreeNode node) { + TreeNode.BaseNodeViewHolder viewHolder = node.getViewHolder(); + if (viewHolder == null) { + try { + final Object object = defaultViewHolderClass.getConstructor(Context.class).newInstance(mContext); + viewHolder = (TreeNode.BaseNodeViewHolder) object; + node.setViewHolder(viewHolder); + } catch (Exception e) { + throw new RuntimeException("Could not instantiate class " + defaultViewHolderClass); + } + } + if (viewHolder.getContainerStyle() <= 0) { + viewHolder.setContainerStyle(containerStyle); + } + if (viewHolder.getTreeView() == null) { + viewHolder.setTreeViev(this); + } + return viewHolder; + } + + private static void expand(final View v) { + v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + final int targetHeight = v.getMeasuredHeight(); + + v.getLayoutParams().height = 0; + v.setVisibility(View.VISIBLE); + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + v.getLayoutParams().height = interpolatedTime == 1 + ? LinearLayout.LayoutParams.WRAP_CONTENT + : (int) (targetHeight * interpolatedTime); + v.requestLayout(); + } + + @Override + public boolean willChangeBounds() { + return true; + } + }; + + // 1dp/ms + a.setDuration((int) (targetHeight / v.getContext().getResources().getDisplayMetrics().density)); + v.startAnimation(a); + } + + private static void collapse(final View v) { + final int initialHeight = v.getMeasuredHeight(); + + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + if (interpolatedTime == 1) { + v.setVisibility(View.GONE); + } else { + v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime); + v.requestLayout(); + } + } + + @Override + public boolean willChangeBounds() { + return true; + } + }; + + // 1dp/ms + a.setDuration((int) (initialHeight / v.getContext().getResources().getDisplayMetrics().density)); + v.startAnimation(a); + } + + //----------------------------------------------------------------- + //Add / Remove + + public void addNode(TreeNode parent, final TreeNode nodeToAdd) { + parent.addChild(nodeToAdd); + if (parent.isExpanded()) { + final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent); + addNode(parentViewHolder.getNodeItemsView(), nodeToAdd); + } + } + + public void removeNode(TreeNode node) { + if (node.getParent() != null) { + TreeNode parent = node.getParent(); + int index = parent.deleteChild(node); + if (parent.isExpanded() && index >= 0) { + final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent); + parentViewHolder.getNodeItemsView().removeViewAt(index); + } + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/view/TreeNodeWrapperView.java b/widget/src/main/java/com/hzecool/widget/treeview/view/TreeNodeWrapperView.java new file mode 100644 index 0000000..a96a2af --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/view/TreeNodeWrapperView.java @@ -0,0 +1,53 @@ +package com.hzecool.widget.treeview.view; + +import android.content.Context; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import com.hzecool.widget.R; + + +/** + * Created by Bogdan Melnychuk on 2/10/15. + */ +public class TreeNodeWrapperView extends LinearLayout { + private LinearLayout nodeItemsContainer; + private ViewGroup nodeContainer; + private final int containerStyle; + + public TreeNodeWrapperView(Context context, int containerStyle) { + super(context); + this.containerStyle = containerStyle; + init(); + } + + private void init() { + setOrientation(LinearLayout.VERTICAL); + + nodeContainer = new RelativeLayout(getContext()); + nodeContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + nodeContainer.setId(R.id.node_header); + + ContextThemeWrapper newContext = new ContextThemeWrapper(getContext(), containerStyle); + nodeItemsContainer = new LinearLayout(newContext, null, containerStyle); + nodeItemsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + nodeItemsContainer.setId(R.id.node_items); + nodeItemsContainer.setOrientation(LinearLayout.VERTICAL); + nodeItemsContainer.setVisibility(View.GONE); + + addView(nodeContainer); + addView(nodeItemsContainer); + } + + + public void insertNodeView(View nodeView) { + nodeContainer.addView(nodeView); + } + + public ViewGroup getNodeContainer() { + return nodeContainer; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/view/TwoDScrollView.java b/widget/src/main/java/com/hzecool/widget/treeview/view/TwoDScrollView.java new file mode 100644 index 0000000..2982314 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/view/TwoDScrollView.java @@ -0,0 +1,1107 @@ +package com.hzecool.widget.treeview.view; + + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Scroller; +import android.widget.TextView; + +import java.util.List; + +/** + * Layout container for a view hierarchy that can be scrolled by the user, + * allowing it to be larger than the physical display. A TwoDScrollView + * is a {@link FrameLayout}, meaning you should place one child in it + * containing the entire contents to scroll; this child may itself be a layout + * manager with a complex hierarchy of objects. A child that is often used + * is a {@link LinearLayout} in a vertical orientation, presenting a vertical + * array of top-level items that the user can scroll through. + * he {@link TextView} class also + * takes care of its own scrolling, so does not require a TwoDScrollView, but + * using the two together is possible to achieve the effect of a text view + * within a larger container. + */ +public class TwoDScrollView extends FrameLayout { + static final int ANIMATED_SCROLL_GAP = 250; + static final float MAX_SCROLL_FACTOR = 0.5f; + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private Scroller mScroller; + + /** + * Flag to indicate that we are moving focus ourselves. This is so the + * code that watches for focus changes initiated outside this TwoDScrollView + * knows that it does not have to do anything. + */ + private boolean mTwoDScrollViewMovedFocus; + + /** + * Position of the last motion event. + */ + private float mLastMotionY; + private float mLastMotionX; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this TwoDScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * Whether arrow scrolling is animated. + */ + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + public TwoDScrollView(Context context) { + super(context); + initTwoDScrollView(); + } + + public TwoDScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + initTwoDScrollView(); + } + + public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initTwoDScrollView(); + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + if (getScrollY() < length) { + return getScrollY() / (float) length; + } + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + return 1.0f; + } + + @Override + protected float getLeftFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + if (getScrollX() < length) { + return getScrollX() / (float) length; + } + return 1.0f; + } + + @Override + protected float getRightFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollX() - rightEdge; + if (span < length) { + return span / (float) length; + } + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmountVertical() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + public int getMaxScrollAmountHorizontal() { + return (int) (MAX_SCROLL_FACTOR * getWidth()); + } + + private void initTwoDScrollView() { + mScroller = new Scroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index, params); + } + + /** + * @return Returns true this TwoDScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + int childWidth = child.getWidth(); + return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) || + (getWidth() < childWidth + getPaddingLeft() + getPaddingRight()); + } + return false; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + boolean handled = super.dispatchKeyEvent(event); + if (handled) { + return true; + } + return executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + if (!canScroll()) { + if (isFocused()) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); + return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP, false); + } else { + handled = fullScroll(View.FOCUS_UP, false); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN, false); + } else { + handled = fullScroll(View.FOCUS_DOWN, false); + } + break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_LEFT, true); + } else { + handled = fullScroll(View.FOCUS_LEFT, true); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_RIGHT, true); + } else { + handled = fullScroll(View.FOCUS_RIGHT, true); + } + break; + } + } + return handled; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + * + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + if (!canScroll()) { + mIsBeingDragged = false; + return false; + } + final float y = ev.getY(); + final float x = ev.getX(); + switch (action) { + case MotionEvent.ACTION_MOVE: + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int yDiff = (int) Math.abs(y - mLastMotionY); + final int xDiff = (int) Math.abs(x - mLastMotionX); + if (yDiff > mTouchSlop || xDiff > mTouchSlop) { + mIsBeingDragged = true; + } + break; + + case MotionEvent.ACTION_DOWN: + /* Remember location of down touch */ + mLastMotionY = y; + mLastMotionX = x; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mIsBeingDragged = !mScroller.isFinished(); + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (!canScroll()) { + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + final float y = ev.getY(); + final float x = ev.getX(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = y; + mLastMotionX = x; + break; + case MotionEvent.ACTION_MOVE: + // Scroll to follow the motion event + int deltaX = (int) (mLastMotionX - x); + int deltaY = (int) (mLastMotionY - y); + mLastMotionX = x; + mLastMotionY = y; + + if (deltaX < 0) { + if (getScrollX() < 0) { + deltaX = 0; + } + } else if (deltaX > 0) { + final int rightEdge = getWidth() - getPaddingRight(); + final int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge; + if (availableToScroll > 0) { + deltaX = Math.min(availableToScroll, deltaX); + } else { + deltaX = 0; + } + } + if (deltaY < 0) { + if (getScrollY() < 0) { + deltaY = 0; + } + } else if (deltaY > 0) { + final int bottomEdge = getHeight() - getPaddingBottom(); + final int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (availableToScroll > 0) { + deltaY = Math.min(availableToScroll, deltaY); + } else { + deltaY = 0; + } + } + if (deltaY != 0 || deltaX != 0) + scrollBy(deltaX, deltaY); + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialXVelocity = (int) velocityTracker.getXVelocity(); + int initialYVelocity = (int) velocityTracker.getYVelocity(); + if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) && getChildCount() > 0) { + fling(-initialXVelocity, -initialYVelocity); + } + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + return true; + } + + /** + * Finds the next focusable component that fits in this View's bounds + * (excluding fading edges) pretending that this View's top is located at + * the parameter top. + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found (the fading edge is assumed to start at this position) + * @param preferredFocusable the View that has highest priority and will be + * returned if it is within my bounds (null is valid) + * @return the next focusable component in the bounds or null if none can be + * found + */ + private View findFocusableViewInMyBounds(final boolean topFocus, final int top, final boolean leftFocus, final int left, View preferredFocusable) { + /* + * The fading edge's transparent side should be considered for focus + * since it's mostly visible, so we divide the actual fading edge length + * by 2. + */ + final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2; + final int topWithoutFadingEdge = top + verticalFadingEdgeLength; + final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength; + final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2; + final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength; + final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength; + + if ((preferredFocusable != null) + && (preferredFocusable.getTop() < bottomWithoutFadingEdge) + && (preferredFocusable.getBottom() > topWithoutFadingEdge) + && (preferredFocusable.getLeft() < rightWithoutFadingEdge) + && (preferredFocusable.getRight() > leftWithoutFadingEdge)) { + return preferredFocusable; + } + return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge); + } + + /** + * Finds the next focusable component that fits in the specified bounds. + *

+ * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, int left, int right) { + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + int viewLeft = view.getLeft(); + int viewRight = view.getRight(); + + if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) && (left < viewLeft) && (viewRight < right); + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToVerticalBoundary = + (topFocus && viewTop < focusCandidate.getTop()) || + (!topFocus && viewBottom > focusCandidate.getBottom()); + final boolean viewIsCloserToHorizontalBoundary = + (leftFocus && viewLeft < focusCandidate.getLeft()) || + (!leftFocus && viewRight > focusCandidate.getRight()); + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + return focusCandidate; + } + + /** + *

Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction, boolean horizontal) { + if (!horizontal) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + mTempRect.top = 0; + mTempRect.bottom = height; + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0); + } else { + boolean right = direction == View.FOCUS_DOWN; + int width = getWidth(); + mTempRect.left = 0; + mTempRect.right = width; + if (right) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.right = view.getBottom(); + mTempRect.left = mTempRect.right - width; + } + } + return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom); + } + } + + /** + *

Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this scrollview.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward + * {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, int right) { + boolean handled = true; + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = directionY == View.FOCUS_UP; + int width = getWidth(); + int containerLeft = getScrollX(); + int containerRight = containerLeft + width; + boolean leftwards = directionX == View.FOCUS_UP; + View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right); + if (newFocused == null) { + newFocused = this; + } + if ((top >= containerTop && bottom <= containerBottom) || (left >= containerLeft && right <= containerRight)) { + handled = false; + } else { + int deltaY = up ? (top - containerTop) : (bottom - containerBottom); + int deltaX = leftwards ? (left - containerLeft) : (right - containerRight); + doScroll(deltaX, deltaY); + } + if (newFocused != findFocus() && newFocused.requestFocus(directionY)) { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction, boolean horizontal) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + final int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical(); + + if (!horizontal) { + if (nextFocused != null) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(0, scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + } else { + if (nextFocused != null) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta, 0); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0); + } + } + return true; + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScroll(int deltaX, int deltaY) { + if (deltaX != 0 || deltaY != 0) { + smoothScrollBy(deltaX, deltaY); + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + mScroller.startScroll(getScrollX(), getScrollY(), dx, dy); + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

The scroll range of a scroll view is the overall height of all of its + * children.

+ */ + @Override + protected int computeVerticalScrollRange() { + int count = getChildCount(); + return count == 0 ? getHeight() : (getChildAt(0)).getBottom(); + } + + @Override + protected int computeHorizontalScrollRange() { + int count = getChildCount(); + return count == 0 ? getWidth() : (getChildAt(0)).getRight(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + // This is called at drawing time by ViewGroup. We don't want to + // re-show the scrollbars at this point, which scrollTo will do, + // so we replicate most of scrollTo here. + // + // It's a little odd to call onScrollChanged from inside the drawing. + // + // It is, except when you remember that computeScroll() is used to + // animate scrolling. So unless we want to defer the onScrollChanged() + // until the end of the animated scrolling, we don't really have a + // choice here. + // + // I agree. The alternative, which I think would be worse, is to post + // something and tell the subclasses later. This is bad because there + // will be a window where mScrollX/Y is different from what the app + // thinks it is. + // + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollTo(clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()), + clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight())); + } else { + scrollTo(x, y); + } + if (oldX != getScrollX() || oldY != getScrollY()) { + onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); + } + + // Keep on drawing until the animation has finished. + postInvalidate(); + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + /* Offset from child's local coordinates to TwoDScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return 0; + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + int fadingEdge = getVerticalFadingEdgeLength(); + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + int scrollYDelta = 0; + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mTwoDScrollViewMovedFocus) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + } + super.requestChildFocus(child, focused); + } + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null ? + FocusFinder.getInstance().findNextFocus(this, null, direction) : + FocusFinder.getInstance().findNextFocusFromRect(this, + previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + // Calling this with the present values causes it to re-clam them + scrollTo(getScrollX(), getScrollY()); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) + return; + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDeltaX, scrollDeltaY); + } + + /** + * Return true if child is an descendant of parent, (or equal to the parent). + */ + private boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/curor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityX, int velocityY) { + if (getChildCount() > 0) { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + int width = getWidth() - getPaddingRight() - getPaddingLeft(); + int right = getChildAt(0).getWidth(); + + mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, bottom - height); + + final boolean movingDown = velocityY > 0; + final boolean movingRight = velocityX > 0; + + View newFocused = findFocusableViewInMyBounds(movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus()); + if (newFocused == null) { + newFocused = this; + } + + if (newFocused != findFocus() && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } + } + + /** + * {@inheritDoc} + * This version also clamps the scrolling to the bounds of our child. + */ + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/GlideSetting.java b/widget/src/main/java/com/hzecool/widget/utils/GlideSetting.java new file mode 100644 index 0000000..cec9a5e --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/GlideSetting.java @@ -0,0 +1,22 @@ +package com.hzecool.widget.utils; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; + +/** + * Created by 47066 on 2017/9/7. + */ + +public class GlideSetting { + public static RequestOptions getGlideSetting() { + RequestOptions options = new RequestOptions() + .centerCrop() + //.placeholder(R.drawable.placeholder) + //.error(R.drawable.error) + .priority(Priority.HIGH) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC); + return options; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/ImageUtils.java b/widget/src/main/java/com/hzecool/widget/utils/ImageUtils.java new file mode 100644 index 0000000..b92d95b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/ImageUtils.java @@ -0,0 +1,146 @@ +package com.hzecool.widget.utils; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ThumbnailUtils; +import android.net.Uri; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.util.DisplayMetrics; +import android.util.Log; + +/** + * 图片简单处理工具类 + */ +public class ImageUtils { + + /** + * 屏幕宽 + * + * @param context + * @return + */ + public static int getWidth(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.widthPixels; + } + + /** + * 屏幕高 + * + * @param context + * @return + */ + public static int getHeight(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.heightPixels; + } + + /** + * 解决小米、魅族等定制ROM + * @param context + * @param intent + * @return + */ + public static Uri getUri(Context context , Intent intent) { + Uri uri = intent.getData(); + String type = intent.getType(); + if (uri.getScheme().equals("file") && (type.contains("image/"))) { + String path = uri.getEncodedPath(); + if (path != null) { + path = Uri.decode(path); + ContentResolver cr = context.getContentResolver(); + StringBuffer buff = new StringBuffer(); + buff.append("(").append(Images.ImageColumns.DATA).append("=") + .append("'" + path + "'").append(")"); + Cursor cur = cr.query(Images.Media.EXTERNAL_CONTENT_URI, + new String[] { Images.ImageColumns._ID }, + buff.toString(), null, null); + int index = 0; + for (cur.moveToFirst(); !cur.isAfterLast(); cur.moveToNext()) { + index = cur.getColumnIndex(Images.ImageColumns._ID); + // set _id value + index = cur.getInt(index); + } + if (index == 0) { + // do nothing + } else { + Uri uri_temp = Uri + .parse("content://media/external/images/media/" + + index); + if (uri_temp != null) { + uri = uri_temp; + Log.i("urishi", uri.toString()); + } + } + } + } + return uri; + } + + /** + * 根据文件Uri获取路径 + * + * @param context + * @param uri + * @return + */ + public static String getFilePathByFileUri(Context context, Uri uri) { + String filePath = null; + Cursor cursor = context.getContentResolver().query(uri, null, null, + null, null); + if (cursor.moveToFirst()) { + filePath = cursor.getString(cursor + .getColumnIndex(MediaStore.Images.Media.DATA)); + } + cursor.close(); + return filePath; + } + + /** + * 根据图片原始路径获取图片缩略图 + * + * @param imagePath 图片原始路径 + * @param width 缩略图宽度 + * @param height 缩略图高度 + * @return + */ + public static Bitmap getImageThumbnail(String imagePath, int width, + int height) { + Bitmap bitmap = null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true;//不加载直接获取Bitmap宽高 + // 获取这个图片的宽和高,注意此处的bitmap为null + bitmap = BitmapFactory.decodeFile(imagePath, options); + if(bitmap == null){ + // 计算缩放比 + int h = options.outHeight; + int w = options.outWidth; + Log.i("test", "optionsH"+h+"optionsW"+w); + int beWidth = w / width; + int beHeight = h / height; + int rate = 1; + if (beWidth < beHeight) { + rate = beWidth; + } else { + rate = beHeight; + } + if (rate <= 0) {//图片实际大小小于缩略图,不缩放 + rate = 1; + } + options.inSampleSize = rate; + options.inJustDecodeBounds = false; + // 重新读入图片,读取缩放后的bitmap,注意这次要把options.inJustDecodeBounds 设为 false + bitmap = BitmapFactory.decodeFile(imagePath, options); + // 利用ThumbnailUtils来创建缩略图,这里要指定要缩放哪个Bitmap对象 + bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height, + ThumbnailUtils.OPTIONS_RECYCLE_INPUT); + } + return bitmap; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/SizeUtils.java b/widget/src/main/java/com/hzecool/widget/utils/SizeUtils.java new file mode 100644 index 0000000..6d3135a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/SizeUtils.java @@ -0,0 +1,179 @@ +package com.hzecool.widget.utils; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2016/8/2
+ *     desc  : 尺寸相关工具类
+ * 
+ */ +public class SizeUtils { + + private SizeUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * dp转px + * + * @param dpValue dp值 + * @return px值 + */ + public static int dp2px(Context context, double dpValue) { + final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * px转dp + * + * @param pxValue px值 + * @return dp值 + */ + public static int px2dp(Context context, float pxValue) { + final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * sp转px + * + * @param spValue sp值 + * @return px值 + */ + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + /** + * px转sp + * + * @param pxValue px值 + * @return sp值 + */ + public static int px2sp(Context context, float pxValue) { + final float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 各种单位转换 + *

该方法存在于TypedValue

+ * + * @param unit 单位 + * @param value 值 + * @param metrics DisplayMetrics + * @return 转换结果 + */ + public static float applyDimension(int unit, float value, DisplayMetrics metrics) { + switch (unit) { + case TypedValue.COMPLEX_UNIT_PX: + return value; + case TypedValue.COMPLEX_UNIT_DIP: + return value * metrics.density; + case TypedValue.COMPLEX_UNIT_SP: + return value * metrics.scaledDensity; + case TypedValue.COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0f / 72); + case TypedValue.COMPLEX_UNIT_IN: + return value * metrics.xdpi; + case TypedValue.COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0f / 25.4f); + } + return 0; + } + + /** + * 在onCreate中获取视图的尺寸 + *

需回调onGetSizeListener接口,在onGetSize中获取view宽高

+ *

用法示例如下所示

+ *
+     * SizeUtils.forceGetViewSize(view, new SizeUtils.onGetSizeListener() {
+     *     Override
+     *     public void onGetSize(View view) {
+     *         view.getWidth();
+     *     }
+     * });
+     * 
+ * + * @param view 视图 + * @param listener 监听器 + */ + public static void forceGetViewSize(final View view, final onGetSizeListener listener) { + view.post(new Runnable() { + @Override + public void run() { + if (listener != null) { + listener.onGetSize(view); + } + } + }); + } + + /** + * 获取到View尺寸的监听 + */ + public interface onGetSizeListener { + void onGetSize(View view); + } + + public static void setListener(onGetSizeListener listener) { + mListener = listener; + } + + private static onGetSizeListener mListener; + + /** + * 测量视图尺寸 + * + * @param view 视图 + * @return arr[0]: 视图宽度, arr[1]: 视图高度 + */ + public static int[] measureView(View view) { + ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp == null) { + lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + } + int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, lp.width); + int lpHeight = lp.height; + int heightSpec; + if (lpHeight > 0) { + heightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + return new int[]{view.getMeasuredWidth(), view.getMeasuredHeight()}; + } + + /** + * 获取测量视图宽度 + * + * @param view 视图 + * @return 视图宽度 + */ + public static int getMeasuredWidth(View view) { + return measureView(view)[0]; + } + + /** + * 获取测量视图高度 + * + * @param view 视图 + * @return 视图高度 + */ + public static int getMeasuredHeight(View view) { + return measureView(view)[1]; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/UpLoadUtils.java b/widget/src/main/java/com/hzecool/widget/utils/UpLoadUtils.java new file mode 100644 index 0000000..3e733ac --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/UpLoadUtils.java @@ -0,0 +1,101 @@ +package com.hzecool.widget.utils; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +/** + * 文件上传util + * Created by song on 2017/5/9. + */ + +public class UpLoadUtils { + private static final String TAG = "uploadFile"; + private static final int TIME_OUT = 10*1000; //超时时间 + private static final String CHARSET = "utf-8"; //设置编码 + /** + * android上传文件到服务器 + * @param file 需要上传的文件 + * @param RequestURL 请求的rul + * @return 返回响应的内容 + */ + public static String uploadFile(File file, String RequestURL){ + String result = null; + String BOUNDARY = UUID.randomUUID().toString(); //边界标识 随机生成 + String PREFIX = "--" , LINE_END = "\r\n"; + String CONTENT_TYPE = "multipart/form-data"; //内容类型 + + try { + URL url = new URL(RequestURL); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setReadTimeout(TIME_OUT); + conn.setConnectTimeout(TIME_OUT); + conn.setDoInput(true); //允许输入流 + conn.setDoOutput(true); //允许输出流 + conn.setUseCaches(false); //不允许使用缓存 + conn.setRequestMethod("POST"); //请求方式 + conn.setRequestProperty("Charset", CHARSET); //设置编码 + conn.setRequestProperty("connection", "keep-alive"); + conn.setRequestProperty("Content-Type", CONTENT_TYPE + ";boundary=" + BOUNDARY); + conn.connect(); + + if(file!=null){ + /** + * 当文件不为空,把文件包装并且上传 + */ + DataOutputStream dos = new DataOutputStream( conn.getOutputStream()); + StringBuffer sb = new StringBuffer(); + sb.append(PREFIX); + sb.append(BOUNDARY); + sb.append(LINE_END); + /** + * 这里重点注意: + * name里面的值为服务器端需要key 只有这个key 才可以得到对应的文件 + * filename是文件的名字,包含后缀名的 比如:abc.png + */ + + sb.append("Content-Disposition: form-data; name=\"img\"; filename=\""+file.getName()+"\""+LINE_END); + sb.append("Content-Type: application/octet-stream; charset="+CHARSET+LINE_END); + sb.append(LINE_END); + dos.write(sb.toString().getBytes()); + InputStream is = new FileInputStream(file); + byte[] bytes = new byte[1024]; + int len = 0; + while((len=is.read(bytes))!=-1){ + dos.write(bytes, 0, len); + } + is.close(); + dos.write(LINE_END.getBytes()); + byte[] end_data = (PREFIX+BOUNDARY+PREFIX+LINE_END).getBytes(); + dos.write(end_data); + dos.flush(); + /** + * 获取响应码 200=成功 + * 当响应成功,获取响应的流 + */ + int res = conn.getResponseCode(); + if(res==200){ + InputStream input = conn.getInputStream(); + StringBuffer sb1= new StringBuffer(); + int ss ; + while((ss=input.read())!=-1){ + sb1.append((char)ss); + } + result = sb1.toString(); + System.out.println(result); + } + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return result; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/AppBarStateChangeListener.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/AppBarStateChangeListener.java new file mode 100644 index 0000000..f8e48cd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/AppBarStateChangeListener.java @@ -0,0 +1,40 @@ +package com.hzecool.widget.xRecyclerView; + +import android.support.design.widget.AppBarLayout; + +/** + * Created by jianghejie on 16/6/19. + */ + +public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener { + + public enum State { + EXPANDED, + COLLAPSED, + IDLE + } + + private State mCurrentState = State.IDLE; + + @Override + public final void onOffsetChanged(AppBarLayout appBarLayout, int i) { + if (i == 0) { + if (mCurrentState != State.EXPANDED) { + onStateChanged(appBarLayout, State.EXPANDED); + } + mCurrentState = State.EXPANDED; + } else if (Math.abs(i) >= appBarLayout.getTotalScrollRange()) { + if (mCurrentState != State.COLLAPSED) { + onStateChanged(appBarLayout, State.COLLAPSED); + } + mCurrentState = State.COLLAPSED; + } else { + if (mCurrentState != State.IDLE) { + onStateChanged(appBarLayout, State.IDLE); + } + mCurrentState = State.IDLE; + } + } + public abstract void onStateChanged(AppBarLayout appBarLayout, State state); +} + diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/ArrowRefreshHeader.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ArrowRefreshHeader.java new file mode 100644 index 0000000..2941ef6 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ArrowRefreshHeader.java @@ -0,0 +1,268 @@ +package com.hzecool.widget.xRecyclerView; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.xRecyclerView.progressindicator.AVLoadingIndicatorView; + +import java.util.Date; + +public class ArrowRefreshHeader extends LinearLayout implements BaseRefreshHeader { + + private LinearLayout mContainer; + private ImageView mArrowImageView; + private SimpleViewSwitcher mProgressBar; + private TextView mStatusTextView; + private int mState = STATE_NORMAL; + + private TextView mHeaderTimeView; + + private Animation mRotateUpAnim; + private Animation mRotateDownAnim; + + private static final int ROTATE_ANIM_DURATION = 180; + + public int mMeasuredHeight; + + public ArrowRefreshHeader(Context context) { + super(context); + initView(); + } + + /** + * @param context + * @param attrs + */ + public ArrowRefreshHeader(Context context, AttributeSet attrs) { + super(context, attrs); + initView(); + } + + private void initView() { + // 初始情况,设置下拉刷新view高度为0 + mContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate( + R.layout.listview_header, null); + LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + lp.setMargins(0, 0, 0, 0); + this.setLayoutParams(lp); + this.setPadding(0, 0, 0, 0); + + addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0)); + setGravity(Gravity.BOTTOM); + + mArrowImageView = (ImageView)findViewById(R.id.listview_header_arrow); + mStatusTextView = (TextView)findViewById(R.id.refresh_status_textview); + + //init the progress view + mProgressBar = (SimpleViewSwitcher)findViewById(R.id.listview_header_progressbar); + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(ProgressStyle.BallSpinFadeLoader); + mProgressBar.setView(progressView); + + + mRotateUpAnim = new RotateAnimation(0.0f, -180.0f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION); + mRotateUpAnim.setFillAfter(true); + mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION); + mRotateDownAnim.setFillAfter(true); + + mHeaderTimeView = (TextView)findViewById(R.id.last_refresh_time); + measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); + mMeasuredHeight = getMeasuredHeight(); + } + + public void setProgressStyle(int style) { + if(style == ProgressStyle.SysProgress){ + mProgressBar.setView(new ProgressBar(getContext(), null, android.R.attr.progressBarStyle)); + }else{ + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(this.getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(style); + mProgressBar.setView(progressView); + } + } + + public void setArrowImageView(int resid){ + mArrowImageView.setImageResource(resid); + } + + public void setState(int state) { + if (state == mState) return ; + + if (state == STATE_REFRESHING) { // 显示进度 + mArrowImageView.clearAnimation(); + mArrowImageView.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.VISIBLE); + smoothScrollTo(mMeasuredHeight); + } else if(state == STATE_DONE) { + mArrowImageView.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.INVISIBLE); + } else { // 显示箭头图片 + mArrowImageView.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.INVISIBLE); + } + + switch(state){ + case STATE_NORMAL: + if (mState == STATE_RELEASE_TO_REFRESH) { + mArrowImageView.startAnimation(mRotateDownAnim); + } + if (mState == STATE_REFRESHING) { + mArrowImageView.clearAnimation(); + } + mStatusTextView.setText(R.string.listview_header_hint_normal); + break; + case STATE_RELEASE_TO_REFRESH: + if (mState != STATE_RELEASE_TO_REFRESH) { + mArrowImageView.clearAnimation(); + mArrowImageView.startAnimation(mRotateUpAnim); + mStatusTextView.setText(R.string.listview_header_hint_release); + } + break; + case STATE_REFRESHING: + mStatusTextView.setText(R.string.refreshing); + break; + case STATE_DONE: + mStatusTextView.setText(R.string.refresh_done); + break; + default: + } + + mState = state; + } + + public int getState() { + return mState; + } + + @Override + public void refreshComplete(){ + mHeaderTimeView.setText(friendlyTime(new Date())); + setState(STATE_DONE); + new Handler().postDelayed(new Runnable(){ + public void run() { + reset(); + } + }, 200); + } + + public void setVisibleHeight(int height) { + if (height < 0) height = 0; + LayoutParams lp = (LayoutParams) mContainer .getLayoutParams(); + lp.height = height; + mContainer.setLayoutParams(lp); + } + + public int getVisibleHeight() { + LayoutParams lp = (LayoutParams) mContainer.getLayoutParams(); + return lp.height; + } + + @Override + public void onMove(float delta) { + if(getVisibleHeight() > 0 || delta > 0) { + setVisibleHeight((int) delta + getVisibleHeight()); + if (mState <= STATE_RELEASE_TO_REFRESH) { // 未处于刷新状态,更新箭头 + if (getVisibleHeight() > mMeasuredHeight) { + setState(STATE_RELEASE_TO_REFRESH); + }else { + setState(STATE_NORMAL); + } + } + } + } + + @Override + public boolean releaseAction() { + boolean isOnRefresh = false; + int height = getVisibleHeight(); + if (height == 0) // not visible. + isOnRefresh = false; + + if(getVisibleHeight() > mMeasuredHeight && mState < STATE_REFRESHING){ + setState(STATE_REFRESHING); + isOnRefresh = true; + } + // refreshing and header isn't shown fully. do nothing. + if (mState == STATE_REFRESHING && height <= mMeasuredHeight) { + //return; + } + if (mState != STATE_REFRESHING) { + smoothScrollTo(0); + } + + if (mState == STATE_REFRESHING) { + int destHeight = mMeasuredHeight; + smoothScrollTo(destHeight); + } + + return isOnRefresh; + } + + public void reset() { + smoothScrollTo(0); + new Handler().postDelayed(new Runnable() { + public void run() { + setState(STATE_NORMAL); + } + }, 500); + } + + private void smoothScrollTo(int destHeight) { + ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight); + animator.setDuration(300).start(); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) + { + setVisibleHeight((int) animation.getAnimatedValue()); + } + }); + animator.start(); + } + + public static String friendlyTime(Date time) { + //获取time距离当前的秒数 + int ct = (int)((System.currentTimeMillis() - time.getTime())/1000); + + if(ct == 0) { + return "刚刚"; + } + + if(ct > 0 && ct < 60) { + return ct + "秒前"; + } + + if(ct >= 60 && ct < 3600) { + return Math.max(ct / 60,1) + "分钟前"; + } + if(ct >= 3600 && ct < 86400) + return ct / 3600 + "小时前"; + if(ct >= 86400 && ct < 2592000){ //86400 * 30 + int day = ct / 86400 ; + return day + "天前"; + } + if(ct >= 2592000 && ct < 31104000) { //86400 * 30 + return ct / 2592000 + "月前"; + } + return ct / 31104000 + "年前"; + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/BaseRefreshHeader.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/BaseRefreshHeader.java new file mode 100644 index 0000000..eafef3c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/BaseRefreshHeader.java @@ -0,0 +1,19 @@ +package com.hzecool.widget.xRecyclerView; + +/** + * Created by jianghejie on 15/11/22. + */ +interface BaseRefreshHeader { + + int STATE_NORMAL = 0; + int STATE_RELEASE_TO_REFRESH = 1; + int STATE_REFRESHING = 2; + int STATE_DONE = 3; + + void onMove(float delta); + + boolean releaseAction(); + + void refreshComplete(); + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/ItemTouchHelperAdapter.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ItemTouchHelperAdapter.java new file mode 100644 index 0000000..e79cf35 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ItemTouchHelperAdapter.java @@ -0,0 +1,39 @@ +package com.hzecool.widget.xRecyclerView; + +import android.support.v7.widget.RecyclerView; + +/** + * Created by jianghejie on 16/6/20. + */ + +public interface ItemTouchHelperAdapter { + + /** + * Called when an item has been dragged far enough to trigger a move. This is called every time + * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after + * adjusting the underlying data to reflect this move. + * + * @param fromPosition The start position of the moved item. + * @param toPosition Then resolved position of the moved item. + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + void onItemMove(int fromPosition, int toPosition); + + + /** + * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after + * adjusting the underlying data to reflect this removal. + * + * @param position The position of the item dismissed. + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + void onItemDismiss(int position); +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/JellyView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/JellyView.java new file mode 100644 index 0000000..799a507 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/JellyView.java @@ -0,0 +1,108 @@ +package com.hzecool.widget.xRecyclerView; + +/** + * Created by jianghejie on 15/11/22. + */ + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + + +public class JellyView extends View implements BaseRefreshHeader{ + Path path; + + Paint paint; + + private int minimumHeight = 0; + + private int jellyHeight =0; + + public JellyView(Context context) { + super(context); + init(); + } + + public JellyView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public JellyView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @SuppressWarnings("unused") + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public JellyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + if (isInEditMode()) { + return; + } + path = new Path(); + paint = new Paint(); + paint.setColor(getContext().getResources().getColor(android.R.color.holo_blue_bright)); + paint.setAntiAlias(true); + } + + public void setJellyColor(int jellyColor) { + paint.setColor(jellyColor); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + path.reset(); + path.lineTo(0, minimumHeight); + path.quadTo(getMeasuredWidth() / 2, minimumHeight + jellyHeight, getMeasuredWidth(), minimumHeight); + path.lineTo(getMeasuredWidth(), 0); + canvas.drawPath(path, paint); + } + + @Override + public void setMinimumHeight(int minimumHeight) { + this.minimumHeight = minimumHeight; + } + + public void setJellyHeight(int ribbonHeight) { + this.jellyHeight = ribbonHeight; + } + + @Override + public int getMinimumHeight() { + return minimumHeight; + } + + public int getJellyHeight() { + return jellyHeight; + } + + + @Override + public void refreshComplete(){ + + } + + @Override + public void onMove(float delta) { + jellyHeight = jellyHeight + (int)delta; + Log.i("jellyHeight", "delta = " + delta); + this.invalidate(); + } + + @Override + public boolean releaseAction() { + return false; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/LoadingMoreFooter.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/LoadingMoreFooter.java new file mode 100644 index 0000000..e9df633 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/LoadingMoreFooter.java @@ -0,0 +1,110 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.xRecyclerView.progressindicator.AVLoadingIndicatorView; + + +public class LoadingMoreFooter extends LinearLayout { + + private SimpleViewSwitcher progressCon; + public final static int STATE_LOADING = 0; + public final static int STATE_COMPLETE = 1; + public final static int STATE_NOMORE = 2; + private TextView mText; + private String loadingHint; + private String noMoreHint; + private String loadingDoneHint; + + public LoadingMoreFooter(Context context) { + super(context); + initView(); + } + + /** + * @param context + * @param attrs + */ + public LoadingMoreFooter(Context context, AttributeSet attrs) { + super(context, attrs); + initView(); + } + + public void setLoadingHint(String hint) { + loadingHint = hint; + } + + public void setNoMoreHint(String hint) { + noMoreHint = hint; + } + + public void setLoadingDoneHint(String hint) { + loadingDoneHint = hint; + } + + public void initView() { + setGravity(Gravity.CENTER); + setLayoutParams(new RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + progressCon = new SimpleViewSwitcher(getContext()); + progressCon.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(this.getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(ProgressStyle.BallSpinFadeLoader); + progressCon.setView(progressView); + + addView(progressCon); + mText = new TextView(getContext()); + mText.setText("正在加载..."); + loadingHint = (String) getContext().getText(R.string.listview_loading); + noMoreHint = (String) getContext().getText(R.string.nomore_loading); + loadingDoneHint = (String) getContext().getText(R.string.loading_done); + LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams + .WRAP_CONTENT); + layoutParams.setMargins((int) getResources().getDimension(R.dimen.textandiconmargin), 0, 0, 0); + + mText.setLayoutParams(layoutParams); + addView(mText); + } + + public void setProgressStyle(int style) { + if (style == ProgressStyle.SysProgress) { + progressCon.setView(new ProgressBar(getContext(), null, android.R.attr.progressBarStyle)); + } else { + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(this.getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(style); + progressCon.setView(progressView); + } + } + + public void setState(int state) { + switch (state) { + case STATE_LOADING: + progressCon.setVisibility(View.VISIBLE); + mText.setText(loadingHint); + this.setVisibility(View.VISIBLE); + break; + case STATE_COMPLETE: + mText.setText(loadingDoneHint); + this.setVisibility(View.GONE); + break; + case STATE_NOMORE: + mText.setText(noMoreHint); + progressCon.setVisibility(View.GONE); + this.setVisibility(View.VISIBLE); + break; + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/ProgressStyle.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ProgressStyle.java new file mode 100644 index 0000000..3de062c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ProgressStyle.java @@ -0,0 +1,36 @@ +package com.hzecool.widget.xRecyclerView; + +/** + * Created by jianghejie on 15/11/23. + */ +public class ProgressStyle { + public static final int SysProgress=-1; + public static final int BallPulse=0; + public static final int BallGridPulse=1; + public static final int BallClipRotate=2; + public static final int BallClipRotatePulse=3; + public static final int SquareSpin=4; + public static final int BallClipRotateMultiple=5; + public static final int BallPulseRise=6; + public static final int BallRotate=7; + public static final int CubeTransition=8; + public static final int BallZigZag=9; + public static final int BallZigZagDeflect=10; + public static final int BallTrianglePath=11; + public static final int BallScale=12; + public static final int LineScale=13; + public static final int LineScaleParty=14; + public static final int BallScaleMultiple=15; + public static final int BallPulseSync=16; + public static final int BallBeat=17; + public static final int LineScalePulseOut=18; + public static final int LineScalePulseOutRapid=19; + public static final int BallScaleRipple=20; + public static final int BallScaleRippleMultiple=21; + public static final int BallSpinFadeLoader=22; + public static final int LineSpinFadeLoader=23; + public static final int TriangleSkewSpin=24; + public static final int Pacman=25; + public static final int BallGridBeat=26; + public static final int SemiCircleSpin=27; +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/RecyclerViewEmptyView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/RecyclerViewEmptyView.java new file mode 100644 index 0000000..642be62 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/RecyclerViewEmptyView.java @@ -0,0 +1,92 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.support.annotation.ColorInt; +import android.support.annotation.DimenRes; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.utils.SizeUtils; + + +/** + * Created by tutu on 2017/1/12. + */ + +public class RecyclerViewEmptyView extends RelativeLayout { + private ImageView iv; + private TextView tv; + /** + * textview 距离 imageview 的距离 + */ + private int tvTopMargin = 0; + + private LayoutParams params; + + + public RecyclerViewEmptyView(Context context) { + this(context, null); + initView(); + } + + public RecyclerViewEmptyView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RecyclerViewEmptyView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + + private void initView() { + params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + iv = new ImageView(getContext()); + tv = new TextView(getContext()); + + params.addRule(CENTER_IN_PARENT, TRUE); + iv.setLayoutParams(params); + iv.setImageResource(R.mipmap.empty); + + params.addRule(CENTER_HORIZONTAL, TRUE); + params.addRule(BELOW, iv.getId()); + params.topMargin = SizeUtils.dp2px(getContext(),10); + tv.setLayoutParams(params); + tv.setText(R.string.empty_data); + + + this.addView(iv); + this.addView(tv); + } + + + public RecyclerViewEmptyView setText(String text) { + tv.setText(text); + return this; + } + + public RecyclerViewEmptyView setTextColor(@ColorInt int color) { + tv.setTextColor(color); + return this; + } + + public RecyclerViewEmptyView setTextSize(@DimenRes int size) { + tv.setTextSize(size); + return this; + } + + public RecyclerViewEmptyView setTvIvMargin(int dp) { + params.topMargin = SizeUtils.dp2px(getContext(),dp); + tv.setLayoutParams(params); + return this; + } + + public RecyclerViewEmptyView setImageSrc(int resId) { + iv.setImageResource(resId); + return this; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleItemTouchHelperCallback.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleItemTouchHelperCallback.java new file mode 100644 index 0000000..5a2c2ef --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleItemTouchHelperCallback.java @@ -0,0 +1,87 @@ +package com.hzecool.widget.xRecyclerView; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.View; + +/** + * Created by jianghejie on 16/6/20. + */ + +public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { + + public static final float ALPHA_FULL = 1.0f; + + private final ItemTouchHelperAdapter mAdapter; + private XRecyclerView mXrecyclerView; + + public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter, XRecyclerView recyclerView) { + mAdapter = adapter; + this.mXrecyclerView = recyclerView; + } + + @Override + public boolean isLongPressDragEnabled() { + return true; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + // Enable drag and swipe in both directions + final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; + return makeMovementFlags(dragFlags, swipeFlags); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType()) { + return false; + } + // Notify the adapter of the move + mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition()); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { + // Notify the adapter of the dismissal + mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); + } + + @Override + public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + + // Fade out the view as it is swiped out of the parent's bounds + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + View itemView = viewHolder.itemView; + final float alpha = ALPHA_FULL - Math.abs(dX) / (float) itemView.getWidth(); + itemView.setAlpha(alpha); + } + } + + @Override + public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { + // Let the view holder know that this item is being moved or dragged + viewHolder.itemView.setBackgroundColor(Color.LTGRAY); + } + + super.onSelectedChanged(viewHolder, actionState); + } + + @Override + public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + viewHolder.itemView.setAlpha(ALPHA_FULL); + viewHolder.itemView.setBackgroundColor(0); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleViewSwitcher.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleViewSwitcher.java new file mode 100644 index 0000000..ec8e8ce --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleViewSwitcher.java @@ -0,0 +1,60 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * Created by jianghejie on 15/11/22. + */ +public class SimpleViewSwitcher extends ViewGroup { + + public SimpleViewSwitcher(Context context) { + super(context); + } + + public SimpleViewSwitcher(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SimpleViewSwitcher(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int childCount = this.getChildCount(); + int maxHeight = 0; + int maxWidth = 0; + for (int i = 0; i < childCount; i++) { + View child = this.getChildAt(i); + this.measureChild(child, widthMeasureSpec, heightMeasureSpec); + int cw = child.getMeasuredWidth(); + // int ch = child.getMeasuredHeight(); + maxWidth = child.getMeasuredWidth(); + maxHeight = child.getMeasuredHeight(); + } + setMeasuredDimension(maxWidth, maxHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + child.layout(0, 0, r - l, b - t); + + } + } + } + + public void setView(View view) { + if (this.getChildCount() != 0){ + this.removeViewAt(0); + } + this.addView(view,0); + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/XRecyclerView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/XRecyclerView.java new file mode 100644 index 0000000..721e1c5 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/XRecyclerView.java @@ -0,0 +1,727 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CoordinatorLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import java.util.ArrayList; +import java.util.List; + +public class XRecyclerView extends RecyclerView { + public static String REFRESH = "refresh"; + public static String ADDMORE = "addmore"; + + + private boolean isLoadingData = false; + private boolean isNoMore = false; + private int mRefreshProgressStyle = ProgressStyle.SysProgress; + private int mLoadingMoreProgressStyle = ProgressStyle.SysProgress; + private ArrayList mHeaderViews = new ArrayList<>(); + private WrapAdapter mWrapAdapter; + private float mLastY = -1; + private static final float DRAG_RATE = 3; + private LoadingListener mLoadingListener; + private ArrowRefreshHeader mRefreshHeader; + private boolean pullRefreshEnabled = true; + private boolean loadingMoreEnabled = true; + //下面的ItemViewType是保留值(ReservedItemViewType),如果用户的adapter与它们重复将会强制抛出异常。不过为了简化,我们检测到重复时对用户的提示是ItemViewType必须小于10000 + private static final int TYPE_REFRESH_HEADER = 10000;//设置一个很大的数字,尽可能避免和用户的adapter冲突 + private static final int TYPE_FOOTER = 10001; + private static final int HEADER_INIT_INDEX = 10002; + private static List sHeaderTypes = new ArrayList<>();//每个header必须有不同的type,不然滚动的时候顺序会变化 + private int mPageCount = 0; + //adapter没有数据的时候显示,类似于listView的emptyView + private View mEmptyView; + private View mFootView; + private final AdapterDataObserver mDataObserver = new DataObserver(); + private AppBarStateChangeListener.State appbarState = AppBarStateChangeListener.State.EXPANDED; + + public XRecyclerView(Context context) { + this(context, null); + } + + public XRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public XRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + if (pullRefreshEnabled) { + mRefreshHeader = new ArrowRefreshHeader(getContext()); + mRefreshHeader.setProgressStyle(mRefreshProgressStyle); + } + LoadingMoreFooter footView = new LoadingMoreFooter(getContext()); + footView.setProgressStyle(mLoadingMoreProgressStyle); + mFootView = footView; + mFootView.setVisibility(GONE); + } + + public void setFootViewText(String loading, String noMore) { + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setLoadingHint(loading); + ((LoadingMoreFooter) mFootView).setNoMoreHint(noMore); + } + } + + public void addHeaderView(View view) { + sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size()); + mHeaderViews.add(view); + if (mWrapAdapter != null) { + mWrapAdapter.notifyDataSetChanged(); + } + } + + //根据header的ViewType判断是哪个header + private View getHeaderViewByType(int itemType) { + if (!isHeaderType(itemType)) { + return null; + } + return mHeaderViews.get(itemType - HEADER_INIT_INDEX); + } + + //判断一个type是否为HeaderType + private boolean isHeaderType(int itemViewType) { + return mHeaderViews.size() > 0 && sHeaderTypes.contains(itemViewType); + } + + //判断是否是XRecyclerView保留的itemViewType + private boolean isReservedItemViewType(int itemViewType) { + if (itemViewType == TYPE_REFRESH_HEADER || itemViewType == TYPE_FOOTER || sHeaderTypes.contains + (itemViewType)) { + return true; + } else { + return false; + } + } + + public void setFootView(final View view) { + mFootView = view; + } + + public void loadMoreComplete() { + isLoadingData = false; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_COMPLETE); + } else { + mFootView.setVisibility(GONE); + } + } + + public void setNoMore(boolean noMore) { + isLoadingData = false; + isNoMore = noMore; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(isNoMore ? LoadingMoreFooter.STATE_NOMORE : LoadingMoreFooter + .STATE_COMPLETE); + } else { + mFootView.setVisibility(GONE); + } + } + + public void refresh() { + if (pullRefreshEnabled && mLoadingListener != null) { + mRefreshHeader.setState(ArrowRefreshHeader.STATE_REFRESHING); + mLoadingListener.onRefresh(); + } + } + + public void reset() { + setNoMore(false); + loadMoreComplete(); + refreshComplete(); + } + + public void refreshComplete() { + mRefreshHeader.refreshComplete(); + setNoMore(false); + } + + public void setRefreshHeader(ArrowRefreshHeader refreshHeader) { + mRefreshHeader = refreshHeader; + } + + public void setPullRefreshEnabled(boolean enabled) { + pullRefreshEnabled = enabled; + } + + public void setLoadingMoreEnabled(boolean enabled) { + loadingMoreEnabled = enabled; + if (!enabled) { + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_COMPLETE); + } + } + } + + public void setRefreshProgressStyle(int style) { + mRefreshProgressStyle = style; + if (mRefreshHeader != null) { + mRefreshHeader.setProgressStyle(style); + } + } + + public void setLoadingMoreProgressStyle(int style) { + mLoadingMoreProgressStyle = style; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setProgressStyle(style); + } + } + + public void setArrowImageView(int resId) { + if (mRefreshHeader != null) { + mRefreshHeader.setArrowImageView(resId); + } + } + + public void setEmptyView(View emptyView) { + this.mEmptyView = emptyView; + mDataObserver.onChanged(); + } + + public View getEmptyView() { + return mEmptyView; + } + + @Override + public void setAdapter(Adapter adapter) { + mWrapAdapter = new WrapAdapter(adapter); + super.setAdapter(mWrapAdapter); + adapter.registerAdapterDataObserver(mDataObserver); + mDataObserver.onChanged(); + } + + //避免用户自己调用getAdapter() 引起的ClassCastException + @Override + public Adapter getAdapter() { + if (mWrapAdapter != null) + return mWrapAdapter.getOriginalAdapter(); + else + return null; + } + + @Override + public void setLayoutManager(LayoutManager layout) { + super.setLayoutManager(layout); + if (mWrapAdapter != null) { + if (layout instanceof GridLayoutManager) { + final GridLayoutManager gridManager = ((GridLayoutManager) layout); + gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return (mWrapAdapter.isHeader(position) || mWrapAdapter.isFooter(position) || mWrapAdapter + .isRefreshHeader(position)) + ? gridManager.getSpanCount() : 1; + } + }); + + } + } + } + + @Override + public void onScrollStateChanged(int state) { + super.onScrollStateChanged(state); + if (state == RecyclerView.SCROLL_STATE_IDLE && mLoadingListener != null && !isLoadingData && + loadingMoreEnabled) { + LayoutManager layoutManager = getLayoutManager(); + int lastVisibleItemPosition; + if (layoutManager instanceof GridLayoutManager) { + lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; + ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into); + lastVisibleItemPosition = findMax(into); + } else { + lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + } + if (layoutManager.getChildCount() > 0 + && lastVisibleItemPosition >= layoutManager.getItemCount() - 1 && layoutManager.getItemCount() > + layoutManager.getChildCount() && !isNoMore && mRefreshHeader.getState() < ArrowRefreshHeader + .STATE_REFRESHING) { + isLoadingData = true; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_LOADING); + } else { + mFootView.setVisibility(View.VISIBLE); + } + mLoadingListener.onLoadMore(); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mLastY == -1) { + mLastY = ev.getRawY(); + } + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + mLastY = ev.getRawY(); + break; + case MotionEvent.ACTION_MOVE: + final float deltaY = ev.getRawY() - mLastY; + mLastY = ev.getRawY(); + if (isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) { + mRefreshHeader.onMove(deltaY / DRAG_RATE); + if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader + .STATE_REFRESHING) { + return false; + } + } + break; + default: + mLastY = -1; // reset + if (isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) { + if (mRefreshHeader.releaseAction()) { + if (mLoadingListener != null) { + mLoadingListener.onRefresh(); + } + } + } + break; + } + return super.onTouchEvent(ev); + } + + private int findMax(int[] lastPositions) { + int max = lastPositions[0]; + for (int value : lastPositions) { + if (value > max) { + max = value; + } + } + return max; + } + + private boolean isOnTop() { + if (mRefreshHeader.getParent() != null) { + return true; + } else { + return false; + } + } + + private class DataObserver extends AdapterDataObserver { + @Override + public void onChanged() { + if (mWrapAdapter != null) { + mWrapAdapter.notifyDataSetChanged(); + } + if (mWrapAdapter != null && mEmptyView != null) { + int emptyCount = 1 + mWrapAdapter.getHeadersCount(); + if (loadingMoreEnabled) { + emptyCount++; + } + if (mWrapAdapter.getItemCount() == emptyCount) { + mEmptyView.setVisibility(View.VISIBLE); + XRecyclerView.this.setVisibility(GONE); + } else { + + mEmptyView.setVisibility(GONE); + XRecyclerView.this.setVisibility(View.VISIBLE); + } + } + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeInserted(positionStart, itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeChanged(positionStart, itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + mWrapAdapter.notifyItemRangeChanged(positionStart, itemCount, payload); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeRemoved(positionStart, itemCount); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + mWrapAdapter.notifyItemMoved(fromPosition, toPosition); + } + } + + ; + + private class WrapAdapter extends Adapter { + + private Adapter adapter; + + public WrapAdapter(Adapter adapter) { + this.adapter = adapter; + } + + public Adapter getOriginalAdapter() { + return this.adapter; + } + + public boolean isHeader(int position) { + return position >= 1 && position < mHeaderViews.size() + 1; + } + + public boolean isFooter(int position) { + if (loadingMoreEnabled) { + return position == getItemCount() - 1; + } else { + return false; + } + } + + public boolean isRefreshHeader(int position) { + return position == 0; + } + + public int getHeadersCount() { + return mHeaderViews.size(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == TYPE_REFRESH_HEADER) { + return new SimpleViewHolder(mRefreshHeader); + } else if (isHeaderType(viewType)) { + return new SimpleViewHolder(getHeaderViewByType(viewType)); + } else if (viewType == TYPE_FOOTER) { + return new SimpleViewHolder(mFootView); + } + return adapter.onCreateViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + if (isHeader(position) || isRefreshHeader(position)) { + return; + } + int adjPosition = position - (getHeadersCount() + 1); + int adapterCount; + if (adapter != null) { + adapterCount = adapter.getItemCount(); + if (adjPosition < adapterCount) { + adapter.onBindViewHolder(holder, adjPosition); + } + } + } + + // some times we need to override this + @Override + public void onBindViewHolder(ViewHolder holder, int position, List payloads) { + if (isHeader(position) || isRefreshHeader(position)) { + return; + } + int adjPosition = position - (getHeadersCount() + 1); + int adapterCount; + if (adapter != null) { + adapterCount = adapter.getItemCount(); + if (adjPosition < adapterCount) { + if (payloads.isEmpty()) { + adapter.onBindViewHolder(holder, adjPosition); + } else { + adapter.onBindViewHolder(holder, adjPosition, payloads); + } + } + } + } + + @Override + public int getItemCount() { + if (loadingMoreEnabled) { + if (adapter != null) { + return getHeadersCount() + adapter.getItemCount() + 2; + } else { + return getHeadersCount() + 2; + } + } else { + if (adapter != null) { + return getHeadersCount() + adapter.getItemCount() + 1; + } else { + return getHeadersCount() + 1; + } + } + } + + @Override + public int getItemViewType(int position) { + int adjPosition = position - (getHeadersCount() + 1); + if (isRefreshHeader(position)) { + return TYPE_REFRESH_HEADER; + } + if (isHeader(position)) { + position = position - 1; + return sHeaderTypes.get(position); + } + if (isFooter(position)) { + return TYPE_FOOTER; + } + int adapterCount; + if (adapter != null) { + adapterCount = adapter.getItemCount(); + if (adjPosition < adapterCount) { + int type = adapter.getItemViewType(adjPosition); + if (isReservedItemViewType(type)) { + throw new IllegalStateException("XRecyclerView require itemViewType in adapter should be less " + + "than 10000 "); + } + return type; + } + } + return 0; + } + + @Override + public long getItemId(int position) { + if (adapter != null && position >= getHeadersCount() + 1) { + int adjPosition = position - (getHeadersCount() + 1); + if (adjPosition < adapter.getItemCount()) { + return adapter.getItemId(adjPosition); + } + } + return -1; + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + LayoutManager manager = recyclerView.getLayoutManager(); + if (manager instanceof GridLayoutManager) { + final GridLayoutManager gridManager = ((GridLayoutManager) manager); + gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return (isHeader(position) || isFooter(position) || isRefreshHeader(position)) + ? gridManager.getSpanCount() : 1; + } + }); + } + adapter.onAttachedToRecyclerView(recyclerView); + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + adapter.onDetachedFromRecyclerView(recyclerView); + } + + @Override + public void onViewAttachedToWindow(ViewHolder holder) { + super.onViewAttachedToWindow(holder); + ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + if (lp != null + && lp instanceof StaggeredGridLayoutManager.LayoutParams + && (isHeader(holder.getLayoutPosition()) || isRefreshHeader(holder.getLayoutPosition()) || isFooter + (holder.getLayoutPosition()))) { + StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; + p.setFullSpan(true); + } + adapter.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(ViewHolder holder) { + adapter.onViewDetachedFromWindow(holder); + } + + @Override + public void onViewRecycled(ViewHolder holder) { + adapter.onViewRecycled(holder); + } + + @Override + public boolean onFailedToRecycleView(ViewHolder holder) { + return adapter.onFailedToRecycleView(holder); + } + + @Override + public void unregisterAdapterDataObserver(AdapterDataObserver observer) { + adapter.unregisterAdapterDataObserver(observer); + } + + @Override + public void registerAdapterDataObserver(AdapterDataObserver observer) { + adapter.registerAdapterDataObserver(observer); + } + + private class SimpleViewHolder extends ViewHolder { + public SimpleViewHolder(View itemView) { + super(itemView); + } + } + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + public interface LoadingListener { + + void onRefresh(); + + void onLoadMore(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + //解决和CollapsingToolbarLayout冲突的问题 + AppBarLayout appBarLayout = null; + ViewParent p = getParent(); + while (p != null) { + if (p instanceof CoordinatorLayout) { + break; + } + p = p.getParent(); + } + if (p instanceof CoordinatorLayout) { + CoordinatorLayout coordinatorLayout = (CoordinatorLayout) p; + final int childCount = coordinatorLayout.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = coordinatorLayout.getChildAt(i); + if (child instanceof AppBarLayout) { + appBarLayout = (AppBarLayout) child; + break; + } + } + if (appBarLayout != null) { + appBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() { + @Override + public void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state) { + appbarState = state; + } + }); + } + } + } + + public class DividerItemDecoration extends ItemDecoration { + + private Drawable mDivider; + private int mOrientation; + + /** + * Sole constructor. Takes in a {@link Drawable} to be used as the interior + * divider. + * + * @param divider A divider {@code Drawable} to be drawn on the RecyclerView + */ + public DividerItemDecoration(Drawable divider) { + mDivider = divider; + } + + /** + * Draws horizontal or vertical dividers onto the parent RecyclerView. + * + * @param canvas The {@link Canvas} onto which dividers will be drawn + * @param parent The RecyclerView onto which dividers are being added + * @param state The current RecyclerView.State of the RecyclerView + */ + @Override + public void onDraw(Canvas canvas, RecyclerView parent, State state) { + if (mOrientation == LinearLayoutManager.HORIZONTAL) { + drawHorizontalDividers(canvas, parent); + } else if (mOrientation == LinearLayoutManager.VERTICAL) { + drawVerticalDividers(canvas, parent); + } + } + + /** + * Determines the size and location of offsets between items in the parent + * RecyclerView. + * + * @param outRect The {@link Rect} of offsets to be added around the child + * view + * @param view The child view to be decorated with an offset + * @param parent The RecyclerView onto which dividers are being added + * @param state The current RecyclerView.State of the RecyclerView + */ + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) { + super.getItemOffsets(outRect, view, parent, state); + + if (parent.getChildAdapterPosition(view) <= mWrapAdapter.getHeadersCount() + 1) { + return; + } + mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation(); + if (mOrientation == LinearLayoutManager.HORIZONTAL) { + outRect.left = mDivider.getIntrinsicWidth(); + } else if (mOrientation == LinearLayoutManager.VERTICAL) { + outRect.top = mDivider.getIntrinsicHeight(); + } + } + + /** + * Adds dividers to a RecyclerView with a LinearLayoutManager or its + * subclass oriented horizontally. + * + * @param canvas The {@link Canvas} onto which horizontal dividers will be + * drawn + * @param parent The RecyclerView onto which horizontal dividers are being + * added + */ + private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) { + int parentTop = parent.getPaddingTop(); + int parentBottom = parent.getHeight() - parent.getPaddingBottom(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + + LayoutParams params = (LayoutParams) child.getLayoutParams(); + + int parentLeft = child.getRight() + params.rightMargin; + int parentRight = parentLeft + mDivider.getIntrinsicWidth(); + + mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom); + mDivider.draw(canvas); + } + } + + /** + * Adds dividers to a RecyclerView with a LinearLayoutManager or its + * subclass oriented vertically. + * + * @param canvas The {@link Canvas} onto which vertical dividers will be + * drawn + * @param parent The RecyclerView onto which vertical dividers are being + * added + */ + private void drawVerticalDividers(Canvas canvas, RecyclerView parent) { + int parentLeft = parent.getPaddingLeft(); + int parentRight = parent.getWidth() - parent.getPaddingRight(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + + LayoutParams params = (LayoutParams) child.getLayoutParams(); + + int parentTop = child.getBottom() + params.bottomMargin; + int parentBottom = parentTop + mDivider.getIntrinsicHeight(); + + mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom); + mDivider.draw(canvas); + } + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/AVLoadingIndicatorView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/AVLoadingIndicatorView.java new file mode 100644 index 0000000..b70fb76 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/AVLoadingIndicatorView.java @@ -0,0 +1,365 @@ +package com.hzecool.widget.xRecyclerView.progressindicator; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Build; +import android.support.annotation.IntDef; +import android.util.AttributeSet; +import android.view.View; + +import com.hzecool.widget.R; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallBeatIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallClipRotateIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallClipRotateMultipleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallClipRotatePulseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallGridBeatIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallGridPulseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallPulseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallPulseRiseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallPulseSyncIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallRotateIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleMultipleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleRippleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleRippleMultipleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallSpinFadeLoaderIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallTrianglePathIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallZigZagDeflectIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallZigZagIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BaseIndicatorController; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.CubeTransitionIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScaleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScalePartyIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScalePulseOutIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScalePulseOutRapidIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineSpinFadeLoaderIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.PacmanIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.SemiCircleSpinIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.SquareSpinIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.TriangleSkewSpinIndicator; + + +/** + * Created by Jack on 2015/10/15 + * + .BallPulse, + .BallGridPulse, + .BallClipRotate, + .BallClipRotatePulse, + .SquareSpin, + .BallClipRotateMultiple, + .BallPulseRise, + .BallRotate, + .CubeTransition, + .BallZigZag, + .BallZigZagDeflect, + .BallTrianglePath, + .BallScale, + .LineScale, + .LineScaleParty, + .BallScaleMultiple, + .BallPulseSync, + .BallBeat, + .LineScalePulseOut, + .LineScalePulseOutRapid, + .BallScaleRipple, + .BallScaleRippleMultiple, + .BallSpinFadeLoader, + .LineSpinFadeLoader, + .TriangleSkewSpin, + .Pacman, + .BallGridBeat, + .SemiCircleSpin + * + */ +public class AVLoadingIndicatorView extends View{ + //indicators + public static final int BallPulse=0; + public static final int BallGridPulse=1; + public static final int BallClipRotate=2; + public static final int BallClipRotatePulse=3; + public static final int SquareSpin=4; + public static final int BallClipRotateMultiple=5; + public static final int BallPulseRise=6; + public static final int BallRotate=7; + public static final int CubeTransition=8; + public static final int BallZigZag=9; + public static final int BallZigZagDeflect=10; + public static final int BallTrianglePath=11; + public static final int BallScale=12; + public static final int LineScale=13; + public static final int LineScaleParty=14; + public static final int BallScaleMultiple=15; + public static final int BallPulseSync=16; + public static final int BallBeat=17; + public static final int LineScalePulseOut=18; + public static final int LineScalePulseOutRapid=19; + public static final int BallScaleRipple=20; + public static final int BallScaleRippleMultiple=21; + public static final int BallSpinFadeLoader=22; + public static final int LineSpinFadeLoader=23; + public static final int TriangleSkewSpin=24; + public static final int Pacman=25; + public static final int BallGridBeat=26; + public static final int SemiCircleSpin=27; + + + @IntDef(flag = true, + value = { + BallPulse, + BallGridPulse, + BallClipRotate, + BallClipRotatePulse, + SquareSpin, + BallClipRotateMultiple, + BallPulseRise, + BallRotate, + CubeTransition, + BallZigZag, + BallZigZagDeflect, + BallTrianglePath, + BallScale, + LineScale, + LineScaleParty, + BallScaleMultiple, + BallPulseSync, + BallBeat, + LineScalePulseOut, + LineScalePulseOutRapid, + BallScaleRipple, + BallScaleRippleMultiple, + BallSpinFadeLoader, + LineSpinFadeLoader, + TriangleSkewSpin, + Pacman, + BallGridBeat, + SemiCircleSpin + }) + public @interface Indicator{} + + //Sizes (with defaults in DP) + public static final int DEFAULT_SIZE=30; + + //attrs + int mIndicatorId; + int mIndicatorColor; + + Paint mPaint; + + BaseIndicatorController mIndicatorController; + + private boolean mHasAnimation; + + public AVLoadingIndicatorView(Context context) { + super(context); + init(null, 0); + } + + public AVLoadingIndicatorView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public AVLoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs, defStyleAttr); + } + + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public AVLoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(attrs, defStyleAttr); + } + + private void init(AttributeSet attrs, int defStyle) { + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.AVLoadingIndicatorView); + mIndicatorId=a.getInt(R.styleable.AVLoadingIndicatorView_indicator, BallPulse); + mIndicatorColor=a.getColor(R.styleable.AVLoadingIndicatorView_indicator_color, Color.WHITE); + a.recycle(); + mPaint=new Paint(); + mPaint.setColor(mIndicatorColor); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setAntiAlias(true); + applyIndicator(); + } + + public void setIndicatorId(int indicatorId){ + mIndicatorId = indicatorId; + applyIndicator(); + } + + public void setIndicatorColor(int color){ + mIndicatorColor = color; + mPaint.setColor(mIndicatorColor); + this.invalidate(); + } + + private void applyIndicator(){ + switch (mIndicatorId){ + case BallPulse: + mIndicatorController=new BallPulseIndicator(); + break; + case BallGridPulse: + mIndicatorController=new BallGridPulseIndicator(); + break; + case BallClipRotate: + mIndicatorController=new BallClipRotateIndicator(); + break; + case BallClipRotatePulse: + mIndicatorController=new BallClipRotatePulseIndicator(); + break; + case SquareSpin: + mIndicatorController=new SquareSpinIndicator(); + break; + case BallClipRotateMultiple: + mIndicatorController=new BallClipRotateMultipleIndicator(); + break; + case BallPulseRise: + mIndicatorController=new BallPulseRiseIndicator(); + break; + case BallRotate: + mIndicatorController=new BallRotateIndicator(); + break; + case CubeTransition: + mIndicatorController=new CubeTransitionIndicator(); + break; + case BallZigZag: + mIndicatorController=new BallZigZagIndicator(); + break; + case BallZigZagDeflect: + mIndicatorController=new BallZigZagDeflectIndicator(); + break; + case BallTrianglePath: + mIndicatorController=new BallTrianglePathIndicator(); + break; + case BallScale: + mIndicatorController=new BallScaleIndicator(); + break; + case LineScale: + mIndicatorController=new LineScaleIndicator(); + break; + case LineScaleParty: + mIndicatorController=new LineScalePartyIndicator(); + break; + case BallScaleMultiple: + mIndicatorController=new BallScaleMultipleIndicator(); + break; + case BallPulseSync: + mIndicatorController=new BallPulseSyncIndicator(); + break; + case BallBeat: + mIndicatorController=new BallBeatIndicator(); + break; + case LineScalePulseOut: + mIndicatorController=new LineScalePulseOutIndicator(); + break; + case LineScalePulseOutRapid: + mIndicatorController=new LineScalePulseOutRapidIndicator(); + break; + case BallScaleRipple: + mIndicatorController=new BallScaleRippleIndicator(); + break; + case BallScaleRippleMultiple: + mIndicatorController=new BallScaleRippleMultipleIndicator(); + break; + case BallSpinFadeLoader: + mIndicatorController=new BallSpinFadeLoaderIndicator(); + break; + case LineSpinFadeLoader: + mIndicatorController=new LineSpinFadeLoaderIndicator(); + break; + case TriangleSkewSpin: + mIndicatorController=new TriangleSkewSpinIndicator(); + break; + case Pacman: + mIndicatorController=new PacmanIndicator(); + break; + case BallGridBeat: + mIndicatorController=new BallGridBeatIndicator(); + break; + case SemiCircleSpin: + mIndicatorController=new SemiCircleSpinIndicator(); + break; + } + mIndicatorController.setTarget(this); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = measureDimension(dp2px(DEFAULT_SIZE), widthMeasureSpec); + int height = measureDimension(dp2px(DEFAULT_SIZE), heightMeasureSpec); + setMeasuredDimension(width, height); + } + + private int measureDimension(int defaultSize,int measureSpec){ + int result = defaultSize; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + if (specMode == MeasureSpec.EXACTLY) { + result = specSize; + } else if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(defaultSize, specSize); + } else { + result = defaultSize; + } + return result; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawIndicator(canvas); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (!mHasAnimation){ + mHasAnimation=true; + applyAnimation(); + } + } + + @Override + public void setVisibility(int v) { + if (getVisibility() != v) { + super.setVisibility(v); + if (v == GONE || v == INVISIBLE) { + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.END); + } else { + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.CANCEL); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START); + } + + void drawIndicator(Canvas canvas){ + mIndicatorController.draw(canvas, mPaint); + } + + void applyAnimation(){ + mIndicatorController.initAnimation(); + } + + private int dp2px(int dpValue) { + return (int) getContext().getResources().getDisplayMetrics().density * dpValue; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallBeatIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallBeatIndicator.java new file mode 100644 index 0000000..1191b9b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallBeatIndicator.java @@ -0,0 +1,82 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallBeatIndicator extends BaseIndicatorController { + + public static final float SCALE = 1.0f; + + public static final int ALPHA = 255; + + private float[] scaleFloats = new float[]{SCALE, + SCALE, + SCALE}; + + int[] alphas = new int[]{ALPHA, + ALPHA, + ALPHA,}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing = 4; + float radius = (getWidth() - circleSpacing * 2) / 6; + float x = getWidth() / 2 - (radius * 2 + circleSpacing); + float y = getHeight() / 2; + for (int i = 0; i < 3; i++) { + canvas.save(); + float translateX = x + (radius * 2) * i + circleSpacing * i; + canvas.translate(translateX, y); + canvas.scale(scaleFloats[i], scaleFloats[i]); + paint.setAlpha(alphas[i]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators = new ArrayList<>(); + int[] delays = new int[]{350, 0, 350}; + for (int i = 0; i < 3; i++) { + final int index = i; + ValueAnimator scaleAnim = ValueAnimator.ofFloat(1, 0.75f, 1); + scaleAnim.setDuration(700); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim = ValueAnimator.ofInt(255, 51, 255); + alphaAnim.setDuration(700); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateIndicator.java new file mode 100644 index 0000000..d491163 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateIndicator.java @@ -0,0 +1,65 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallClipRotateIndicator extends BaseIndicatorController { + + float scaleFloat=1,degrees; + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + + float circleSpacing=12; + float x = (getWidth()) / 2; + float y=(getHeight()) / 2; + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.rotate(degrees); + RectF rectF=new RectF(-x+circleSpacing,-y+circleSpacing,0+x-circleSpacing,0+y-circleSpacing); + canvas.drawArc(rectF, -45, 270, false, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.6f,0.5f,1); + scaleAnim.setDuration(750); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0,180,360); + rotateAnim.setDuration(750); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateMultipleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateMultipleIndicator.java new file mode 100644 index 0000000..67fcfed --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateMultipleIndicator.java @@ -0,0 +1,85 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/17. + */ +public class BallClipRotateMultipleIndicator extends BaseIndicatorController{ + + float scaleFloat=1,degrees; + + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStrokeWidth(3); + paint.setStyle(Paint.Style.STROKE); + + float circleSpacing=12; + float x=getWidth()/2; + float y=getHeight()/2; + + canvas.save(); + + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.rotate(degrees); + + //draw two big arc + float[] bStartAngles=new float[]{135,-45}; + for (int i = 0; i < 2; i++) { + RectF rectF=new RectF(-x+circleSpacing,-y+circleSpacing,x-circleSpacing,y-circleSpacing); + canvas.drawArc(rectF, bStartAngles[i], 90, false, paint); + } + + canvas.restore(); + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.rotate(-degrees); + //draw two small arc + float[] sStartAngles=new float[]{225,45}; + for (int i = 0; i < 2; i++) { + RectF rectF=new RectF(-x/1.8f+circleSpacing,-y/1.8f+circleSpacing,x/1.8f-circleSpacing,y/1.8f-circleSpacing); + canvas.drawArc(rectF, sStartAngles[i], 90, false, paint); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.6f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0, 180,360); + rotateAnim.setDuration(1000); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotatePulseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotatePulseIndicator.java new file mode 100644 index 0000000..96a3322 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotatePulseIndicator.java @@ -0,0 +1,94 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallClipRotatePulseIndicator extends BaseIndicatorController { + + float scaleFloat1,scaleFloat2,degrees; + + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=12; + float x=getWidth()/2; + float y=getHeight()/2; + + //draw fill circle + canvas.save(); + canvas.translate(x, y); + canvas.scale(scaleFloat1, scaleFloat1); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(0, 0, x / 2.5f, paint); + + canvas.restore(); + + canvas.translate(x, y); + canvas.scale(scaleFloat2, scaleFloat2); + canvas.rotate(degrees); + + paint.setStrokeWidth(3); + paint.setStyle(Paint.Style.STROKE); + + //draw two arc + float[] startAngles=new float[]{225,45}; + for (int i = 0; i < 2; i++) { + RectF rectF=new RectF(-x+circleSpacing,-y+circleSpacing,x-circleSpacing,y-circleSpacing); + canvas.drawArc(rectF, startAngles[i], 90, false, paint); + } + } + + @Override + public List createAnimation() { + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.3f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat1 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator scaleAnim2=ValueAnimator.ofFloat(1,0.6f,1); + scaleAnim2.setDuration(1000); + scaleAnim2.setRepeatCount(-1); + scaleAnim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat2 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim2.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0, 180,360); + rotateAnim.setDuration(1000); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + List animators=new ArrayList<>(); + animators.add(scaleAnim); + animators.add(scaleAnim2); + animators.add(rotateAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridBeatIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridBeatIndicator.java new file mode 100644 index 0000000..1d55eec --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridBeatIndicator.java @@ -0,0 +1,76 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class BallGridBeatIndicator extends BaseIndicatorController { + + public static final int ALPHA=255; + + int[] alphas=new int[]{ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*4)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + float y = getWidth()/ 2-(radius*2+circleSpacing); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + canvas.save(); + float translateX=x+(radius*2)*j+circleSpacing*j; + float translateY=y+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, translateY); + paint.setAlpha(alphas[3 * i + j]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + + int[] durations={960, 930, 1190, 1130, 1340, 940, 1200, 820, 1190}; + int[] delays= {360, 400, 680, 410, 710, -150, -120, 10, 320}; + + for (int i = 0; i < 9; i++) { + final int index=i; + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 168,255); + alphaAnim.setDuration(durations[i]); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(alphaAnim); + } + return animators; + } + + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridPulseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridPulseIndicator.java new file mode 100644 index 0000000..09b3219 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridPulseIndicator.java @@ -0,0 +1,103 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallGridPulseIndicator extends BaseIndicatorController{ + + public static final int ALPHA=255; + + public static final float SCALE=1.0f; + + int[] alphas=new int[]{ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA}; + + float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE}; + + + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*4)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + float y = getWidth()/ 2-(radius*2+circleSpacing); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + canvas.save(); + float translateX=x+(radius*2)*j+circleSpacing*j; + float translateY=y+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, translateY); + canvas.scale(scaleFloats[3 * i + j], scaleFloats[3 * i + j]); + paint.setAlpha(alphas[3 * i + j]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + int[] durations={720, 1020, 1280, 1420, 1450, 1180, 870, 1450, 1060}; + int[] delays= {-60, 250, -170, 480, 310, 30, 460, 780, 450}; + + for (int i = 0; i < 9; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.5f,1); + scaleAnim.setDuration(durations[i]); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 210, 122, 255); + alphaAnim.setDuration(durations[i]); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseIndicator.java new file mode 100644 index 0000000..fb7705f --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseIndicator.java @@ -0,0 +1,68 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallPulseIndicator extends BaseIndicatorController{ + + public static final float SCALE=1.0f; + + //scale x ,y + private float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE}; + + + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(Math.min(getWidth(),getHeight())-circleSpacing*2)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + float y=getHeight() / 2; + for (int i = 0; i < 3; i++) { + canvas.save(); + float translateX=x+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, y); + canvas.scale(scaleFloats[i], scaleFloats[i]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + int[] delays=new int[]{120,240,360}; + for (int i = 0; i < 3; i++) { + final int index=i; + + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.3f,1); + + scaleAnim.setDuration(750); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseRiseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseRiseIndicator.java new file mode 100644 index 0000000..598a615 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseRiseIndicator.java @@ -0,0 +1,41 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/17. + */ +public class BallPulseRiseIndicator extends BaseIndicatorController{ + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + canvas.drawCircle(getWidth()/4,radius*2,radius,paint); + canvas.drawCircle(getWidth()*3/4,radius*2,radius,paint); + + canvas.drawCircle(radius,getHeight()-2*radius,radius,paint); + canvas.drawCircle(getWidth()/2,getHeight()-2*radius,radius,paint); + canvas.drawCircle(getWidth()-radius,getHeight()-2*radius,radius,paint); + } + + @Override + public List createAnimation() { + PropertyValuesHolder rotation6=PropertyValuesHolder.ofFloat("rotationX",0,360); + ObjectAnimator animator=ObjectAnimator.ofPropertyValuesHolder(getTarget(), rotation6); + animator.setInterpolator(new LinearInterpolator()); + animator.setRepeatCount(-1); + animator.setDuration(1500); + animator.start(); + List animators=new ArrayList<>(); + animators.add(animator); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseSyncIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseSyncIndicator.java new file mode 100644 index 0000000..7b74eea --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseSyncIndicator.java @@ -0,0 +1,57 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallPulseSyncIndicator extends BaseIndicatorController { + + float[] translateYFloats=new float[3]; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*2)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + for (int i = 0; i < 3; i++) { + canvas.save(); + float translateX=x+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, translateYFloats[i]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*2)/6; + int[] delays=new int[]{70,140,210}; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(getHeight()/2,getHeight()/2-radius*2,getHeight()/2); + scaleAnim.setDuration(600); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallRotateIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallRotateIndicator.java new file mode 100644 index 0000000..2c46631 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallRotateIndicator.java @@ -0,0 +1,71 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/17. + */ +public class BallRotateIndicator extends BaseIndicatorController{ + + float scaleFloat=0.5f; + + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + float x = getWidth()/ 2; + float y=getHeight()/2; + + canvas.save(); + canvas.translate(x - radius * 2 - radius, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + + canvas.save(); + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + + canvas.save(); + canvas.translate(x + radius * 2 + radius, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.drawCircle(0,0,radius, paint); + canvas.restore(); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0.5f,1,0.5f); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ObjectAnimator rotateAnim=ObjectAnimator.ofFloat(getTarget(),"rotation",0,180,360); + rotateAnim.setDuration(1000); + rotateAnim.setRepeatCount(-1); + rotateAnim.start(); + + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleIndicator.java new file mode 100644 index 0000000..2865b09 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleIndicator.java @@ -0,0 +1,63 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleIndicator extends BaseIndicatorController { + + float scale=1; + int alpha=255; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + paint.setAlpha(alpha); + canvas.scale(scale,scale,getWidth()/2,getHeight()/2); + paint.setAlpha(alpha); + canvas.drawCircle(getWidth()/2,getHeight()/2,getWidth()/2-circleSpacing,paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scale = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 0); + alphaAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alpha = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleMultipleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleMultipleIndicator.java new file mode 100644 index 0000000..9af4551 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleMultipleIndicator.java @@ -0,0 +1,70 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleMultipleIndicator extends BaseIndicatorController { + + float[] scaleFloats=new float[]{1,1,1}; + int[] alphaInts=new int[]{255,255,255}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + for (int i = 0; i < 3; i++) { + paint.setAlpha(alphaInts[i]); + canvas.scale(scaleFloats[i],scaleFloats[i],getWidth()/2,getHeight()/2); + canvas.drawCircle(getWidth()/2,getHeight()/2,getWidth()/2-circleSpacing,paint); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{0, 200, 400}; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255,0); + alphaAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphaInts[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + alphaAnim.start(); + + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleIndicator.java new file mode 100644 index 0000000..47008ba --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleIndicator.java @@ -0,0 +1,59 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleRippleIndicator extends BallScaleIndicator { + + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + super.draw(canvas, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scale = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(0, 255); + alphaAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alpha = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + + animators.add(scaleAnim); + animators.add(alphaAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleMultipleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleMultipleIndicator.java new file mode 100644 index 0000000..9abbea7 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleMultipleIndicator.java @@ -0,0 +1,65 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleRippleMultipleIndicator extends BallScaleMultipleIndicator{ + + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + super.draw(canvas, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{0, 200, 400}; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(0,255); + scaleAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphaInts[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + alphaAnim.start(); + + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallSpinFadeLoaderIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallSpinFadeLoaderIndicator.java new file mode 100644 index 0000000..f23ab8f --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallSpinFadeLoaderIndicator.java @@ -0,0 +1,116 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class BallSpinFadeLoaderIndicator extends BaseIndicatorController { + + public static final float SCALE=1.0f; + + public static final int ALPHA=255; + + float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE}; + + int[] alphas=new int[]{ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA}; + + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + for (int i = 0; i < 8; i++) { + canvas.save(); + Point point=circleAt(getWidth(),getHeight(),getWidth()/2-radius,i*(Math.PI/4)); + canvas.translate(point.x,point.y); + canvas.scale(scaleFloats[i],scaleFloats[i]); + paint.setAlpha(alphas[i]); + canvas.drawCircle(0,0,radius,paint); + canvas.restore(); + } + } + + /** + * 圆O的圆心为(a,b),半径为R,点A与到X轴的为角α. + *则点A的坐标为(a+R*cosα,b+R*sinα) + * @param width + * @param height + * @param radius + * @param angle + * @return + */ + Point circleAt(int width,int height,float radius,double angle){ + float x= (float) (width/2+radius*(Math.cos(angle))); + float y= (float) (height/2+radius*(Math.sin(angle))); + return new Point(x,y); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + int[] delays= {0, 120, 240, 360, 480, 600, 720, 780, 840}; + for (int i = 0; i < 8; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.4f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 77, 255); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + + final class Point{ + public float x; + public float y; + + public Point(float x, float y){ + this.x=x; + this.y=y; + } + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallTrianglePathIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallTrianglePathIndicator.java new file mode 100644 index 0000000..7a66938 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallTrianglePathIndicator.java @@ -0,0 +1,82 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallTrianglePathIndicator extends BaseIndicatorController { + + float[] translateX=new float[3],translateY=new float[3]; + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStrokeWidth(3); + paint.setStyle(Paint.Style.STROKE); + for (int i = 0; i < 3; i++) { + canvas.save(); + canvas.translate(translateX[i], translateY[i]); + canvas.drawCircle(0, 0, getWidth() / 10, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/5; + float startY=getWidth()/5; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator translateXAnim=ValueAnimator.ofFloat(getWidth()/2,getWidth()-startX,startX,getWidth()/2); + if (i==1){ + translateXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,getWidth()/2,getWidth()-startX); + }else if (i==2){ + translateXAnim=ValueAnimator.ofFloat(startX,getWidth()/2,getWidth()-startX,startX); + } + ValueAnimator translateYAnim=ValueAnimator.ofFloat(startY,getHeight()-startY,getHeight()-startY,startY); + if (i==1){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,startY,getHeight()-startY); + }else if (i==2){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,startY,getHeight()-startY,getHeight()-startY); + } + + translateXAnim.setDuration(2000); + translateXAnim.setInterpolator(new LinearInterpolator()); + translateXAnim.setRepeatCount(-1); + translateXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateXAnim.start(); + + translateYAnim.setDuration(2000); + translateYAnim.setInterpolator(new LinearInterpolator()); + translateYAnim.setRepeatCount(-1); + translateYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateYAnim.start(); + + animators.add(translateXAnim); + animators.add(translateYAnim); + } + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagDeflectIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagDeflectIndicator.java new file mode 100644 index 0000000..a7fe02a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagDeflectIndicator.java @@ -0,0 +1,65 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallZigZagDeflectIndicator extends BallZigZagIndicator { + + + + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/6; + float startY=getWidth()/6; + for (int i = 0; i < 2; i++) { + final int index=i; + ValueAnimator translateXAnim=ValueAnimator.ofFloat(startX,getWidth()-startX,startX,getWidth()-startX,startX); + if (i==1){ + translateXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,getWidth()-startX,startX,getWidth()-startX); + } + ValueAnimator translateYAnim=ValueAnimator.ofFloat(startY,startY,getHeight()-startY,getHeight()-startY,startY); + if (i==1){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,startY,startY,getHeight()-startY); + } + + translateXAnim.setDuration(2000); + translateXAnim.setInterpolator(new LinearInterpolator()); + translateXAnim.setRepeatCount(-1); + translateXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateXAnim.start(); + + translateYAnim.setDuration(2000); + translateYAnim.setInterpolator(new LinearInterpolator()); + translateYAnim.setRepeatCount(-1); + translateYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateYAnim.start(); + + animators.add(translateXAnim); + animators.add(translateYAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagIndicator.java new file mode 100644 index 0000000..5b4f63d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagIndicator.java @@ -0,0 +1,75 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallZigZagIndicator extends BaseIndicatorController { + + float[] translateX=new float[2],translateY=new float[2]; + + + @Override + public void draw(Canvas canvas, Paint paint) { + for (int i = 0; i < 2; i++) { + canvas.save(); + canvas.translate(translateX[i], translateY[i]); + canvas.drawCircle(0, 0, getWidth() / 10, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/6; + float startY=getWidth()/6; + for (int i = 0; i < 2; i++) { + final int index=i; + ValueAnimator translateXAnim=ValueAnimator.ofFloat(startX,getWidth()-startX,getWidth()/2,startX); + if (i==1){ + translateXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,getWidth()/2,getWidth()-startX); + } + ValueAnimator translateYAnim=ValueAnimator.ofFloat(startY,startY,getHeight()/2,startY); + if (i==1){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,getHeight()/2,getHeight()-startY); + } + + translateXAnim.setDuration(1000); + translateXAnim.setInterpolator(new LinearInterpolator()); + translateXAnim.setRepeatCount(-1); + translateXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateXAnim.start(); + + translateYAnim.setDuration(1000); + translateYAnim.setInterpolator(new LinearInterpolator()); + translateYAnim.setRepeatCount(-1); + translateYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateYAnim.start(); + animators.add(translateXAnim); + animators.add(translateYAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BaseIndicatorController.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BaseIndicatorController.java new file mode 100644 index 0000000..93dd4db --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BaseIndicatorController.java @@ -0,0 +1,100 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.View; + +import java.util.List; + +/** + * Created by Jack on 2015/10/15. + */ +public abstract class BaseIndicatorController { + + + private View mTarget; + + private List mAnimators; + + + public void setTarget(View target){ + this.mTarget=target; + } + + public View getTarget(){ + return mTarget; + } + + + public int getWidth(){ + return mTarget.getWidth(); + } + + public int getHeight(){ + return mTarget.getHeight(); + } + + public void postInvalidate(){ + mTarget.postInvalidate(); + } + + /** + * draw indicator + * @param canvas + * @param paint + */ + public abstract void draw(Canvas canvas,Paint paint); + + /** + * create animation or animations + */ + public abstract List createAnimation(); + + public void initAnimation(){ + mAnimators=createAnimation(); + } + + /** + * make animation to start or end when target + * view was be Visible or Gone or Invisible. + * make animation to cancel when target view + * be onDetachedFromWindow. + * @param animStatus + */ + public void setAnimationStatus(AnimStatus animStatus){ + if (mAnimators==null){ + return; + } + int count=mAnimators.size(); + for (int i = 0; i < count; i++) { + Animator animator=mAnimators.get(i); + boolean isRunning=animator.isRunning(); + switch (animStatus){ + case START: + if (!isRunning){ + animator.start(); + } + break; + case END: + if (isRunning){ + animator.end(); + } + break; + case CANCEL: + if (isRunning){ + animator.cancel(); + } + break; + } + } + } + + + public enum AnimStatus{ + START,END,CANCEL + } + + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/CubeTransitionIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/CubeTransitionIndicator.java new file mode 100644 index 0000000..8a0543b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/CubeTransitionIndicator.java @@ -0,0 +1,110 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/18. + */ +public class CubeTransitionIndicator extends BaseIndicatorController { + + float[] translateX=new float[2],translateY=new float[2]; + float degrees,scaleFloat=1.0f; + + @Override + public void draw(Canvas canvas, Paint paint) { + float rWidth=getWidth()/5; + float rHeight=getHeight()/5; + for (int i = 0; i < 2; i++) { + canvas.save(); + canvas.translate(translateX[i], translateY[i]); + canvas.rotate(degrees); + canvas.scale(scaleFloat,scaleFloat); + RectF rectF=new RectF(-rWidth/2,-rHeight/2,rWidth/2,rHeight/2); + canvas.drawRect(rectF,paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/5; + float startY=getHeight()/5; + for (int i = 0; i < 2; i++) { + final int index=i; + translateX[index]=startX; + ValueAnimator translationXAnim=ValueAnimator.ofFloat(startX,getWidth()-startX,getWidth()-startX, startX,startX); + if (i==1){ + translationXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,startX, getWidth()-startX,getWidth()-startX); + } + translationXAnim.setInterpolator(new LinearInterpolator()); + translationXAnim.setDuration(1600); + translationXAnim.setRepeatCount(-1); + translationXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translationXAnim.start(); + translateY[index]=startY; + ValueAnimator translationYAnim=ValueAnimator.ofFloat(startY,startY,getHeight()-startY,getHeight()- startY,startY); + if (i==1){ + translationYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,startY,startY,getHeight()-startY); + } + translationYAnim.setDuration(1600); + translationYAnim.setInterpolator(new LinearInterpolator()); + translationYAnim.setRepeatCount(-1); + translationYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translationYAnim.start(); + + animators.add(translationXAnim); + animators.add(translationYAnim); + } + + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.5f,1,0.5f,1); + scaleAnim.setDuration(1600); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0,180,360,1.5f*360,2*360); + rotateAnim.setDuration(1600); + rotateAnim.setInterpolator(new LinearInterpolator()); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScaleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScaleIndicator.java new file mode 100644 index 0000000..828be12 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScaleIndicator.java @@ -0,0 +1,62 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScaleIndicator extends BaseIndicatorController { + + public static final float SCALE=1.0f; + + float[] scaleYFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE,}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float translateX=getWidth()/11; + float translateY=getHeight()/2; + for (int i = 0; i < 5; i++) { + canvas.save(); + canvas.translate((2 + i * 2) * translateX - translateX / 2, translateY); + canvas.scale(SCALE, scaleYFloats[i]); + RectF rectF=new RectF(-translateX/2,-getHeight()/2.5f,translateX/2,getHeight()/2.5f); + canvas.drawRoundRect(rectF, 5, 5, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{100,200,300,400,500}; + for (int i = 0; i < 5; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1, 0.4f, 1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePartyIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePartyIndicator.java new file mode 100644 index 0000000..6ce067a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePartyIndicator.java @@ -0,0 +1,64 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScalePartyIndicator extends BaseIndicatorController { + + public static final float SCALE=1.0f; + + float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE,}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float translateX=getWidth()/9; + float translateY=getHeight()/2; + for (int i = 0; i < 4; i++) { + canvas.save(); + canvas.translate((2 + i * 2) * translateX - translateX / 2, translateY); + canvas.scale(scaleFloats[i], scaleFloats[i]); + RectF rectF=new RectF(-translateX/2,-getHeight()/2.5f,translateX/2,getHeight()/2.5f); + canvas.drawRoundRect(rectF,5,5,paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] durations=new long[]{1260, 430, 1010, 730}; + long[] delays=new long[]{770, 290, 280, 740}; + for (int i = 0; i < 4; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.4f,1); + scaleAnim.setDuration(durations[i]); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutIndicator.java new file mode 100644 index 0000000..dacd45d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutIndicator.java @@ -0,0 +1,37 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScalePulseOutIndicator extends LineScaleIndicator { + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{500,250,0,250,500}; + for (int i = 0; i < 5; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.3f,1); + scaleAnim.setDuration(900); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutRapidIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutRapidIndicator.java new file mode 100644 index 0000000..8f7facc --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutRapidIndicator.java @@ -0,0 +1,37 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScalePulseOutRapidIndicator extends LineScaleIndicator { + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{400,200,0,200,400}; + for (int i = 0; i < 5; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.4f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineSpinFadeLoaderIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineSpinFadeLoaderIndicator.java new file mode 100644 index 0000000..246c8ca --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineSpinFadeLoaderIndicator.java @@ -0,0 +1,30 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +/** + * Created by Jack on 2015/10/24. + * Email:81813780@qq.com + */ +public class LineSpinFadeLoaderIndicator extends BallSpinFadeLoaderIndicator { + + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + for (int i = 0; i < 8; i++) { + canvas.save(); + Point point=circleAt(getWidth(),getHeight(),getWidth()/2.5f-radius,i*(Math.PI/4)); + canvas.translate(point.x, point.y); + canvas.scale(scaleFloats[i], scaleFloats[i]); + canvas.rotate(i*45); + paint.setAlpha(alphas[i]); + RectF rectF=new RectF(-radius,-radius/1.5f,1.5f*radius,radius/1.5f); + canvas.drawRoundRect(rectF,5,5,paint); + canvas.restore(); + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/PacmanIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/PacmanIndicator.java new file mode 100644 index 0000000..2b161f2 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/PacmanIndicator.java @@ -0,0 +1,119 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class PacmanIndicator extends BaseIndicatorController{ + + private float translateX; + + private int alpha; + + private float degrees1,degrees2; + + @Override + public void draw(Canvas canvas, Paint paint) { + drawPacman(canvas,paint); + drawCircle(canvas,paint); + } + + private void drawPacman(Canvas canvas,Paint paint){ + float x=getWidth()/2; + float y=getHeight()/2; + + canvas.save(); + + canvas.translate(x, y); + canvas.rotate(degrees1); + paint.setAlpha(255); + RectF rectF1=new RectF(-x/1.7f,-y/1.7f,x/1.7f,y/1.7f); + canvas.drawArc(rectF1, 0, 270, true, paint); + + canvas.restore(); + + canvas.save(); + canvas.translate(x, y); + canvas.rotate(degrees2); + paint.setAlpha(255); + RectF rectF2=new RectF(-x/1.7f,-y/1.7f,x/1.7f,y/1.7f); + canvas.drawArc(rectF2,90,270,true,paint); + canvas.restore(); + } + + + private void drawCircle(Canvas canvas, Paint paint) { + float radius=getWidth()/11; + paint.setAlpha(alpha); + canvas.drawCircle(translateX, getHeight() / 2, radius, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startT=getWidth()/11; + ValueAnimator translationAnim=ValueAnimator.ofFloat(getWidth()-startT,getWidth()/2); + translationAnim.setDuration(650); + translationAnim.setInterpolator(new LinearInterpolator()); + translationAnim.setRepeatCount(-1); + translationAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translationAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255,122); + alphaAnim.setDuration(650); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alpha = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + + ValueAnimator rotateAnim1=ValueAnimator.ofFloat(0, 45, 0); + rotateAnim1.setDuration(650); + rotateAnim1.setRepeatCount(-1); + rotateAnim1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees1 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim1.start(); + + ValueAnimator rotateAnim2=ValueAnimator.ofFloat(0,-45,0); + rotateAnim2.setDuration(650); + rotateAnim2.setRepeatCount(-1); + rotateAnim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees2 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim2.start(); + + animators.add(translationAnim); + animators.add(alphaAnim); + animators.add(rotateAnim1); + animators.add(rotateAnim2); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SemiCircleSpinIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SemiCircleSpinIndicator.java new file mode 100644 index 0000000..8e6e613 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SemiCircleSpinIndicator.java @@ -0,0 +1,36 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class SemiCircleSpinIndicator extends BaseIndicatorController { + + + @Override + public void draw(Canvas canvas, Paint paint) { + RectF rectF=new RectF(0,0,getWidth(),getHeight()); + canvas.drawArc(rectF,-60,120,false,paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ObjectAnimator rotateAnim=ObjectAnimator.ofFloat(getTarget(),"rotation",0,180,360); + rotateAnim.setDuration(600); + rotateAnim.setRepeatCount(-1); + rotateAnim.start(); + animators.add(rotateAnim); + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SquareSpinIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SquareSpinIndicator.java new file mode 100644 index 0000000..b7567f0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SquareSpinIndicator.java @@ -0,0 +1,37 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class SquareSpinIndicator extends BaseIndicatorController { + + @Override + public void draw(Canvas canvas, Paint paint) { + canvas.drawRect(new RectF(getWidth()/5,getHeight()/5,getWidth()*4/5,getHeight()*4/5),paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + PropertyValuesHolder rotation5=PropertyValuesHolder.ofFloat("rotationX",0,180,180,0,0); + PropertyValuesHolder rotation6=PropertyValuesHolder.ofFloat("rotationY",0,0,180,180,0); + ObjectAnimator animator=ObjectAnimator.ofPropertyValuesHolder(getTarget(), rotation6,rotation5); + animator.setInterpolator(new LinearInterpolator()); + animator.setRepeatCount(-1); + animator.setDuration(2500); + animator.start(); + animators.add(animator); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/TriangleSkewSpinIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/TriangleSkewSpinIndicator.java new file mode 100644 index 0000000..3b3ff1d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/TriangleSkewSpinIndicator.java @@ -0,0 +1,45 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class TriangleSkewSpinIndicator extends BaseIndicatorController { + + @Override + public void draw(Canvas canvas, Paint paint) { + Path path=new Path(); + path.moveTo(getWidth()/5,getHeight()*4/5); + path.lineTo(getWidth()*4/5, getHeight()*4/5); + path.lineTo(getWidth()/2,getHeight()/5); + path.close(); + canvas.drawPath(path, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + PropertyValuesHolder rotation5=PropertyValuesHolder.ofFloat("rotationX",0,180,180,0,0); + PropertyValuesHolder rotation6=PropertyValuesHolder.ofFloat("rotationY",0,0,180,180,0); + + ObjectAnimator animator=ObjectAnimator.ofPropertyValuesHolder(getTarget(), rotation6,rotation5); + animator.setInterpolator(new LinearInterpolator()); + animator.setRepeatCount(-1); + animator.setDuration(2500); + animator.start(); + + animators.add(animator); + return animators; + } + +} diff --git a/widget/src/main/res/anim/action_sheet_dialog_enter.xml b/widget/src/main/res/anim/action_sheet_dialog_enter.xml new file mode 100644 index 0000000..a3cb29a --- /dev/null +++ b/widget/src/main/res/anim/action_sheet_dialog_enter.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/action_sheet_dialog_exit.xml b/widget/src/main/res/anim/action_sheet_dialog_exit.xml new file mode 100644 index 0000000..c64b482 --- /dev/null +++ b/widget/src/main/res/anim/action_sheet_dialog_exit.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/decelerate_cubic.xml b/widget/src/main/res/anim/decelerate_cubic.xml new file mode 100644 index 0000000..58432b6 --- /dev/null +++ b/widget/src/main/res/anim/decelerate_cubic.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/widget/src/main/res/anim/dialog_enter_anim.xml b/widget/src/main/res/anim/dialog_enter_anim.xml new file mode 100644 index 0000000..b97db24 --- /dev/null +++ b/widget/src/main/res/anim/dialog_enter_anim.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/dialog_exit_anim.xml b/widget/src/main/res/anim/dialog_exit_anim.xml new file mode 100644 index 0000000..fd09a5b --- /dev/null +++ b/widget/src/main/res/anim/dialog_exit_anim.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/hot_pop_enter.xml b/widget/src/main/res/anim/hot_pop_enter.xml new file mode 100644 index 0000000..9437732 --- /dev/null +++ b/widget/src/main/res/anim/hot_pop_enter.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/widget/src/main/res/anim/hot_pop_out.xml b/widget/src/main/res/anim/hot_pop_out.xml new file mode 100644 index 0000000..367bcfb --- /dev/null +++ b/widget/src/main/res/anim/hot_pop_out.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/popup_enter.xml b/widget/src/main/res/anim/popup_enter.xml new file mode 100644 index 0000000..130b9cb --- /dev/null +++ b/widget/src/main/res/anim/popup_enter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/popup_exit.xml b/widget/src/main/res/anim/popup_exit.xml new file mode 100644 index 0000000..02e9649 --- /dev/null +++ b/widget/src/main/res/anim/popup_exit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svfade_in_center.xml b/widget/src/main/res/anim/svfade_in_center.xml new file mode 100644 index 0000000..8b162db --- /dev/null +++ b/widget/src/main/res/anim/svfade_in_center.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svfade_out_center.xml b/widget/src/main/res/anim/svfade_out_center.xml new file mode 100644 index 0000000..bda8de2 --- /dev/null +++ b/widget/src/main/res/anim/svfade_out_center.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_in_bottom.xml b/widget/src/main/res/anim/svslide_in_bottom.xml new file mode 100644 index 0000000..2d923d8 --- /dev/null +++ b/widget/src/main/res/anim/svslide_in_bottom.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_in_top.xml b/widget/src/main/res/anim/svslide_in_top.xml new file mode 100644 index 0000000..18a6b56 --- /dev/null +++ b/widget/src/main/res/anim/svslide_in_top.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_out_bottom.xml b/widget/src/main/res/anim/svslide_out_bottom.xml new file mode 100644 index 0000000..c496975 --- /dev/null +++ b/widget/src/main/res/anim/svslide_out_bottom.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_out_top.xml b/widget/src/main/res/anim/svslide_out_top.xml new file mode 100644 index 0000000..07d3830 --- /dev/null +++ b/widget/src/main/res/anim/svslide_out_top.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/color/mis_default_text_color.xml b/widget/src/main/res/color/mis_default_text_color.xml new file mode 100644 index 0000000..bdbd36c --- /dev/null +++ b/widget/src/main/res/color/mis_default_text_color.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/color/mis_folder_text_color.xml b/widget/src/main/res/color/mis_folder_text_color.xml new file mode 100644 index 0000000..0a07caf --- /dev/null +++ b/widget/src/main/res/color/mis_folder_text_color.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-hdpi/pickup.png b/widget/src/main/res/drawable-hdpi/pickup.png new file mode 100644 index 0000000000000000000000000000000000000000..b15d66a2c4e61462322aa51a905c6215a8a2c720 GIT binary patch literal 1922 zcmbVNc~BE~8xC?*B9|PF#pByX6s&G`H|Hi2H6%ffj+bG!n;q;I0%;e@89sI%Bn;(I3tv0tZV?RV_(BBRkU*<>+e$0(emG?IE8NVQ-l zJPXH+m1Sq}L=MNljL6Y3I&FsBKw9{i55sp@Y%H6@NnGNvVTKZ%0ra?uuqt@>PIdDD z!l>Zoi?y)UrozocN)?4?S7qiHs!9wpBX7xKAkiUb4Onmn100r8D=l{@cu(xg*|~2U z;sH+}Oo@W`)G3{I1)w4+91!!xpaDiifKPF9uE{7GRT41AcPM?7K`s*PtY`zh5zZs zbJ288r45I&aGETq3~WA%=S+~<*!{hb56F5WUrrHhQm|4rX(+egRwh-g;ISusBVm-I zGCdCCGCe3LMrEK#B*H;dWDtO`L;&kCDJGHN!U;RiGCT)O91tjDX zRwHSr=le4B^ulVKA}VlWGDTW|Cn1&-&yh!P5rSY+7(~PZ9MlU%0uU>Pi$Mt{MWjZF z7(rzc-ZPC>pp`AgkQY_Y}Yn?CCTcJZu_a4Xw96x$`C;Nzog^Bqi8D{~x| ze|$TAd9*UZV=@JsCpEwS`|Ght!6Tq+`InPtPIV6d5EXd(v#9FHucz^1yI(tETuE+F zC%PwXjIT>hG*`r&j+`?*_47?!*VxwB;|1RD?(X*P3|cU%x;kQ-UxRW68kSe`cqC+Y?V_Rp z#}sEhk;UcLZX4>J>$QJit(J%Pt+u}KR_3r}>#fG<)w&QT`q`Dq7Yd`X_#S7}?sK+~ zuHfU!BL>&6R7T|x|KpdFE4Q!x=rNSIABk^o`RiBPtc<1DyYcR@9i_W`}utfMX z*6xg16>}#vY%O~5?mn+7W~jfl;7mwy{BWrUjLrOG>4EUQYre`~Gsi`D)*lWaG^WDE z+}`)nk*4|I-rsyQOtWmqri9)bTlE>Uyl&o_m4`-0TWfA-j~-bjSU<2JaWp6=AfdJP z*0xw;q~+b*$g=q6PXLRoLe-f6%(~BGQ!f2G&_ln(Z5f&#*Ri?&i~72#pqxi4cNr4dQGhxI zIT*BlNa7tey>+SH&wZ8a$?c~gOGX>#nrd3!%g*l`4)?EaZ`$G=Oy8kf?>C%gXl|Ef zre9t*VQp?s}-Yk#S==4jRTpTq^vhzeUgDJ>~lLra4_E-%+xtvJ!^ zK9=lK=FIK9(nMf;-}CQ0dEl63&*(*n$#k`-A_n#!4gYy;U+O;b7VaMLmMlx?Z3+E@ zA9vHM#Le3uUpwg^F>rW#wyAByor_dy^QysldE&<(Z2KeO{lRU=$I<}qU&pB=;6@a z)^jaOS1C6aTx~nwKW|ua@8Y`3;NEa-XYz>~VKsF{;g_u7nu>WJG2W4Qskyu6x9eg@ j*zTHxO`CV@S~mse1UVI^NZB@j-(N9Rlc{b?DyscIDG%g7 literal 0 HcmV?d00001 diff --git a/widget/src/main/res/drawable-hdpi/rgb.png b/widget/src/main/res/drawable-hdpi/rgb.png new file mode 100644 index 0000000000000000000000000000000000000000..f70d9d965010fc9f46b563fd772a7d40d507e293 GIT binary patch literal 48014 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%0DN01_bGJwUUx- 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 F(O)||BisN0 literal 0 HcmV?d00001 diff --git a/widget/src/main/res/drawable-xhdpi/down.png b/widget/src/main/res/drawable-xhdpi/down.png new file mode 100644 index 0000000000000000000000000000000000000000..b24c7e8a956ef678aaffca7d34c3c644285c220c GIT binary patch literal 9980 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 literal 0 HcmV?d00001 diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selected.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selected.xml new file mode 100644 index 0000000..65d97c8 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selected.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selected_dark.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selected_dark.xml new file mode 100644 index 0000000..f91e7d7 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selected_dark.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector.xml new file mode 100644 index 0000000..ead9d21 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector_dark.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector_dark.xml new file mode 100644 index 0000000..564e141 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple.xml new file mode 100644 index 0000000..732eea4 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple_dark.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple_dark.xml new file mode 100644 index 0000000..732eea4 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple_dark.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_shape.xml b/widget/src/main/res/drawable-xhdpi/md_btn_shape.xml new file mode 100644 index 0000000..a0060c7 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_shape.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_item_selected.xml b/widget/src/main/res/drawable-xhdpi/md_item_selected.xml new file mode 100644 index 0000000..10bf840 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_item_selected.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_item_selected_dark.xml b/widget/src/main/res/drawable-xhdpi/md_item_selected_dark.xml new file mode 100644 index 0000000..b925200 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_item_selected_dark.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_nav_back.xml b/widget/src/main/res/drawable-xhdpi/md_nav_back.xml new file mode 100644 index 0000000..a95494b --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_nav_back.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_selector.xml b/widget/src/main/res/drawable-xhdpi/md_selector.xml new file mode 100644 index 0000000..df3322b --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_selector_dark.xml b/widget/src/main/res/drawable-xhdpi/md_selector_dark.xml new file mode 100644 index 0000000..8c599e9 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_selector_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_transparent.xml b/widget/src/main/res/drawable-xhdpi/md_transparent.xml new file mode 100644 index 0000000..261a644 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_transparent.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/up.png b/widget/src/main/res/drawable-xhdpi/up.png new file mode 100644 index 0000000000000000000000000000000000000000..e11e1dfaf3a4149925d25c18dd766553de506570 GIT binary patch literal 6709 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 + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/bg_square_voice.xml b/widget/src/main/res/drawable/bg_square_voice.xml new file mode 100644 index 0000000..d3e07e8 --- /dev/null +++ b/widget/src/main/res/drawable/bg_square_voice.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/widget/src/main/res/drawable/bg_svprogresshuddefault.xml b/widget/src/main/res/drawable/bg_svprogresshuddefault.xml new file mode 100644 index 0000000..2bcad62 --- /dev/null +++ b/widget/src/main/res/drawable/bg_svprogresshuddefault.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/color_cursor.xml b/widget/src/main/res/drawable/color_cursor.xml new file mode 100644 index 0000000..d7bb3b3 --- /dev/null +++ b/widget/src/main/res/drawable/color_cursor.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/mis_action_btn.xml b/widget/src/main/res/drawable/mis_action_btn.xml new file mode 100644 index 0000000..dcd2cbb --- /dev/null +++ b/widget/src/main/res/drawable/mis_action_btn.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/progressbar.xml b/widget/src/main/res/drawable/progressbar.xml new file mode 100644 index 0000000..da267ee --- /dev/null +++ b/widget/src/main/res/drawable/progressbar.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/progressloading.xml b/widget/src/main/res/drawable/progressloading.xml new file mode 100644 index 0000000..2bdaeb6 --- /dev/null +++ b/widget/src/main/res/drawable/progressloading.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/radios.xml b/widget/src/main/res/drawable/radios.xml new file mode 100644 index 0000000..5471d56 --- /dev/null +++ b/widget/src/main/res/drawable/radios.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/rectangle.xml b/widget/src/main/res/drawable/rectangle.xml new file mode 100644 index 0000000..9b39d49 --- /dev/null +++ b/widget/src/main/res/drawable/rectangle.xml @@ -0,0 +1,30 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/rectangle_pop_bg.xml b/widget/src/main/res/drawable/rectangle_pop_bg.xml new file mode 100644 index 0000000..c25c9d7 --- /dev/null +++ b/widget/src/main/res/drawable/rectangle_pop_bg.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/selector_btn_back_gray.xml b/widget/src/main/res/drawable/selector_btn_back_gray.xml new file mode 100644 index 0000000..16f32be --- /dev/null +++ b/widget/src/main/res/drawable/selector_btn_back_gray.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/top_radio.xml b/widget/src/main/res/drawable/top_radio.xml new file mode 100644 index 0000000..6a883bf --- /dev/null +++ b/widget/src/main/res/drawable/top_radio.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-ldrtl/md_listitem_multichoice.xml b/widget/src/main/res/layout-ldrtl/md_listitem_multichoice.xml new file mode 100644 index 0000000..aec2f16 --- /dev/null +++ b/widget/src/main/res/layout-ldrtl/md_listitem_multichoice.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml b/widget/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml new file mode 100644 index 0000000..270550d --- /dev/null +++ b/widget/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/md_stub_progress.xml b/widget/src/main/res/layout-v14/md_stub_progress.xml new file mode 100644 index 0000000..a469811 --- /dev/null +++ b/widget/src/main/res/layout-v14/md_stub_progress.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/md_stub_progress_indeterminate.xml b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate.xml new file mode 100644 index 0000000..1e780c1 --- /dev/null +++ b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml new file mode 100644 index 0000000..a4ef9b2 --- /dev/null +++ b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/mis_fragment_multi_image.xml b/widget/src/main/res/layout-v14/mis_fragment_multi_image.xml new file mode 100644 index 0000000..0847d14 --- /dev/null +++ b/widget/src/main/res/layout-v14/mis_fragment_multi_image.xml @@ -0,0 +1,47 @@ + + + + + + + +