├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── qw │ │ └── curtain │ │ └── sample │ │ ├── AdapterViewActivity.java │ │ ├── CurtainFlowGuideActivity.java │ │ ├── MainActivity.java │ │ ├── RecyclerViewActivity.java │ │ ├── SimpleGuideActivity.java │ │ ├── adapter │ │ └── BaseArrayAdapter.java │ │ └── fragment │ │ ├── GirdViewFragment.java │ │ └── ListViewFragment.java │ └── res │ ├── drawable-v21 │ ├── ic_menu_camera.xml │ ├── ic_menu_gallery.xml │ ├── ic_menu_manage.xml │ ├── ic_menu_send.xml │ ├── ic_menu_share.xml │ └── ic_menu_slideshow.xml │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ ├── selector_shape_blue.xml │ ├── selector_shape_test.xml │ ├── shape_circle_green.xml │ ├── shape_pill_white_hollow.xml │ ├── shape_round_red.xml │ ├── shape_round_red_deep.xml │ └── side_nav_bar.xml │ ├── layout │ ├── activity_array_guide.xml │ ├── activity_main.xml │ ├── activity_simple_guide.xml │ ├── app_bar_main.xml │ ├── content_main.xml │ ├── item_grid_view.xml │ ├── item_list_view.xml │ ├── nav_header_main.xml │ ├── nav_left_main.xml │ ├── view_guide_1.xml │ ├── view_guide_2.xml │ ├── view_guide_flow1.xml │ ├── view_guide_flow2.xml │ └── view_guide_flow3.xml │ ├── menu │ └── main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ ├── ic_launcher_round.png │ └── ic_virtual_line.png │ ├── mipmap-xxxhdpi │ ├── ic_image.webp │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── values-v21 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── drawables.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── curtain ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── qw │ │ └── curtain │ │ └── lib │ │ ├── Constance.java │ │ ├── Curtain.java │ │ ├── CurtainFlow.java │ │ ├── GuideDialogFragment.java │ │ ├── GuideView.java │ │ ├── HollowInfo.java │ │ ├── IGuide.java │ │ ├── InnerUtils.java │ │ ├── OnViewInTopClickListener.java │ │ ├── Padding.java │ │ ├── ViewGetter.java │ │ ├── debug │ │ └── CurtainDebug.java │ │ ├── dialog │ │ ├── NoInterceptActivityDialog.java │ │ └── NoInterceptViewAlertDialog.java │ │ ├── flow │ │ └── CurtainFlowInterface.java │ │ └── shape │ │ ├── CircleShape.java │ │ ├── RoundShape.java │ │ └── Shape.java │ └── res │ ├── anim │ ├── alpha_in.xml │ └── alpha_out.xml │ └── values │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Windows thumbnail db 37 | Thumbs.db 38 | 39 | # OSX files 40 | .DS_Store 41 | 42 | # Eclipse project files 43 | .classpath 44 | .project 45 | 46 | # Android Studio 47 | *.iml 48 | .idea 49 | 50 | #NDK 51 | obj/ 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Curtain 2 | [![Hex.pm](https://img.shields.io/badge/download-0.3.0-green)](https://bintray.com/beta/#/soulqw/AndroidFrame/curtain?tab=overview) 3 | [![Hex.pm](https://img.shields.io/badge/Jetpack-AndroidX-orange)]() 4 | [![Hex.pm](https://img.shields.io/hexpm/l/plug.svg)](https://www.apache.org/licenses/LICENSE-2.0) 5 | #### 一个更简洁好用的高亮蒙层库: 6 | - 一行代码完成某个View,或者多个View的高亮展示 7 | - 支持基于AapterView(如ListView、GridView) 和RecyclerView 的item以及item中元素的高亮 8 | - 自动识别圆角背景,也可以自定义高亮形状 9 | - 高亮区域支持自定义大小、操作灵活 10 | - 顺应变化,基于Android X 11 | - 配置简单,导入方便 12 | 13 | ![image](https://img-blog.csdnimg.cn/20191009181206920.png) 14 | 15 | ## Installation: 16 | 17 | ```java 18 | dependencies { 19 | implementation 'com.github.soulqw:Curtain:0.3.0' 20 | } 21 | 22 | ``` 23 | ## Usage: 24 | - 仅仅是高亮某个View 25 | ```java 26 | private void showCurtain(){ 27 | new Curtain(MainActivity.this) 28 | .with(findViewById(R.id.textView)) 29 | .show(); 30 | } 31 | ``` 32 | ![image](https://upload-images.jianshu.io/upload_images/11595074-8647d1dd531f225e.png) 33 | 34 | - 如果你希望那个view的蒙层区域更大一些: 35 | 36 | ```java 37 | private void showCurtain(){ 38 | new Curtain(MainActivity.this) 39 | .with(findViewById(R.id.textView)) 40 | .withPadding(findViewById(R.id.textView),Padding.all(10)) 41 | .show(); 42 | } 43 | 44 | ``` 45 | - 也可以同时高亮多个View: 46 | 47 | ```java 48 | private void showCurtain(){ 49 | new Curtain(MainActivity.this) 50 | .with(findViewById(R.id.textView)) 51 | .with(findViewById(R.id.imageView)) 52 | .show(); 53 | } 54 | ``` 55 | - 如果你在蒙层上加上一些其他的元素,可以额外传入View布局: 56 | 57 | ```java 58 | private void showCurtain(){ 59 | new Curtain(MainActivity.this) 60 | .with(findViewById(R.id.textView)) 61 | .setTopView(R.layout.nav_header_main) 62 | .show(); 63 | } 64 | ``` 65 | ![image](https://upload-images.jianshu.io/upload_images/11595074-35d1f98e309d52de.gif) 66 | 67 | - 如果你想监听蒙层的展示或者消失的回调: 68 | 69 | ```java 70 | private void showCurtain(){ 71 | new Curtain(MainActivity.this) 72 | .with(findViewById(R.id.imageView)) 73 | .setCallBack(new Curtain.CallBack() { 74 | @Override 75 | public void onShow(IGuide iGuide) { 76 | 77 | } 78 | 79 | @Override 80 | public void onDismiss(IGuide iGuide) { 81 | 82 | } 83 | }).show(); 84 | } 85 | ``` 86 | - 默认会识别View的背景而生成相关高亮区域的形状,也可以自定形状: 87 | 88 | ```java 89 | private void showThirdGuide() { 90 | new Curtain(SimpleGuideActivity.this) 91 | .with(findViewById(R.id.btn_shape_custom)) 92 | //圆角 93 | .withShape(findViewById(R.id.btn_shape_custom), new RoundShape(12)) 94 | //椭圆形 95 | // .withShape(findViewById(R.id.btn_shape_custom),new CircleShape()) 96 | // 也可继承自 Shape 自己实现形状 97 | // .withShape(findViewById(R.id.btn_shape_custom), new Shape() { 98 | // @Override 99 | // public void drawShape(Canvas canvas, Paint paint, HollowInfo info) { 100 | //draw your shape here 101 | // } 102 | // }) 103 | .show(); 104 | } 105 | ``` 106 | - 在ListView 或者GridView 中使用: 107 | ```java 108 | /** 109 | * 高亮item 110 | */ 111 | private void showGuideInItem() { 112 | View item1 = ViewGetter.getFromAdapterView(listView, 5); 113 | View item2 = ViewGetter.getFromAdapterView(listView, 2); 114 | //如果你的View的位置不在屏幕中,返回值为null 需要判空处理 115 | if (null == item1 || null == item2) { 116 | return; 117 | } 118 | new Curtain(this) 119 | .with(item1) 120 | .with(item2) 121 | .show(); 122 | } 123 | 124 | /** 125 | * 高亮item中的元素 126 | */ 127 | private void showGuideInItemChild() { 128 | View item1 = ViewGetter.getFromAdapterView(listView, 1); 129 | View item2 = ViewGetter.getFromAdapterView(listView, 3); 130 | //如果你的View的位置不在屏幕中,返回值为null 需要判空处理 131 | if (null == item1 || null == item2) { 132 | return; 133 | } 134 | new Curtain(this) 135 | .withShape(item1.findViewById(R.id.image), new CircleShape()) 136 | .with(item2.findViewById(R.id.tv_text)) 137 | .show(); 138 | } 139 | ``` 140 | 效果: 141 | 142 | ![image](https://upload-images.jianshu.io/upload_images/11595074-3c8fc50488da539b.gif) 143 | 144 | - 其他一些功能介绍: 145 | 146 | ```java 147 | private void showInitGuide() { 148 | new Curtain(SimpleGuideActivity.this) 149 | .with(findViewById(R.id.iv_guide_first)) 150 | .with(findViewById(R.id.btn_shape_circle)) 151 | .with(findViewById(R.id.btn_shape_custom)) 152 | //自定义高亮形状 153 | .withShape(findViewById(R.id.btn_shape_custom), new RoundShape(12)) 154 | //自定义高亮形状的Padding 155 | // .withPadding(findViewById(R.id.btn_shape_custom), Padding.only(30,20)) 156 | .withPadding(findViewById(R.id.btn_shape_custom), Padding.all(10)) 157 | .setTopView(R.layout.view_guide_1) 158 | // .setNoCurtainAnimation(true) 159 | //如果你希望高亮的目标View仍然可以响应touch事件的话(默认true) 160 | .setInterceptTargetView(false) 161 | //如果你不希望Curtain拦截蒙层之下的事件的话(默认true) 162 | // .setInterceptTouchEvent(false) 163 | //add onclick listener in the top view 164 | .addOnTopViewClickListener(R.id.tv_i_know, new OnViewInTopClickListener() { 165 | @Override 166 | public void onClick(View current, IGuide currentHost) { 167 | //close the 168 | currentHost.dismissGuide(); 169 | } 170 | }) 171 | .show(); 172 | } 173 | ``` 174 | #### CurtainFlow 175 | 176 | 如果你想按照一定的顺序去高亮一些列的View,可以方便的管理前进后退,减少方法的嵌套的场景下推荐使用: 177 | 1. 仅仅需要按照步骤的Id,和构建你想要高亮的Curtain对象,统一交给CurtianFlow来处理 178 | 179 | ```java 180 | private void showInitGuide() { 181 | new CurtainFlow.Builder() 182 | .with(ID_STEP_1, getStepOneGuide()) 183 | .with(ID_STEP_2, getStepTwoGuide()) 184 | .with(ID_STEP_3, getStepThreeGuide()) 185 | .create() 186 | .start(new CurtainFlow.CallBack() { 187 | @Override 188 | public void onProcess(int currentId, final CurtainFlowInterface curtainFlow) { 189 | switch (currentId) { 190 | case ID_STEP_2: 191 | //回到上个 192 | curtainFlow.findViewInCurrentCurtain(R.id.tv_to_last) 193 | .setOnClickListener(new View.OnClickListener() { 194 | @Override 195 | public void onClick(View v) { 196 | curtainFlow.pop(); 197 | } 198 | }); 199 | break; 200 | case ID_STEP_3: 201 | curtainFlow.findViewInCurrentCurtain(R.id.tv_to_last) 202 | .setOnClickListener(new View.OnClickListener() { 203 | @Override 204 | public void onClick(View v) { 205 | curtainFlow.pop(); 206 | } 207 | }); 208 | //重新来一遍,即回到第一步 209 | curtainFlow.findViewInCurrentCurtain(R.id.tv_retry) 210 | .setOnClickListener(new View.OnClickListener() { 211 | @Override 212 | public void onClick(View v) { 213 | curtainFlow.toCurtainById(ID_STEP_1); 214 | } 215 | }); 216 | break; 217 | } 218 | //去下一个 219 | curtainFlow.findViewInCurrentCurtain(R.id.tv_to_next) 220 | .setOnClickListener(new View.OnClickListener() { 221 | @Override 222 | public void onClick(View v) { 223 | curtainFlow.push(); 224 | } 225 | }); 226 | } 227 | }); 228 | } 229 | ``` 230 | 2.效果 231 | 232 | ![image](https://upload-images.jianshu.io/upload_images/11595074-36db1fcb908deea8.gif) 233 | 234 | 235 | 3. APi细节上可以参考Demo 236 | 237 | [设计原理详解](https://blog.csdn.net/u014626094/article/details/105430981) 238 | 239 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Windows thumbnail db 37 | Thumbs.db 38 | 39 | # OSX files 40 | .DS_Store 41 | 42 | # Eclipse project files 43 | .classpath 44 | .project 45 | 46 | # Android Studio 47 | *.iml 48 | .idea 49 | 50 | #NDK 51 | obj/ 52 | 53 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 30 5 | defaultConfig { 6 | applicationId "com.qw.curtain.sample" 7 | minSdkVersion 17 8 | targetSdkVersion 30 9 | versionCode 4 10 | versionName "1.3" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | namespace 'com.qw.curtain.sample' 20 | } 21 | 22 | dependencies { 23 | implementation 'androidx.appcompat:appcompat:1.2.0' 24 | implementation 'com.google.android.material:material:1.3.0' 25 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 26 | implementation 'androidx.recyclerview:recyclerview:1.2.0' 27 | // implementation project(':curtain') 28 | implementation 'com.github.soulqw:Curtain:0.3.0' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 30 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulqw/Curtain/ba45b24eedebc617fbc62d5bbdfbcddcf66ba048/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/AdapterViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | 6 | import androidx.annotation.Nullable; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | import com.qw.curtain.sample.fragment.GirdViewFragment; 10 | import com.qw.curtain.sample.fragment.ListViewFragment; 11 | 12 | public class AdapterViewActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(@Nullable Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_array_guide); 18 | showList(null); 19 | } 20 | 21 | public void showList(View view) { 22 | getSupportFragmentManager() 23 | .beginTransaction() 24 | .replace(R.id.root, new ListViewFragment()) 25 | .commitAllowingStateLoss(); 26 | } 27 | 28 | public void showGird(View view) { 29 | getSupportFragmentManager() 30 | .beginTransaction() 31 | .replace(R.id.root, new GirdViewFragment()) 32 | .commitAllowingStateLoss(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/CurtainFlowGuideActivity.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.Toast; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import androidx.appcompat.widget.Toolbar; 9 | 10 | import com.qw.curtain.lib.Curtain; 11 | import com.qw.curtain.lib.CurtainFlow; 12 | import com.qw.curtain.lib.flow.CurtainFlowInterface; 13 | import com.qw.curtain.lib.shape.RoundShape; 14 | 15 | public class CurtainFlowGuideActivity extends AppCompatActivity { 16 | 17 | /** 18 | * 第一步 高亮一个View 19 | */ 20 | private static final int ID_STEP_1 = 1; 21 | 22 | /** 23 | * 第二步 高亮一个带圆形的View 24 | */ 25 | private static final int ID_STEP_2 = 2; 26 | 27 | /** 28 | * 第三步 为一个View指定自定义的透明形状 29 | */ 30 | private static final int ID_STEP_3 = 3; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.app_bar_main); 36 | Toolbar toolbar = findViewById(R.id.toolbar); 37 | setSupportActionBar(toolbar); 38 | //first guide 39 | showInitGuide(); 40 | findViewById(R.id.btn_open_left).setVisibility(View.GONE); 41 | } 42 | 43 | private void showInitGuide() { 44 | new CurtainFlow.Builder() 45 | .with(ID_STEP_1, getStepOneGuide()) 46 | .with(ID_STEP_2, getStepTwoGuide()) 47 | .with(ID_STEP_3, getStepThreeGuide()) 48 | .create() 49 | .start(new CurtainFlow.CallBack() { 50 | @Override 51 | public void onProcess(int currentId, final CurtainFlowInterface curtainFlow) { 52 | switch (currentId) { 53 | case ID_STEP_2: 54 | //回到上个 55 | curtainFlow.findViewInCurrentCurtain(R.id.tv_to_last) 56 | .setOnClickListener(new View.OnClickListener() { 57 | @Override 58 | public void onClick(View v) { 59 | curtainFlow.pop(); 60 | } 61 | }); 62 | break; 63 | case ID_STEP_3: 64 | curtainFlow.findViewInCurrentCurtain(R.id.tv_to_last) 65 | .setOnClickListener(new View.OnClickListener() { 66 | @Override 67 | public void onClick(View v) { 68 | curtainFlow.pop(); 69 | } 70 | }); 71 | //重新来一遍,即回到第一步 72 | curtainFlow.findViewInCurrentCurtain(R.id.tv_retry) 73 | .setOnClickListener(new View.OnClickListener() { 74 | @Override 75 | public void onClick(View v) { 76 | curtainFlow.toCurtainById(ID_STEP_1); 77 | } 78 | }); 79 | break; 80 | } 81 | //去下一个 82 | curtainFlow.findViewInCurrentCurtain(R.id.tv_to_next) 83 | .setOnClickListener(new View.OnClickListener() { 84 | @Override 85 | public void onClick(View v) { 86 | curtainFlow.push(); 87 | } 88 | }); 89 | } 90 | 91 | @Override 92 | public void onFinish() { 93 | Toast.makeText(CurtainFlowGuideActivity.this, "all flow ended", Toast.LENGTH_SHORT).show(); 94 | } 95 | }); 96 | } 97 | 98 | private Curtain getStepOneGuide() { 99 | return new Curtain(CurtainFlowGuideActivity.this) 100 | .with(findViewById(R.id.iv_guide_first)) 101 | .setNoCurtainAnimation(true) 102 | .setTopView(R.layout.view_guide_flow1); 103 | } 104 | 105 | private Curtain getStepTwoGuide() { 106 | return new Curtain(CurtainFlowGuideActivity.this) 107 | .with(findViewById(R.id.btn_shape_circle)) 108 | .setNoCurtainAnimation(true) 109 | .setTopView(R.layout.view_guide_flow2); 110 | } 111 | 112 | private Curtain getStepThreeGuide() { 113 | return new Curtain(CurtainFlowGuideActivity.this) 114 | //自定义高亮形状 115 | .withShape(findViewById(R.id.btn_shape_custom), new RoundShape(12)) 116 | //自定义高亮形状的Padding 117 | .withPadding(findViewById(R.id.btn_shape_custom), 24) 118 | .setNoCurtainAnimation(true) 119 | .setTopView(R.layout.view_guide_flow3); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | public class MainActivity extends AppCompatActivity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_main); 15 | } 16 | 17 | public void showViewGuide(View view) { 18 | startActivity(new Intent(this, SimpleGuideActivity.class)); 19 | } 20 | 21 | public void showAdapterViewGuide(View view) { 22 | startActivity(new Intent(this, AdapterViewActivity.class)); 23 | } 24 | 25 | public void showRecyclerViewGuide(View view) { 26 | startActivity(new Intent(this, RecyclerViewActivity.class)); 27 | } 28 | 29 | /** 30 | * 在复杂多个引导的情况下推荐使用可有效减少方法嵌套 31 | * 32 | */ 33 | public void curtainFlow(View view) { 34 | startActivity(new Intent(this, CurtainFlowGuideActivity.class)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/RecyclerViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.recyclerview.widget.LinearLayoutManager; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | import com.qw.curtain.lib.Curtain; 16 | import com.qw.curtain.lib.ViewGetter; 17 | import com.qw.curtain.lib.shape.CircleShape; 18 | 19 | import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE; 20 | 21 | public class RecyclerViewActivity extends AppCompatActivity { 22 | 23 | RecyclerView recyclerView; 24 | 25 | @Override 26 | protected void onCreate(@Nullable Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | recyclerView = new RecyclerView(this); 29 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 30 | recyclerView.setAdapter(new RecyclerAdapter()); 31 | recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 32 | @Override 33 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 34 | super.onScrollStateChanged(recyclerView, newState); 35 | if (newState == SCROLL_STATE_IDLE) { 36 | showGuideInItemChild(); 37 | } 38 | } 39 | }); 40 | setContentView(recyclerView); 41 | //如果需要在初始化的时候调用 请用post 42 | recyclerView.post(new Runnable() { 43 | @Override 44 | public void run() { 45 | showGuideInItem(); 46 | } 47 | }); 48 | } 49 | 50 | private void showGuideInItem() { 51 | View item1 = ViewGetter.getFromRecyclerView(recyclerView, 1); 52 | View item2 = ViewGetter.getFromRecyclerView(recyclerView, 4); 53 | if (null == item1 || null == item2) { 54 | return; 55 | } 56 | new Curtain(this) 57 | .with(item1) 58 | .with(item2) 59 | .show(); 60 | } 61 | 62 | private void showGuideInItemChild() { 63 | View item1 = ViewGetter.getFromRecyclerView(recyclerView, 2); 64 | View item2 = ViewGetter.getFromRecyclerView(recyclerView, 5); 65 | if (null == item1 || null == item2) { 66 | return; 67 | } 68 | new Curtain(this) 69 | .withShape(item1.findViewById(R.id.image), new CircleShape()) 70 | .with(item2.findViewById(R.id.tv_text)) 71 | .show(); 72 | } 73 | 74 | public class RecyclerAdapter extends RecyclerView.Adapter { 75 | 76 | String[] data = {"this", "is", "a", "better", "guide", "view", "generate", "lib", ".", "for", "android", "more", "and", "more", "useful", "to", "simplify", "your", "coding", ".", "have", "a", "nice", "day", "~"}; 77 | 78 | @NonNull 79 | @Override 80 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 81 | View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_view, parent, false); 82 | return new MyRecyclerHolder(itemView); 83 | } 84 | 85 | @Override 86 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 87 | ((TextView) (holder.itemView.findViewById(R.id.tv_text))).setText(data[position]); 88 | } 89 | 90 | @Override 91 | public int getItemCount() { 92 | return data.length; 93 | } 94 | } 95 | 96 | public class MyRecyclerHolder extends RecyclerView.ViewHolder { 97 | 98 | MyRecyclerHolder(@NonNull View itemView) { 99 | super(itemView); 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/SimpleGuideActivity.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample; 2 | 3 | import android.os.Bundle; 4 | import android.util.Log; 5 | import android.view.View; 6 | import android.widget.Toast; 7 | 8 | import androidx.appcompat.app.ActionBarDrawerToggle; 9 | import androidx.appcompat.app.AppCompatActivity; 10 | import androidx.appcompat.widget.Toolbar; 11 | import androidx.core.view.GravityCompat; 12 | import androidx.drawerlayout.widget.DrawerLayout; 13 | 14 | import com.qw.curtain.lib.Curtain; 15 | import com.qw.curtain.lib.IGuide; 16 | import com.qw.curtain.lib.OnViewInTopClickListener; 17 | import com.qw.curtain.lib.Padding; 18 | import com.qw.curtain.lib.shape.RoundShape; 19 | 20 | public class SimpleGuideActivity extends AppCompatActivity { 21 | 22 | private static String TAG = "SimpleGuideActivity"; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_simple_guide); 28 | Toolbar toolbar = findViewById(R.id.toolbar); 29 | setSupportActionBar(toolbar); 30 | final DrawerLayout drawer = findViewById(R.id.drawer_layout); 31 | ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( 32 | this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); 33 | drawer.addDrawerListener(toggle); 34 | drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { 35 | @Override 36 | public void onDrawerOpened(View drawerView) { 37 | showGuideLeft(); 38 | } 39 | }); 40 | toggle.syncState(); 41 | findViewById(R.id.btn_open_left).setOnClickListener(new View.OnClickListener() { 42 | @Override 43 | public void onClick(View v) { 44 | drawer.openDrawer(GravityCompat.START); 45 | } 46 | }); 47 | //first guide 48 | showInitGuide(); 49 | 50 | findViewById(R.id.btn_shape_custom).setOnClickListener(new View.OnClickListener() { 51 | @Override 52 | public void onClick(View v) { 53 | Toast.makeText(v.getContext(), "custom click", Toast.LENGTH_SHORT).show(); 54 | } 55 | }); 56 | } 57 | 58 | /** 59 | * 高亮图片 60 | * 高亮圆形文字,自动识别圆形背景 61 | * 高亮自定义按钮,形状自定圆角程度 62 | */ 63 | private void showInitGuide() { 64 | new Curtain(SimpleGuideActivity.this) 65 | .with(findViewById(R.id.iv_guide_first)) 66 | .with(findViewById(R.id.btn_shape_circle)) 67 | .with(findViewById(R.id.btn_shape_custom)) 68 | //自定义高亮形状 69 | .withShape(findViewById(R.id.btn_shape_custom), new RoundShape(12)) 70 | //自定义高亮形状的Padding 71 | // .withPadding(findViewById(R.id.btn_shape_custom), Padding.only(30,20)) 72 | .withPadding(findViewById(R.id.btn_shape_custom), Padding.all(10)) 73 | .setTopView(R.layout.view_guide_1) 74 | // .setNoCurtainAnimation(true) 75 | //如果你希望高亮的目标View仍然可以响应touch事件的话(默认true) 76 | .setInterceptTargetView(false) 77 | //offset 78 | // .withOffset(findViewById(R.id.btn_shape_circle),20, HollowInfo.HORIZONTAL) 79 | //如果你不希望Curtain拦截蒙层之下的事件的话(默认true) 80 | // .setInterceptTouchEvent(false) 81 | //add onclick listener in the top view 82 | .addOnTopViewClickListener(R.id.tv_i_know, new OnViewInTopClickListener() { 83 | @Override 84 | public void onClick(View current, IGuide currentHost) { 85 | //close the 86 | currentHost.dismissGuide(); 87 | } 88 | }) 89 | .setCallBack(new Curtain.CallBack() { 90 | @Override 91 | public void onShow(IGuide curtain) { 92 | //get top view when curtain showed 93 | Log.d(TAG, "on guide show: " + curtain.getCurrentTopView() 94 | + "\n and size is" + curtain.getCurrentTopView().getWidth() + " " + curtain.getCurrentTopView().getHeight()); 95 | } 96 | 97 | @Override 98 | public void onDismiss(IGuide iGuide) { 99 | 100 | } 101 | }) 102 | .show(); 103 | } 104 | 105 | 106 | private void showThirdGuide() { 107 | new Curtain(SimpleGuideActivity.this) 108 | .with(findViewById(R.id.btn_open_left)) 109 | .setTopView(R.layout.view_guide_2) 110 | .addOnTopViewClickListener(R.id.tv_i_know, new OnViewInTopClickListener() { 111 | @Override 112 | public void onClick(View current, IGuide currentHost) { 113 | currentHost.dismissGuide(); 114 | } 115 | }).show(); 116 | } 117 | 118 | private void showGuideLeft() { 119 | new Curtain(SimpleGuideActivity.this) 120 | .with(findViewById(R.id.textView)) 121 | .withPadding(findViewById(R.id.textView), 8) 122 | .with(findViewById(R.id.rv)) 123 | .show(); 124 | } 125 | 126 | @Override 127 | public void onBackPressed() { 128 | DrawerLayout drawer = findViewById(R.id.drawer_layout); 129 | if (drawer.isDrawerOpen(GravityCompat.START)) { 130 | drawer.closeDrawer(GravityCompat.START); 131 | } else { 132 | super.onBackPressed(); 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/adapter/BaseArrayAdapter.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample.adapter; 2 | 3 | import android.widget.BaseAdapter; 4 | 5 | public abstract class BaseArrayAdapter extends BaseAdapter { 6 | 7 | protected String[] data = {"this", "is", "a", "better", "guide", "view", "generate", "lib", ".", "for", "android", "more", "and", "more", "useful", "to", "simplify", "your", "coding", ".", "have", "a", "nice", "day", "~"}; 8 | 9 | @Override 10 | public int getCount() { 11 | return data.length; 12 | } 13 | 14 | @Override 15 | public Object getItem(int position) { 16 | return data[position]; 17 | } 18 | 19 | @Override 20 | public long getItemId(int position) { 21 | return position; 22 | } 23 | 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/fragment/GirdViewFragment.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.AbsListView; 8 | import android.widget.GridView; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import androidx.fragment.app.Fragment; 14 | 15 | import com.qw.curtain.lib.Curtain; 16 | import com.qw.curtain.lib.ViewGetter; 17 | import com.qw.curtain.lib.shape.CircleShape; 18 | import com.qw.curtain.sample.R; 19 | import com.qw.curtain.sample.adapter.BaseArrayAdapter; 20 | 21 | public class GirdViewFragment extends Fragment { 22 | 23 | private GridView gridView; 24 | 25 | @Nullable 26 | @Override 27 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 28 | gridView = new GridView(container.getContext()); 29 | gridView.setNumColumns(3); 30 | return gridView; 31 | } 32 | 33 | @Override 34 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 35 | super.onActivityCreated(savedInstanceState); 36 | gridView.setAdapter(new GridAdapter()); 37 | gridView.setHorizontalSpacing(4); 38 | gridView.setVerticalSpacing(4); 39 | gridView.setOnScrollListener(new AbsListView.OnScrollListener() { 40 | @Override 41 | public void onScrollStateChanged(AbsListView view, int scrollState) { 42 | if (scrollState == SCROLL_STATE_IDLE) { 43 | showGuideInItemChild(); 44 | } 45 | } 46 | 47 | @Override 48 | public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 49 | } 50 | }); 51 | // 如果需要在初始化的时候调用 请用post 52 | gridView.post(new Runnable() { 53 | @Override 54 | public void run() { 55 | showGuideInItem(); 56 | } 57 | }); 58 | } 59 | 60 | private void showGuideInItem() { 61 | View item1 = ViewGetter.getFromAdapterView(gridView, 3); 62 | View item2 = ViewGetter.getFromAdapterView(gridView, 8); 63 | if (null == item1 || null == item2) { 64 | return; 65 | } 66 | new Curtain(this) 67 | .with(item1) 68 | .with(item2) 69 | .show(); 70 | } 71 | 72 | private void showGuideInItemChild() { 73 | View item1 = ViewGetter.getFromAdapterView(gridView, 4); 74 | View item2 = ViewGetter.getFromAdapterView(gridView, 9); 75 | if (null == item1 || null == item2) { 76 | return; 77 | } 78 | new Curtain(this) 79 | .withShape(item1.findViewById(R.id.image), new CircleShape()) 80 | .with(item2.findViewById(R.id.tv_text)) 81 | .show(); 82 | } 83 | 84 | public class GridAdapter extends BaseArrayAdapter { 85 | 86 | @Override 87 | public View getView(int position, View convertView, ViewGroup parent) { 88 | View result; 89 | if (null != convertView) { 90 | result = convertView; 91 | } else { 92 | result = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_grid_view, parent, false); 93 | } 94 | ((TextView) (result.findViewById(R.id.tv_text))).setText(data[position]); 95 | return result; 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/qw/curtain/sample/fragment/ListViewFragment.java: -------------------------------------------------------------------------------- 1 | package com.qw.curtain.sample.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.AbsListView; 8 | import android.widget.ListView; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import androidx.fragment.app.Fragment; 14 | 15 | import com.qw.curtain.lib.Curtain; 16 | import com.qw.curtain.lib.ViewGetter; 17 | import com.qw.curtain.lib.shape.CircleShape; 18 | import com.qw.curtain.sample.R; 19 | import com.qw.curtain.sample.adapter.BaseArrayAdapter; 20 | 21 | public class ListViewFragment extends Fragment { 22 | 23 | private ListView listView; 24 | 25 | @Nullable 26 | @Override 27 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 28 | listView = new ListView(container.getContext()); 29 | return listView; 30 | } 31 | 32 | @Override 33 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 34 | super.onActivityCreated(savedInstanceState); 35 | listView.setAdapter(new ListAdapter()); 36 | listView.setOnScrollListener(new AbsListView.OnScrollListener() { 37 | @Override 38 | public void onScrollStateChanged(AbsListView view, int scrollState) { 39 | if (scrollState == SCROLL_STATE_IDLE) { 40 | showGuideInItemChild(); 41 | } 42 | } 43 | 44 | @Override 45 | public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 46 | } 47 | }); 48 | // 如果需要在初始化的时候调用 请用post 49 | listView.post(new Runnable() { 50 | @Override 51 | public void run() { 52 | showGuideInItem(); 53 | } 54 | }); 55 | } 56 | 57 | private void showGuideInItem() { 58 | View item1 = ViewGetter.getFromAdapterView(listView, 5); 59 | View item2 = ViewGetter.getFromAdapterView(listView, 2); 60 | if (null == item1 || null == item2) { 61 | return; 62 | } 63 | new Curtain(this) 64 | .with(item1) 65 | .with(item2) 66 | .show(); 67 | } 68 | 69 | private void showGuideInItemChild() { 70 | View item1 = ViewGetter.getFromAdapterView(listView, 1); 71 | View item2 = ViewGetter.getFromAdapterView(listView, 3); 72 | if (null == item1 || null == item2) { 73 | return; 74 | } 75 | new Curtain(this) 76 | .withShape(item1.findViewById(R.id.image), new CircleShape()) 77 | .with(item2.findViewById(R.id.tv_text)) 78 | .show(); 79 | } 80 | 81 | public class ListAdapter extends BaseArrayAdapter { 82 | 83 | @Override 84 | public View getView(int position, View convertView, ViewGroup parent) { 85 | View result; 86 | if (null != convertView) { 87 | result = convertView; 88 | } else { 89 | result = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_view, null); 90 | } 91 | ((TextView) (result.findViewById(R.id.tv_text))).setText(data[position]); 92 | return result; 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_gallery.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_manage.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_menu_slideshow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selector_shape_blue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selector_shape_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_circle_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_pill_white_hollow.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_round_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_round_red_deep.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_array_guide.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 |