├── .gitignore ├── README.MD ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yunlei │ │ └── douyinlike │ │ ├── MainActivity.kt │ │ ├── adapter │ │ ├── HomeViewPagerAdapter.kt │ │ ├── ItemViewPagerAdapter.kt │ │ └── MainViewPagerAdapter.kt │ │ ├── entity │ │ └── EventBusItem.kt │ │ ├── fragment │ │ ├── HomeFragment.kt │ │ ├── InfoFragment.kt │ │ ├── ItemFragment.kt │ │ └── VideoFragment.kt │ │ ├── utils │ │ └── Expand.kt │ │ └── widget │ │ └── LikeLayout.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_add_box_black_24dp.xml │ ├── ic_arrow_back_black_24dp.xml │ ├── ic_launcher_background.xml │ ├── ic_live_tv_black_24dp.xml │ ├── ic_message_black_24dp.xml │ ├── ic_search_black_24dp.xml │ └── ic_share_black_24dp.xml │ ├── layout │ ├── activity_main.xml │ ├── fragment_home.xml │ ├── fragment_info.xml │ ├── fragment_item.xml │ ├── fragment_video.xml │ └── layout_home_title.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_heart.png │ ├── ic_heart_off.png │ ├── ic_heart_on.png │ ├── ic_launcher.png │ ├── ic_launcher_round.png │ ├── image_head_def.png │ ├── image_info_demo.jpg │ └── image_splash.jpg │ ├── mipmap-xxhdpi │ ├── ic_heart.png │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── file_paths.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── likebutton ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── like │ │ ├── CircleView.java │ │ ├── DotsView.java │ │ ├── Icon.java │ │ ├── IconType.java │ │ ├── LikeButton.java │ │ ├── OnAnimationEndListener.java │ │ ├── OnLikeListener.java │ │ └── Utils.java │ └── res │ ├── drawable │ ├── heart_off.png │ ├── heart_on.png │ ├── star_off.png │ ├── star_on.png │ ├── thumb_off.png │ └── thumb_on.png │ ├── layout │ └── likeview.xml │ └── values │ ├── attrs.xml │ └── strings.xml ├── screenshot ├── 1.gif ├── 2.gif ├── 3.gif ├── 4.gif └── 5.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # TouYin 仿抖音APP视频切换和点赞效果 2 | 3 | ### 1、ViewPager2 4 | 5 | 网上很多仿抖音视频切换的很多都是使用自定义竖方向的ViewPager或者使用RecyclerView+PagerSnapHelper实现。 6 | 但是这两种方式其实都有一定的缺陷: 7 | 8 | 1、但是ViewPager实现,生命周期有一定问题(ps:FragmentPagerAdapter在新版本中提供了BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT来解决这个问题),notifyDataSetChanged支持很不好。关于生命周期的更新可以参考[androidx中的Fragment懒加载方案](https://blog.csdn.net/qq_36486247/article/details/102531304) 9 | 10 | 2、而使用RecyclerView+PagerSnapHelper实现,需要自己处理生命周期。 11 | 12 | 其实我们可以使用androidx中提供的ViewPage2来实现这个功能。 13 | ``` 14 | dependencies { 15 | implementation "androidx.viewpager2:viewpager2:1.0.0" 16 | } 17 | ``` 18 | 查看源码,ViewPager2继承自ViewGroup,其中发现了三个比较重要的成员变量: 19 | ```java 20 | private LinearLayoutManager mLayoutManager; 21 | RecyclerView mRecyclerView; 22 | private PagerSnapHelper mPagerSnapHelper; 23 | ``` 24 | 明眼人一看就知道了,ViewPager2的核心实现就是**RecyclerView+LinearLayoutManager+PagerSnapHelper**了,因为LinearLayoutManager本身就支持竖向和横向两种布局方式,所以ViewPager2也能很容易地支持这两种滚动方向了,而几乎不需要添加任何多余的代码。 25 | 26 | 使用上和老的ViewPager基本没啥却别,下面ViewPager2的几个新东西: 27 | >* 支持RTL布局 28 | >* 支持竖向滚动 29 | >* 完整支持notifyDataSetChanged 30 | >* FragmentStateAdapter替换了原来的 FragmentStatePagerAdapter 31 | >* RecyclerView.Adapter替换了原来的 PagerAdapter 32 | >* registerOnPageChangeCallback替换了原来的 addPageChangeListener (刚开始使用没看完源码,按照之前的set或者add找了半天,囧) 33 | >* public void setUserInputEnabled(boolean enabled)禁止滑动 34 | >* ... 35 | 36 | 关于更多ViewPager2的资料大家可以自行搜索。 37 | 38 | ### 2、视频播放器 39 | 40 | 本demo中使用的是[GSYVideoPlayer](https://github.com/CarGuo/GSYVideoPlayer),实际项目中可自行选择封装。demo中没有对其进行过多的处理,只是为了看效果,实际的抖音中有更多复杂的东西。 41 | 42 | ### 3、demo结构 43 | 44 | ![](https://github.com/leiyun1993/DouYinLike/raw/master/screenshot/5.png) 45 | 46 | 看最后实现的效果,**只是为了做实验,代码写的比较乱。**(gif有点大,加载比较慢) 47 | 48 | ![](https://github.com/leiyun1993/DouYinLike/raw/master/screenshot/3.gif) 49 | ![](https://github.com/leiyun1993/DouYinLike/raw/master/screenshot/4.gif) 50 | 51 | ### 4、无限上滑 52 | 53 | 借助ViewPager2的监听和adapter的notifyDataSetChanged即可实现 54 | ```kotlin 55 | viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { 56 | override fun onPageSelected(position: Int) { 57 | super.onPageSelected(position) 58 | if (position==urlList.size-1){ 59 | urlList.add(urlList[0]) 60 | mPagerAdapter.notifyDataSetChanged() 61 | } 62 | } 63 | }) 64 | ``` 65 | ### 5、视频和作者主页切换控制 66 | ```kotlin 67 | override fun onResume() { 68 | super.onResume() 69 | if (mCurrentPosition > 0) { 70 | videoPlayer?.onVideoResume(false) 71 | } else { 72 | videoPlayer?.postDelayed({ 73 | videoPlayer?.startPlayLogic() 74 | }, 200) 75 | } 76 | } 77 | override fun onPause() { 78 | super.onPause() 79 | likeLayout?.onPause() 80 | videoPlayer?.onVideoPause() 81 | mCurrentPosition = videoPlayer?.gsyVideoManager?.currentPosition ?: 0 82 | } 83 | ``` 84 | 85 | 86 | ### 6、点赞效果 87 | 88 | 抖音的点赞效果是由右侧的桃心点赞和屏幕的点击构成,右侧的点击后为点赞状态并有点赞动画,再次点击取消点赞。 89 | 90 | 先来看看效果: 91 | 92 | ![](https://github.com/leiyun1993/DouYinLike/raw/master/screenshot/2.gif) 93 | 94 | 此处我们观察效果基本和此前用过的一个三方库比较相似,此处就先又此代替,后面有时间再进行完善。 95 | 该库为[Like Button](https://github.com/jd-alexander/LikeButton),使用方式很简单,如下: 96 | ```xml 97 | 107 | ``` 108 | **LikeButton**具有以下属性,使用时可自行去查看 109 | ```xml 110 | 122 | ``` 123 | 设置**LikeButton**的监听事件: 124 | ```kotlin 125 | likeBtn.setOnLikeListener(object : OnLikeListener { 126 | override fun liked(p0: LikeButton?) { 127 | toast("已点赞~~") 128 | } 129 | 130 | override fun unLiked(p0: LikeButton?) { 131 | toast("取消点赞~~") 132 | } 133 | 134 | }) 135 | ``` 136 | 137 | 我们可以看一下点赞按钮的点赞效果 138 | 139 | ![](https://github.com/leiyun1993/DouYinLike/raw/master/screenshot/1.gif) 140 | 141 | 和抖音的还是有点区别的,后续完善 142 | 143 | ### 7、屏幕点赞 144 | 145 | 分析抖音的屏幕点赞由几个部分组合而成(红心可自绘,也可以直接使用图片) 146 | > 1、刚开始显示的时候,有个由大到小缩放动画 147 | 148 | > 2、透明度变化 149 | 150 | > 3、向上平移 151 | 152 | > 4、由小到大的缩放 153 | 154 | > 5、刚开始显示的时候有个小小的偏移,避免每个红心在同一个位置 155 | 156 | #### 8、实现过程 157 | 获取红心 158 | ```kotlin 159 | private var icon: Drawable = resources.getDrawable(R.mipmap.ic_heart) 160 | ``` 161 | 监听Touch事件,并在按下位置添加View 162 | ```kotlin 163 | override fun onTouchEvent(event: MotionEvent?): Boolean { 164 | if (event?.action == MotionEvent.ACTION_DOWN) { //按下时在Layout中生成红心 165 | val x = event.x 166 | val y = event.y 167 | addHeartView(x, y) 168 | onLikeListener() 169 | } 170 | return super.onTouchEvent(event) 171 | } 172 | ``` 173 | 为红心添加一个随机的偏移(此处为-10~10) 174 | ```kotlin 175 | img.scaleType = ImageView.ScaleType.MATRIX 176 | val matrix = Matrix() 177 | matrix.postRotate(getRandomRotate()) //设置红心的微量偏移 178 | ``` 179 | 设置一开始的缩放动画 180 | ```kotlin 181 | private fun getShowAnimSet(view: ImageView): AnimatorSet { 182 | // 缩放动画 183 | val scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1.2f, 1f) 184 | val scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1.2f, 1f) 185 | val animSet = AnimatorSet() 186 | animSet.playTogether(scaleX, scaleY) 187 | animSet.duration = 100 188 | return animSet 189 | } 190 | ``` 191 | 设置慢慢消失时的动画 192 | ```kotlin 193 | private fun getHideAnimSet(view: ImageView): AnimatorSet { 194 | // 1.alpha动画 195 | val alpha = ObjectAnimator.ofFloat(view, "alpha", 1f, 0.1f) 196 | // 2.缩放动画 197 | val scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 2f) 198 | val scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 2f) 199 | // 3.translation动画 200 | val translation = ObjectAnimator.ofFloat(view, "translationY", 0f, -150f) 201 | val animSet = AnimatorSet() 202 | animSet.playTogether(alpha, scaleX, scaleY, translation) 203 | animSet.duration = 500 204 | return animSet 205 | } 206 | ``` 207 | 设置动画关系,并且在动画结束后Remove该红心 208 | ```kotlin 209 | val animSet = getShowAnimSet(img) 210 | val hideSet = getHideAnimSet(img) 211 | animSet.start() 212 | animSet.addListener(object : AnimatorListenerAdapter() { 213 | override fun onAnimationEnd(animation: Animator?) { 214 | super.onAnimationEnd(animation) 215 | hideSet.start() 216 | } 217 | }) 218 | hideSet.addListener(object : AnimatorListenerAdapter() { 219 | override fun onAnimationEnd(animation: Animator?) { 220 | super.onAnimationEnd(animation) 221 | removeView(img) //动画结束移除红心 222 | } 223 | }) 224 | ``` 225 | 区分单击和多次点击,单击的时候控制视频的暂停和播放,多次点击的时候实现点赞功能 226 | ```kotlin 227 | override fun onTouchEvent(event: MotionEvent?): Boolean { 228 | if (event?.action == MotionEvent.ACTION_DOWN) { //按下时在Layout中生成红心 229 | val x = event.x 230 | val y = event.y 231 | mClickCount++ 232 | mHandler.removeCallbacksAndMessages(null) 233 | if (mClickCount >= 2) { 234 | addHeartView(x, y) 235 | onLikeListener() 236 | mHandler.sendEmptyMessageDelayed(1, 500) 237 | } else { 238 | mHandler.sendEmptyMessageDelayed(0, 500) 239 | } 240 | 241 | } 242 | return true 243 | } 244 | 245 | private fun pauseClick() { 246 | if (mClickCount == 1) { 247 | onPauseListener() 248 | } 249 | mClickCount = 0 250 | } 251 | 252 | fun onPause() { 253 | mClickCount = 0 254 | mHandler.removeCallbacksAndMessages(null) 255 | } 256 | ``` 257 | 258 | 259 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | buildToolsVersion "29.0.2" 10 | defaultConfig { 11 | applicationId "com.yunlei.douyinlike" 12 | minSdkVersion 19 13 | targetSdkVersion 28 14 | versionCode 1 15 | versionName "1.0.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | 18 | ndk { 19 | //设置支持的SO库架构 20 | abiFilters 'armeabi-v7a' 21 | } 22 | } 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation fileTree(dir: 'libs', include: ['*.jar']) 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 34 | implementation "org.jetbrains.anko:anko:$anko_version" 35 | implementation 'androidx.appcompat:appcompat:1.1.0' 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 38 | testImplementation 'junit:junit:4.12' 39 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 41 | implementation project(path: ':likebutton') 42 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 43 | implementation 'com.google.android.material:material:1.2.0-alpha03' 44 | implementation 'com.shuyu:gsyVideoPlayer-java:7.1.2' 45 | implementation 'com.shuyu:gsyVideoPlayer-armv7a:7.1.2' 46 | implementation 'de.hdodenhof:circleimageview:3.0.1' 47 | implementation 'org.greenrobot:eventbus:3.1.1' 48 | // 基础依赖包,必须要依赖 49 | implementation 'com.gyf.immersionbar:immersionbar:3.0.0' 50 | // fragment快速实现(可选) 51 | implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0' 52 | // kotlin扩展(可选) 53 | implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0' 54 | implementation 'androidx.core:core:1.2.0-rc01' 55 | } 56 | -------------------------------------------------------------------------------- /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 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike 2 | 3 | import android.Manifest 4 | import android.content.pm.PackageManager 5 | import android.os.Bundle 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.app.ActivityCompat 9 | import androidx.core.content.ContextCompat 10 | import com.gyf.immersionbar.ImmersionBar 11 | import com.yunlei.douyinlike.adapter.MainViewPagerAdapter 12 | import kotlinx.android.synthetic.main.activity_main.* 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | ImmersionBar.with(this).init() 20 | setContentView(R.layout.activity_main) 21 | viewPager.adapter = MainViewPagerAdapter(this) 22 | viewPager.isUserInputEnabled = false 23 | 24 | 25 | val check = ContextCompat.checkSelfPermission(this, Manifest.permission_group.CALENDAR) 26 | if (check != PackageManager.PERMISSION_GRANTED) { 27 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), 0x99) 28 | } 29 | } 30 | 31 | private fun toast(msg: String) { 32 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/adapter/HomeViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | import com.yunlei.douyinlike.fragment.ItemFragment 6 | 7 | /** 8 | * @author Yun.Lei 9 | * @email waitshan@163.com 10 | * @date 2020/1/18 11 | */ 12 | class HomeViewPagerAdapter(fragment: Fragment, private val list:MutableList) : FragmentStateAdapter(fragment) { 13 | 14 | override fun getItemCount(): Int = list.size 15 | 16 | override fun createFragment(position: Int): Fragment = ItemFragment.getNewInstance(list[position]) 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/adapter/ItemViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | import com.yunlei.douyinlike.fragment.InfoFragment 6 | import com.yunlei.douyinlike.fragment.VideoFragment 7 | 8 | /** 9 | * @author Yun.Lei 10 | * @email waitshan@163.com 11 | * @date 2020/1/18 12 | */ 13 | class ItemViewPagerAdapter(fragment: Fragment, private val url: String) : FragmentStateAdapter(fragment) { 14 | 15 | override fun getItemCount(): Int = 2 16 | 17 | override fun createFragment(position: Int): Fragment = when (position) { 18 | 0 -> VideoFragment.getNewInstance(url) 19 | else -> InfoFragment.getNewInstance(url) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/adapter/MainViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.viewpager2.adapter.FragmentStateAdapter 6 | import com.yunlei.douyinlike.fragment.HomeFragment 7 | 8 | /** 9 | * @author Yun.Lei 10 | * @email waitshan@163.com 11 | * @date 2020/1/18 12 | */ 13 | class MainViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { 14 | 15 | override fun getItemCount(): Int = 1 16 | 17 | override fun createFragment(position: Int): Fragment = HomeFragment.getNewInstance() 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/entity/EventBusItem.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.entity 2 | 3 | 4 | data class ToolbarStateEvent(var isShow: Boolean) 5 | 6 | data class ChangePageEvent(var position: Int) 7 | 8 | data class ClearPositionEvent(var isClear: Boolean) -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/fragment/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.fragment 2 | 3 | 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.viewpager2.widget.ViewPager2 10 | import com.gyf.immersionbar.ImmersionBar 11 | import com.shuyu.gsyvideoplayer.GSYVideoManager 12 | import com.yunlei.douyinlike.R 13 | import com.yunlei.douyinlike.adapter.HomeViewPagerAdapter 14 | import com.yunlei.douyinlike.entity.ClearPositionEvent 15 | import com.yunlei.douyinlike.entity.ToolbarStateEvent 16 | import kotlinx.android.synthetic.main.fragment_home.* 17 | import kotlinx.android.synthetic.main.layout_home_title.* 18 | import org.greenrobot.eventbus.EventBus 19 | import org.greenrobot.eventbus.Subscribe 20 | import org.greenrobot.eventbus.ThreadMode 21 | 22 | /** 23 | * @author Yun.Lei 24 | * @email waitshan@163.com 25 | * @date 2020/1/18 26 | */ 27 | class HomeFragment : Fragment() { 28 | 29 | companion object { 30 | @JvmStatic 31 | fun getNewInstance(): HomeFragment = HomeFragment() 32 | } 33 | 34 | private val urlList = mutableListOf( 35 | "https://chengdu-1259068866.cos.ap-chengdu.myqcloud.com/3f4a65314ae666962dd1870d4d390d56.mp4", 36 | "http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4", 37 | "https://chengdu-1259068866.cos.ap-chengdu.myqcloud.com/b92d429b70dab52b830d99124d908f73.mp4", 38 | // "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4", 39 | "https://chengdu-1259068866.cos.ap-chengdu.myqcloud.com/f2cd4ce84bc6c8948198c93d74856ffb.mp4", 40 | // "http://vfx.mtime.cn/Video/2019/03/19/mp4/190319212559089721.mp4", 41 | "https://chengdu-1259068866.cos.ap-chengdu.myqcloud.com/177d7bcff446ba6670089d0793a5a8df.mp4" 42 | ) 43 | private lateinit var mPagerAdapter: HomeViewPagerAdapter 44 | 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | EventBus.getDefault().register(this) 48 | } 49 | 50 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 51 | savedInstanceState: Bundle?): View? { 52 | // Inflate the layout for this fragment 53 | return inflater.inflate(R.layout.fragment_home, container, false) 54 | } 55 | 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 57 | super.onViewCreated(view, savedInstanceState) 58 | ImmersionBar.with(this).titleBar(toolBar).init() 59 | mPagerAdapter = HomeViewPagerAdapter(this, urlList) 60 | viewPager.adapter = mPagerAdapter 61 | viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { 62 | override fun onPageSelected(position: Int) { 63 | super.onPageSelected(position) 64 | EventBus.getDefault().post(ClearPositionEvent(true)) 65 | if (position==urlList.size-1){ 66 | urlList.add(urlList[0]) 67 | mPagerAdapter.notifyDataSetChanged() 68 | } 69 | } 70 | }) 71 | 72 | } 73 | 74 | override fun onResume() { 75 | super.onResume() 76 | // GSYVideoManager.onResume() 77 | } 78 | 79 | @Subscribe(threadMode = ThreadMode.MAIN) 80 | fun onEventToolbarState(event:ToolbarStateEvent){ 81 | toolBar.visibility = if (event.isShow){ 82 | View.VISIBLE 83 | }else{ 84 | View.GONE 85 | } 86 | } 87 | 88 | override fun onPause() { 89 | super.onPause() 90 | GSYVideoManager.onPause() 91 | } 92 | 93 | 94 | override fun onDestroy() { 95 | EventBus.getDefault().unregister(this) 96 | super.onDestroy() 97 | GSYVideoManager.releaseAllVideos() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/fragment/InfoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.fragment 2 | 3 | 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | 10 | import com.yunlei.douyinlike.R 11 | import com.yunlei.douyinlike.entity.ChangePageEvent 12 | import com.yunlei.douyinlike.entity.ToolbarStateEvent 13 | import kotlinx.android.synthetic.main.fragment_info.* 14 | import org.greenrobot.eventbus.EventBus 15 | import org.jetbrains.anko.bundleOf 16 | 17 | /** 18 | * @author Yun.Lei 19 | * @email waitshan@163.com 20 | * @date 2020/1/18 21 | */ 22 | class InfoFragment : Fragment() { 23 | companion object { 24 | @JvmStatic 25 | fun getNewInstance(url: String): InfoFragment { 26 | val fragment = InfoFragment() 27 | fragment.arguments = bundleOf("url" to url) 28 | return fragment 29 | } 30 | } 31 | 32 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 33 | savedInstanceState: Bundle?): View? { 34 | // Inflate the layout for this fragment 35 | return inflater.inflate(R.layout.fragment_info, container, false) 36 | } 37 | 38 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 39 | super.onViewCreated(view, savedInstanceState) 40 | backBtn.setOnClickListener { 41 | EventBus.getDefault().post(ChangePageEvent(0)) 42 | } 43 | } 44 | 45 | override fun onResume() { 46 | super.onResume() 47 | EventBus.getDefault().post(ToolbarStateEvent(false)) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/fragment/ItemFragment.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.fragment 2 | 3 | 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | 10 | import com.yunlei.douyinlike.R 11 | import com.yunlei.douyinlike.adapter.ItemViewPagerAdapter 12 | import com.yunlei.douyinlike.entity.ChangePageEvent 13 | import kotlinx.android.synthetic.main.fragment_item.* 14 | import org.greenrobot.eventbus.EventBus 15 | import org.greenrobot.eventbus.Subscribe 16 | import org.greenrobot.eventbus.ThreadMode 17 | import org.jetbrains.anko.bundleOf 18 | 19 | /** 20 | * @author Yun.Lei 21 | * @email waitshan@163.com 22 | * @date 2020/1/18 23 | */ 24 | class ItemFragment : Fragment() { 25 | 26 | companion object { 27 | @JvmStatic 28 | fun getNewInstance(url: String): ItemFragment { 29 | val fragment = ItemFragment() 30 | fragment.arguments = bundleOf("url" to url) 31 | return fragment 32 | } 33 | } 34 | 35 | private var url = "" 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | EventBus.getDefault().register(this) 40 | url = arguments?.getString("url") ?: "" 41 | } 42 | 43 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 44 | savedInstanceState: Bundle?): View? { 45 | // Inflate the layout for this fragment 46 | return inflater.inflate(R.layout.fragment_item, container, false) 47 | } 48 | 49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 50 | super.onViewCreated(view, savedInstanceState) 51 | itemViewPager.adapter = ItemViewPagerAdapter(this, url) 52 | } 53 | 54 | @Subscribe(threadMode = ThreadMode.MAIN) 55 | fun onEventChangePage(event:ChangePageEvent){ 56 | itemViewPager.currentItem = event.position 57 | } 58 | 59 | override fun onDestroy() { 60 | super.onDestroy() 61 | EventBus.getDefault().unregister(this) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/fragment/VideoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.fragment 2 | 3 | 4 | import android.os.Bundle 5 | import android.util.Log 6 | import androidx.fragment.app.Fragment 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.View.GONE 10 | import android.view.ViewGroup 11 | import com.shuyu.gsyvideoplayer.GSYVideoManager 12 | 13 | import com.yunlei.douyinlike.R 14 | import com.yunlei.douyinlike.entity.ClearPositionEvent 15 | import com.yunlei.douyinlike.entity.ToolbarStateEvent 16 | import com.yunlei.douyinlike.utils.log 17 | import kotlinx.android.synthetic.main.fragment_video.* 18 | import org.greenrobot.eventbus.EventBus 19 | import org.greenrobot.eventbus.Subscribe 20 | import org.greenrobot.eventbus.ThreadMode 21 | import org.jetbrains.anko.bundleOf 22 | 23 | /** 24 | * @author Yun.Lei 25 | * @email waitshan@163.com 26 | * @date 2020/1/18 27 | */ 28 | class VideoFragment : Fragment() { 29 | 30 | 31 | companion object { 32 | @JvmStatic 33 | fun getNewInstance(url: String): VideoFragment { 34 | val fragment = VideoFragment() 35 | fragment.arguments = bundleOf("url" to url) 36 | return fragment 37 | } 38 | } 39 | 40 | private var url = "" 41 | private var mCurrentPosition = 0L 42 | 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | super.onCreate(savedInstanceState) 45 | EventBus.getDefault().register(this) 46 | url = arguments?.getString("url") ?: "" 47 | } 48 | 49 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 50 | savedInstanceState: Bundle?): View? { 51 | // Inflate the layout for this fragment 52 | mCurrentPosition = 0 53 | return inflater.inflate(R.layout.fragment_video, container, false) 54 | } 55 | 56 | 57 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 58 | super.onViewCreated(view, savedInstanceState) 59 | 60 | videoPlayer?.apply { 61 | backButton.visibility = GONE 62 | titleTextView.visibility = GONE 63 | fullscreenButton.visibility = GONE 64 | isNeedShowWifiTip = false 65 | isLooping = true 66 | dismissControlTime = 0 67 | } 68 | videoPlayer.setUpLazy(url, false, null, null, "") 69 | // likeLayout.visibility = View.GONE 70 | likeLayout.onPauseListener = { 71 | if (videoPlayer.gsyVideoManager.isPlaying) { 72 | videoPlayer?.onVideoPause() 73 | } else { 74 | videoPlayer?.onVideoResume(false) 75 | } 76 | // videoPlayer.startButton.performClick() 77 | log("performClick") 78 | } 79 | likeLayout.onLikeListener = { 80 | if (!likeBtn.isLiked) 81 | likeBtn.performClick() 82 | } 83 | 84 | } 85 | 86 | override fun onResume() { 87 | super.onResume() 88 | EventBus.getDefault().post(ToolbarStateEvent(true)) 89 | if (mCurrentPosition > 0) { 90 | videoPlayer?.onVideoResume(false) 91 | } else { 92 | videoPlayer?.postDelayed({ 93 | videoPlayer?.startPlayLogic() 94 | }, 200) 95 | } 96 | 97 | log("onResume->mCurrentPosition:$mCurrentPosition") 98 | } 99 | 100 | @Subscribe(threadMode = ThreadMode.MAIN) 101 | fun onEventClearPosition(event: ClearPositionEvent) { 102 | if (event.isClear) { 103 | mCurrentPosition = 0 104 | } 105 | } 106 | 107 | override fun onPause() { 108 | super.onPause() 109 | likeLayout?.onPause() 110 | videoPlayer?.onVideoPause() 111 | mCurrentPosition = videoPlayer?.gsyVideoManager?.currentPosition ?: 0 112 | 113 | log("onPause") 114 | } 115 | 116 | override fun onStart() { 117 | super.onStart() 118 | log("onStart") 119 | } 120 | 121 | override fun onStop() { 122 | super.onStop() 123 | log("onStop") 124 | } 125 | 126 | override fun onDestroy() { 127 | super.onDestroy() 128 | EventBus.getDefault().unregister(this) 129 | } 130 | 131 | 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/utils/Expand.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.utils 2 | 3 | import android.util.Log 4 | import androidx.fragment.app.Fragment 5 | 6 | /** 7 | * @author Yun.Lei 8 | * @email waitshan@163.com 9 | * @date 2020/1/18 10 | */ 11 | 12 | fun Fragment.log(msg:String){ 13 | Log.d(javaClass.simpleName,msg) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/yunlei/douyinlike/widget/LikeLayout.kt: -------------------------------------------------------------------------------- 1 | package com.yunlei.douyinlike.widget 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.animation.AnimatorSet 6 | import android.animation.ObjectAnimator 7 | import android.content.Context 8 | import android.graphics.Matrix 9 | import android.graphics.drawable.Drawable 10 | import android.os.Handler 11 | import android.os.Message 12 | import android.util.AttributeSet 13 | import android.view.MotionEvent 14 | import android.widget.FrameLayout 15 | import android.widget.ImageView 16 | import com.yunlei.douyinlike.R 17 | import java.lang.ref.WeakReference 18 | import java.util.* 19 | 20 | /** 21 | * 类名:LikeLayout 22 | * 作者:Yun.Lei 23 | * 功能: 24 | * 创建日期:2017-12-20 14:33 25 | * 修改人: 26 | * 修改时间: 27 | * 修改备注: 28 | */ 29 | class LikeLayout : FrameLayout { 30 | 31 | var onLikeListener: () -> Unit = {} //屏幕点赞后,点赞按钮需同步点赞 32 | var onPauseListener: () -> Unit = {} //暂停 或者 继续播放 33 | 34 | constructor(context: Context?) : super(context) 35 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 36 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 37 | 38 | private var icon: Drawable = resources.getDrawable(R.mipmap.ic_heart) 39 | private var mClickCount = 0 //点击一次是暂停,多次是点赞 40 | private val mHandler = LikeLayoutHandler(this) 41 | 42 | init { 43 | clipChildren = false //避免旋转时红心被遮挡 44 | } 45 | 46 | override fun onTouchEvent(event: MotionEvent?): Boolean { 47 | if (event?.action == MotionEvent.ACTION_DOWN) { //按下时在Layout中生成红心 48 | val x = event.x 49 | val y = event.y 50 | mClickCount++ 51 | mHandler.removeCallbacksAndMessages(null) 52 | if (mClickCount >= 2) { 53 | addHeartView(x, y) 54 | onLikeListener() 55 | mHandler.sendEmptyMessageDelayed(1, 500) 56 | } else { 57 | mHandler.sendEmptyMessageDelayed(0, 500) 58 | } 59 | 60 | } 61 | return true 62 | } 63 | 64 | 65 | private fun pauseClick() { 66 | if (mClickCount == 1) { 67 | onPauseListener() 68 | } 69 | mClickCount = 0 70 | } 71 | 72 | fun onPause() { 73 | mClickCount = 0 74 | mHandler.removeCallbacksAndMessages(null) 75 | } 76 | 77 | /** 78 | * 在Layout中添加红心并,播放消失动画 79 | */ 80 | private fun addHeartView(x: Float, y: Float) { 81 | val lp = LayoutParams(icon.intrinsicWidth, icon.intrinsicHeight) //计算点击的点位红心的下部中间 82 | lp.leftMargin = (x - icon.intrinsicWidth / 2).toInt() 83 | lp.topMargin = (y - icon.intrinsicHeight).toInt() 84 | val img = ImageView(context) 85 | img.scaleType = ImageView.ScaleType.MATRIX 86 | val matrix = Matrix() 87 | matrix.postRotate(getRandomRotate()) //设置红心的微量偏移 88 | img.imageMatrix = matrix 89 | img.setImageDrawable(icon) 90 | img.layoutParams = lp 91 | addView(img) 92 | val animSet = getShowAnimSet(img) 93 | val hideSet = getHideAnimSet(img) 94 | animSet.start() 95 | animSet.addListener(object : AnimatorListenerAdapter() { 96 | override fun onAnimationEnd(animation: Animator?) { 97 | super.onAnimationEnd(animation) 98 | hideSet.start() 99 | } 100 | }) 101 | hideSet.addListener(object : AnimatorListenerAdapter() { 102 | override fun onAnimationEnd(animation: Animator?) { 103 | super.onAnimationEnd(animation) 104 | removeView(img) //动画结束移除红心 105 | } 106 | }) 107 | } 108 | 109 | /** 110 | * 刚点击的时候的一个缩放效果 111 | */ 112 | private fun getShowAnimSet(view: ImageView): AnimatorSet { 113 | // 缩放动画 114 | val scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1.2f, 1f) 115 | val scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1.2f, 1f) 116 | val animSet = AnimatorSet() 117 | animSet.playTogether(scaleX, scaleY) 118 | animSet.duration = 100 119 | return animSet 120 | } 121 | 122 | /** 123 | * 缩放结束后到红心消失的效果 124 | */ 125 | private fun getHideAnimSet(view: ImageView): AnimatorSet { 126 | // 1.alpha动画 127 | val alpha = ObjectAnimator.ofFloat(view, "alpha", 1f, 0.1f) 128 | // 2.缩放动画 129 | val scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 2f) 130 | val scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 2f) 131 | // 3.translation动画 132 | val translation = ObjectAnimator.ofFloat(view, "translationY", 0f, -150f) 133 | val animSet = AnimatorSet() 134 | animSet.playTogether(alpha, scaleX, scaleY, translation) 135 | animSet.duration = 500 136 | return animSet 137 | } 138 | 139 | /** 140 | * 生成一个随机的左右偏移量 141 | */ 142 | private fun getRandomRotate(): Float = (Random().nextInt(20) - 10).toFloat() 143 | 144 | companion object { 145 | private class LikeLayoutHandler(view: LikeLayout) : Handler() { 146 | private val mView = WeakReference(view) 147 | override fun handleMessage(msg: Message?) { 148 | super.handleMessage(msg) 149 | when(msg?.what){ 150 | 0-> mView.get()?.pauseClick() 151 | 1-> mView.get()?.onPause() 152 | } 153 | 154 | } 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /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_add_box_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_live_tv_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_message_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 24 | 25 | 29 | 30 | 33 | 34 | 38 | 39 | 42 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 15 | 16 | 22 | 23 | 24 | 25 | 33 | 34 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 25 | 26 | 30 | 31 | 40 | 41 | 48 | 49 | 54 | 55 | 62 | 63 | 68 | 69 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_home_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 29 | 30 | 33 | 34 | 41 | 42 | 43 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/ic_heart.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_heart_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/ic_heart_off.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_heart_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/ic_heart_on.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/image_head_def.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/image_head_def.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/image_info_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/image_info_demo.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/image_splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xhdpi/image_splash.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xxhdpi/ic_heart.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6495ED 4 | #6495ED 5 | #FF4081 6 | #555555 7 | #333333 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TouYin 3 | Hello blank fragment 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.61' 5 | ext.anko_version='0.10.8' 6 | repositories { 7 | google() 8 | jcenter() 9 | 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.5.3' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | maven {url "https://jitpack.io"} 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jan 10 09:40:13 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /likebutton/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /likebutton/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | 4 | android { 5 | compileSdkVersion 28 6 | buildToolsVersion "29.0.2" 7 | 8 | lintOptions { 9 | abortOnError false 10 | } 11 | 12 | defaultConfig { 13 | minSdkVersion 19 14 | targetSdkVersion 28 15 | versionCode 1 16 | versionName "1.0.0" 17 | vectorDrawables.useSupportLibrary = true 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | testImplementation 'junit:junit:4.12' 30 | implementation 'androidx.appcompat:appcompat:1.1.0' 31 | } 32 | -------------------------------------------------------------------------------- /likebutton/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\Joel\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /likebutton/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/CircleView.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | import android.animation.ArgbEvaluator; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import android.graphics.PorterDuff; 9 | import android.graphics.PorterDuffXfermode; 10 | import androidx.annotation.ColorInt; 11 | import android.util.AttributeSet; 12 | import android.util.Property; 13 | import android.view.View; 14 | 15 | /** 16 | * Created by Miroslaw Stanek on 21.12.2015. 17 | * Modified by Joel Dean 18 | */ 19 | 20 | public class CircleView extends View { 21 | private int START_COLOR = 0xFFFF5722; 22 | private int END_COLOR = 0xFFFFC107; 23 | 24 | private ArgbEvaluator argbEvaluator = new ArgbEvaluator(); 25 | 26 | private Paint circlePaint = new Paint(); 27 | private Paint maskPaint = new Paint(); 28 | 29 | private Bitmap tempBitmap; 30 | private Canvas tempCanvas; 31 | 32 | private float outerCircleRadiusProgress = 0f; 33 | private float innerCircleRadiusProgress = 0f; 34 | 35 | private int width = 0; 36 | private int height = 0; 37 | 38 | private int maxCircleSize; 39 | 40 | public CircleView(Context context) { 41 | super(context); 42 | init(); 43 | } 44 | 45 | public CircleView(Context context, AttributeSet attrs) { 46 | super(context, attrs); 47 | init(); 48 | } 49 | 50 | public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { 51 | super(context, attrs, defStyleAttr); 52 | init(); 53 | } 54 | 55 | 56 | private void init() { 57 | circlePaint.setStyle(Paint.Style.FILL); 58 | circlePaint.setAntiAlias(true); 59 | maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 60 | maskPaint.setAntiAlias(true); 61 | } 62 | 63 | public void setSize(int width, int height) { 64 | this.width = width; 65 | this.height = height; 66 | invalidate(); 67 | } 68 | 69 | 70 | @Override 71 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 72 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 73 | 74 | if (width != 0 && height != 0) 75 | setMeasuredDimension(width, height); 76 | } 77 | 78 | 79 | @Override 80 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 81 | super.onSizeChanged(w, h, oldw, oldh); 82 | maxCircleSize = w / 2; 83 | tempBitmap = Bitmap.createBitmap(getWidth(), getWidth(), Bitmap.Config.ARGB_8888); 84 | tempCanvas = new Canvas(tempBitmap); 85 | } 86 | 87 | @Override 88 | protected void onDraw(Canvas canvas) { 89 | super.onDraw(canvas); 90 | tempCanvas.drawColor(0xffffff, PorterDuff.Mode.CLEAR); 91 | tempCanvas.drawCircle(getWidth() / 2, getHeight() / 2, outerCircleRadiusProgress * maxCircleSize, circlePaint); 92 | tempCanvas.drawCircle(getWidth() / 2, getHeight() / 2, innerCircleRadiusProgress * maxCircleSize + 1, maskPaint); 93 | canvas.drawBitmap(tempBitmap, 0, 0, null); 94 | } 95 | 96 | public void setInnerCircleRadiusProgress(float innerCircleRadiusProgress) { 97 | this.innerCircleRadiusProgress = innerCircleRadiusProgress; 98 | postInvalidate(); 99 | } 100 | 101 | public float getInnerCircleRadiusProgress() { 102 | return innerCircleRadiusProgress; 103 | } 104 | 105 | public void setOuterCircleRadiusProgress(float outerCircleRadiusProgress) { 106 | this.outerCircleRadiusProgress = outerCircleRadiusProgress; 107 | updateCircleColor(); 108 | postInvalidate(); 109 | } 110 | 111 | private void updateCircleColor() { 112 | float colorProgress = (float) Utils.clamp(outerCircleRadiusProgress, 0.5, 1); 113 | colorProgress = (float) Utils.mapValueFromRangeToRange(colorProgress, 0.5f, 1f, 0f, 1f); 114 | this.circlePaint.setColor((Integer) argbEvaluator.evaluate(colorProgress, START_COLOR, END_COLOR)); 115 | } 116 | 117 | public float getOuterCircleRadiusProgress() { 118 | return outerCircleRadiusProgress; 119 | } 120 | 121 | public static final Property INNER_CIRCLE_RADIUS_PROGRESS = 122 | new Property(Float.class, "innerCircleRadiusProgress") { 123 | @Override 124 | public Float get(CircleView object) { 125 | return object.getInnerCircleRadiusProgress(); 126 | } 127 | 128 | @Override 129 | public void set(CircleView object, Float value) { 130 | object.setInnerCircleRadiusProgress(value); 131 | } 132 | }; 133 | 134 | public static final Property OUTER_CIRCLE_RADIUS_PROGRESS = 135 | new Property(Float.class, "outerCircleRadiusProgress") { 136 | @Override 137 | public Float get(CircleView object) { 138 | return object.getOuterCircleRadiusProgress(); 139 | } 140 | 141 | @Override 142 | public void set(CircleView object, Float value) { 143 | object.setOuterCircleRadiusProgress(value); 144 | } 145 | }; 146 | 147 | public void setStartColor(@ColorInt int color) { 148 | START_COLOR = color; 149 | invalidate(); 150 | } 151 | 152 | public void setEndColor(@ColorInt int color) { 153 | END_COLOR = color; 154 | invalidate(); 155 | } 156 | } -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/DotsView.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | 4 | import android.animation.ArgbEvaluator; 5 | import android.content.Context; 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import androidx.annotation.ColorInt; 9 | import android.util.AttributeSet; 10 | import android.util.Property; 11 | import android.view.View; 12 | 13 | /** 14 | * Created by Miroslaw Stanek on 21.12.2015. 15 | * Modified by Joel Dean 16 | */ 17 | public class DotsView extends View { 18 | private static final int DOTS_COUNT = 7; 19 | private static final int OUTER_DOTS_POSITION_ANGLE = 51; 20 | 21 | private int COLOR_1 = 0xFFFFC107; 22 | private int COLOR_2 = 0xFFFF9800; 23 | private int COLOR_3 = 0xFFFF5722; 24 | private int COLOR_4 = 0xFFF44336; 25 | 26 | private int width = 0; 27 | private int height = 0; 28 | 29 | private final Paint[] circlePaints = new Paint[4]; 30 | 31 | private int centerX; 32 | private int centerY; 33 | 34 | private float maxOuterDotsRadius; 35 | private float maxInnerDotsRadius; 36 | private float maxDotSize; 37 | 38 | private float currentProgress = 0; 39 | 40 | private float currentRadius1 = 0; 41 | private float currentDotSize1 = 0; 42 | 43 | private float currentDotSize2 = 0; 44 | private float currentRadius2 = 0; 45 | 46 | private ArgbEvaluator argbEvaluator = new ArgbEvaluator(); 47 | 48 | public DotsView(Context context) { 49 | super(context); 50 | init(); 51 | } 52 | 53 | public DotsView(Context context, AttributeSet attrs) { 54 | super(context, attrs); 55 | init(); 56 | } 57 | 58 | public DotsView(Context context, AttributeSet attrs, int defStyleAttr) { 59 | super(context, attrs, defStyleAttr); 60 | init(); 61 | } 62 | 63 | 64 | private void init() { 65 | for (int i = 0; i < circlePaints.length; i++) { 66 | circlePaints[i] = new Paint(); 67 | circlePaints[i].setStyle(Paint.Style.FILL); 68 | circlePaints[i].setAntiAlias(true); 69 | } 70 | } 71 | 72 | @Override 73 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 74 | super.onSizeChanged(w, h, oldw, oldh); 75 | centerX = w / 2; 76 | centerY = h / 2; 77 | maxDotSize = 5; 78 | maxOuterDotsRadius = w / 2 - maxDotSize * 2; 79 | maxInnerDotsRadius = 0.8f * maxOuterDotsRadius; 80 | } 81 | 82 | @Override 83 | protected void onDraw(Canvas canvas) { 84 | drawOuterDotsFrame(canvas); 85 | drawInnerDotsFrame(canvas); 86 | } 87 | 88 | private void drawOuterDotsFrame(Canvas canvas) { 89 | for (int i = 0; i < DOTS_COUNT; i++) { 90 | int cX = (int) (centerX + currentRadius1 * Math.cos(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180)); 91 | int cY = (int) (centerY + currentRadius1 * Math.sin(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180)); 92 | canvas.drawCircle(cX, cY, currentDotSize1, circlePaints[i % circlePaints.length]); 93 | } 94 | } 95 | 96 | private void drawInnerDotsFrame(Canvas canvas) { 97 | for (int i = 0; i < DOTS_COUNT; i++) { 98 | int cX = (int) (centerX + currentRadius2 * Math.cos((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)); 99 | int cY = (int) (centerY + currentRadius2 * Math.sin((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)); 100 | canvas.drawCircle(cX, cY, currentDotSize2, circlePaints[(i + 1) % circlePaints.length]); 101 | } 102 | } 103 | 104 | public void setCurrentProgress(float currentProgress) { 105 | this.currentProgress = currentProgress; 106 | 107 | updateInnerDotsPosition(); 108 | updateOuterDotsPosition(); 109 | updateDotsPaints(); 110 | updateDotsAlpha(); 111 | 112 | postInvalidate(); 113 | } 114 | 115 | public float getCurrentProgress() { 116 | return currentProgress; 117 | } 118 | 119 | private void updateInnerDotsPosition() { 120 | if (currentProgress < 0.3f) { 121 | this.currentRadius2 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0, 0.3f, 0.f, maxInnerDotsRadius); 122 | } else { 123 | this.currentRadius2 = maxInnerDotsRadius; 124 | } 125 | if (currentProgress == 0) { 126 | this.currentDotSize2 = 0; 127 | } else if (currentProgress < 0.2) { 128 | this.currentDotSize2 = maxDotSize; 129 | } else if (currentProgress < 0.5) { 130 | this.currentDotSize2 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.2f, 0.5f, maxDotSize, 0.3 * maxDotSize); 131 | } else { 132 | this.currentDotSize2 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.5f, 1f, maxDotSize * 0.3f, 0); 133 | } 134 | 135 | } 136 | 137 | private void updateOuterDotsPosition() { 138 | if (currentProgress < 0.3f) { 139 | this.currentRadius1 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.0f, 0.3f, 0, maxOuterDotsRadius * 0.8f); 140 | } else { 141 | this.currentRadius1 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.3f, 1f, 0.8f * maxOuterDotsRadius, maxOuterDotsRadius); 142 | } 143 | if (currentProgress == 0) { 144 | this.currentDotSize1 = 0; 145 | } else if (currentProgress < 0.7) { 146 | this.currentDotSize1 = maxDotSize; 147 | } else { 148 | this.currentDotSize1 = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.7f, 1f, maxDotSize, 0); 149 | } 150 | } 151 | 152 | private void updateDotsPaints() { 153 | if (currentProgress < 0.5f) { 154 | float progress = (float) Utils.mapValueFromRangeToRange(currentProgress, 0f, 0.5f, 0, 1f); 155 | circlePaints[0].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_1, COLOR_2)); 156 | circlePaints[1].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_2, COLOR_3)); 157 | circlePaints[2].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_3, COLOR_4)); 158 | circlePaints[3].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_4, COLOR_1)); 159 | } else { 160 | float progress = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.5f, 1f, 0, 1f); 161 | circlePaints[0].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_2, COLOR_3)); 162 | circlePaints[1].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_3, COLOR_4)); 163 | circlePaints[2].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_4, COLOR_1)); 164 | circlePaints[3].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_1, COLOR_2)); 165 | } 166 | } 167 | 168 | public void setColors(@ColorInt int primaryColor, @ColorInt int secondaryColor) { 169 | COLOR_1 = primaryColor; 170 | COLOR_2 = secondaryColor; 171 | COLOR_3 = primaryColor; 172 | COLOR_4 = secondaryColor; 173 | invalidate(); 174 | } 175 | 176 | private void updateDotsAlpha() { 177 | float progress = (float) Utils.clamp(currentProgress, 0.6f, 1f); 178 | int alpha = (int) Utils.mapValueFromRangeToRange(progress, 0.6f, 1f, 255, 0); 179 | circlePaints[0].setAlpha(alpha); 180 | circlePaints[1].setAlpha(alpha); 181 | circlePaints[2].setAlpha(alpha); 182 | circlePaints[3].setAlpha(alpha); 183 | } 184 | 185 | public void setSize(int width, int height) { 186 | this.width = width; 187 | this.height = height; 188 | invalidate(); 189 | } 190 | 191 | @Override 192 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 193 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 194 | 195 | if (width != 0 && height != 0) 196 | setMeasuredDimension(width, height); 197 | } 198 | 199 | 200 | public static final Property DOTS_PROGRESS = new Property(Float.class, "dotsProgress") { 201 | @Override 202 | public Float get(DotsView object) { 203 | return object.getCurrentProgress(); 204 | } 205 | 206 | @Override 207 | public void set(DotsView object, Float value) { 208 | object.setCurrentProgress(value); 209 | } 210 | }; 211 | } -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/Icon.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | import androidx.annotation.DrawableRes; 4 | 5 | /** 6 | * Created by Joel on 23/12/2015. 7 | */ 8 | public class Icon { 9 | private int onIconResourceId; 10 | private int offIconResourceId; 11 | private IconType iconType; 12 | 13 | public Icon(@DrawableRes int onIconResourceId,@DrawableRes int offIconResourceId, IconType iconType) { 14 | this.onIconResourceId = onIconResourceId; 15 | this.offIconResourceId = offIconResourceId; 16 | this.iconType = iconType; 17 | } 18 | 19 | public int getOffIconResourceId() { 20 | return offIconResourceId; 21 | } 22 | 23 | public void setOffIconResourceId(@DrawableRes int offIconResourceId) { 24 | this.offIconResourceId = offIconResourceId; 25 | } 26 | 27 | public int getOnIconResourceId() { 28 | return onIconResourceId; 29 | } 30 | 31 | public void setOnIconResourceId(@DrawableRes int onIconResourceId) { 32 | this.onIconResourceId = onIconResourceId; 33 | } 34 | 35 | public IconType getIconType() { 36 | return iconType; 37 | } 38 | 39 | public void setIconType(IconType iconType) { 40 | this.iconType = iconType; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/IconType.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | /** 4 | * Created by Joel on 23/12/2015. 5 | */ 6 | public enum IconType { 7 | Heart, 8 | Thumb, 9 | Star 10 | } 11 | -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/LikeButton.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.AnimatorSet; 6 | import android.animation.ObjectAnimator; 7 | import android.content.Context; 8 | import android.content.res.TypedArray; 9 | import android.graphics.drawable.Drawable; 10 | import androidx.annotation.ColorInt; 11 | import androidx.annotation.ColorRes; 12 | import androidx.annotation.DrawableRes; 13 | import androidx.core.content.ContextCompat; 14 | import android.util.AttributeSet; 15 | import android.view.LayoutInflater; 16 | import android.view.MotionEvent; 17 | import android.view.View; 18 | import android.view.animation.AccelerateDecelerateInterpolator; 19 | import android.view.animation.DecelerateInterpolator; 20 | import android.view.animation.OvershootInterpolator; 21 | import android.widget.FrameLayout; 22 | import android.widget.ImageView; 23 | 24 | import com.github.like.view.R; 25 | 26 | import java.util.List; 27 | 28 | 29 | public class LikeButton extends FrameLayout implements View.OnClickListener { 30 | private static final DecelerateInterpolator DECCELERATE_INTERPOLATOR = new DecelerateInterpolator(); 31 | private static final AccelerateDecelerateInterpolator ACCELERATE_DECELERATE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); 32 | private static final OvershootInterpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator(4); 33 | 34 | private ImageView icon; 35 | private DotsView dotsView; 36 | private CircleView circleView; 37 | private Icon currentIcon; 38 | private OnLikeListener likeListener; 39 | private OnAnimationEndListener animationEndListener; 40 | private int circleStartColor; 41 | private int circleEndColor; 42 | private int iconSize; 43 | 44 | 45 | private float animationScaleFactor; 46 | 47 | private boolean isChecked; 48 | 49 | 50 | private boolean isEnabled; 51 | private AnimatorSet animatorSet; 52 | 53 | private Drawable likeDrawable; 54 | private Drawable unLikeDrawable; 55 | 56 | public LikeButton(Context context) { 57 | this(context, null); 58 | } 59 | 60 | public LikeButton(Context context, AttributeSet attrs) { 61 | this(context, attrs, 0); 62 | } 63 | 64 | public LikeButton(Context context, AttributeSet attrs, int defStyleAttr) { 65 | super(context, attrs, defStyleAttr); 66 | 67 | if(!isInEditMode()) 68 | init(context, attrs, defStyleAttr); 69 | } 70 | 71 | /** 72 | * Does all the initial setup of the button such as retrieving all the attributes that were 73 | * set in xml and inflating the like button's view and initial state. 74 | * 75 | * @param context 76 | * @param attrs 77 | * @param defStyle 78 | */ 79 | private void init(Context context, AttributeSet attrs, int defStyle) { 80 | LayoutInflater.from(getContext()).inflate(R.layout.likeview, this, true); 81 | icon = findViewById(R.id.icon); 82 | dotsView = findViewById(R.id.dots); 83 | circleView = findViewById(R.id.circle); 84 | 85 | final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LikeButton, defStyle, 0); 86 | 87 | iconSize = array.getDimensionPixelSize(R.styleable.LikeButton_icon_size, -1); 88 | if (iconSize == -1) 89 | iconSize = 40; 90 | 91 | String iconType = array.getString(R.styleable.LikeButton_icon_type); 92 | 93 | likeDrawable = getDrawableFromResource(array, R.styleable.LikeButton_like_drawable); 94 | 95 | if (likeDrawable != null) 96 | setLikeDrawable(likeDrawable); 97 | 98 | unLikeDrawable = getDrawableFromResource(array, R.styleable.LikeButton_unlike_drawable); 99 | 100 | if (unLikeDrawable != null) 101 | setUnlikeDrawable(unLikeDrawable); 102 | 103 | if (iconType != null) 104 | if (!iconType.isEmpty()) 105 | currentIcon = parseIconType(iconType); 106 | 107 | 108 | circleStartColor = array.getColor(R.styleable.LikeButton_circle_start_color, 0); 109 | 110 | if (circleStartColor != 0) 111 | circleView.setStartColor(circleStartColor); 112 | 113 | circleEndColor = array.getColor(R.styleable.LikeButton_circle_end_color, 0); 114 | 115 | if (circleEndColor != 0) 116 | circleView.setEndColor(circleEndColor); 117 | 118 | int dotPrimaryColor = array.getColor(R.styleable.LikeButton_dots_primary_color, 0); 119 | int dotSecondaryColor = array.getColor(R.styleable.LikeButton_dots_secondary_color, 0); 120 | 121 | if (dotPrimaryColor != 0 && dotSecondaryColor != 0) { 122 | dotsView.setColors(dotPrimaryColor, dotSecondaryColor); 123 | } 124 | 125 | 126 | if (likeDrawable == null && unLikeDrawable == null) { 127 | if (currentIcon != null) { 128 | setIcon(); 129 | } else { 130 | setIcon(IconType.Heart); 131 | } 132 | } 133 | 134 | setEnabled(array.getBoolean(R.styleable.LikeButton_is_enabled, true)); 135 | Boolean status = array.getBoolean(R.styleable.LikeButton_liked, false); 136 | setAnimationScaleFactor(array.getFloat(R.styleable.LikeButton_anim_scale_factor, 3)); 137 | setLiked(status); 138 | setOnClickListener(this); 139 | array.recycle(); 140 | } 141 | 142 | private Drawable getDrawableFromResource(TypedArray array, int styleableIndexId) { 143 | int id = array.getResourceId(styleableIndexId, -1); 144 | 145 | return (-1 != id) ? ContextCompat.getDrawable(getContext(), id) : null; 146 | } 147 | 148 | /** 149 | * This triggers the entire functionality of the button such as icon changes, 150 | * animations, listeners etc. 151 | * 152 | * @param v 153 | */ 154 | @Override 155 | public void onClick(View v) { 156 | 157 | if (!isEnabled) 158 | return; 159 | 160 | isChecked = !isChecked; 161 | 162 | icon.setImageDrawable(isChecked ? likeDrawable : unLikeDrawable); 163 | 164 | if (likeListener != null) { 165 | if (isChecked) { 166 | likeListener.liked(this); 167 | } else { 168 | likeListener.unLiked(this); 169 | } 170 | } 171 | 172 | if (animatorSet != null) { 173 | animatorSet.cancel(); 174 | } 175 | 176 | if (isChecked) { 177 | icon.animate().cancel(); 178 | icon.setScaleX(0); 179 | icon.setScaleY(0); 180 | circleView.setInnerCircleRadiusProgress(0); 181 | circleView.setOuterCircleRadiusProgress(0); 182 | dotsView.setCurrentProgress(0); 183 | 184 | animatorSet = new AnimatorSet(); 185 | 186 | ObjectAnimator outerCircleAnimator = ObjectAnimator.ofFloat(circleView, CircleView.OUTER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f); 187 | outerCircleAnimator.setDuration(250); 188 | outerCircleAnimator.setInterpolator(DECCELERATE_INTERPOLATOR); 189 | 190 | ObjectAnimator innerCircleAnimator = ObjectAnimator.ofFloat(circleView, CircleView.INNER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f); 191 | innerCircleAnimator.setDuration(200); 192 | innerCircleAnimator.setStartDelay(200); 193 | innerCircleAnimator.setInterpolator(DECCELERATE_INTERPOLATOR); 194 | 195 | ObjectAnimator starScaleYAnimator = ObjectAnimator.ofFloat(icon, ImageView.SCALE_Y, 0.2f, 1f); 196 | starScaleYAnimator.setDuration(350); 197 | starScaleYAnimator.setStartDelay(250); 198 | starScaleYAnimator.setInterpolator(OVERSHOOT_INTERPOLATOR); 199 | 200 | ObjectAnimator starScaleXAnimator = ObjectAnimator.ofFloat(icon, ImageView.SCALE_X, 0.2f, 1f); 201 | starScaleXAnimator.setDuration(350); 202 | starScaleXAnimator.setStartDelay(250); 203 | starScaleXAnimator.setInterpolator(OVERSHOOT_INTERPOLATOR); 204 | 205 | ObjectAnimator dotsAnimator = ObjectAnimator.ofFloat(dotsView, DotsView.DOTS_PROGRESS, 0, 1f); 206 | dotsAnimator.setDuration(900); 207 | dotsAnimator.setStartDelay(50); 208 | dotsAnimator.setInterpolator(ACCELERATE_DECELERATE_INTERPOLATOR); 209 | 210 | animatorSet.playTogether( 211 | outerCircleAnimator, 212 | innerCircleAnimator, 213 | starScaleYAnimator, 214 | starScaleXAnimator, 215 | dotsAnimator 216 | ); 217 | 218 | animatorSet.addListener(new AnimatorListenerAdapter() { 219 | @Override 220 | public void onAnimationCancel(Animator animation) { 221 | circleView.setInnerCircleRadiusProgress(0); 222 | circleView.setOuterCircleRadiusProgress(0); 223 | dotsView.setCurrentProgress(0); 224 | icon.setScaleX(1); 225 | icon.setScaleY(1); 226 | } 227 | 228 | @Override public void onAnimationEnd(Animator animation) { 229 | if(animationEndListener != null) { 230 | animationEndListener.onAnimationEnd(LikeButton.this); 231 | } 232 | } 233 | }); 234 | 235 | animatorSet.start(); 236 | } 237 | } 238 | 239 | /** 240 | * Used to trigger the scale animation that takes places on the 241 | * icon when the button is touched. 242 | * 243 | * @param event 244 | * @return 245 | */ 246 | @Override 247 | public boolean onTouchEvent(MotionEvent event) { 248 | if (!isEnabled) 249 | return true; 250 | 251 | switch (event.getAction()) { 252 | case MotionEvent.ACTION_DOWN: 253 | /* 254 | Commented out this line and moved the animation effect to the action up event due to 255 | conflicts that were occurring when library is used in sliding type views. 256 | 257 | icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR); 258 | */ 259 | setPressed(true); 260 | break; 261 | 262 | case MotionEvent.ACTION_MOVE: 263 | float x = event.getX(); 264 | float y = event.getY(); 265 | boolean isInside = (x > 0 && x < getWidth() && y > 0 && y < getHeight()); 266 | if (isPressed() != isInside) { 267 | setPressed(isInside); 268 | } 269 | break; 270 | 271 | case MotionEvent.ACTION_UP: 272 | icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR); 273 | icon.animate().scaleX(1).scaleY(1).setInterpolator(DECCELERATE_INTERPOLATOR); 274 | if (isPressed()) { 275 | performClick(); 276 | setPressed(false); 277 | } 278 | break; 279 | case MotionEvent.ACTION_CANCEL: 280 | setPressed(false); 281 | break; 282 | } 283 | return true; 284 | } 285 | 286 | /** 287 | * This drawable is shown when the button is a liked state. 288 | * 289 | * @param resId 290 | */ 291 | public void setLikeDrawableRes(@DrawableRes int resId) { 292 | likeDrawable = ContextCompat.getDrawable(getContext(), resId); 293 | 294 | if (iconSize != 0) { 295 | likeDrawable = Utils.resizeDrawable(getContext(), likeDrawable, iconSize, iconSize); 296 | } 297 | 298 | if (isChecked) { 299 | icon.setImageDrawable(likeDrawable); 300 | } 301 | } 302 | 303 | /** 304 | * This drawable is shown when the button is in a liked state. 305 | * 306 | * @param likeDrawable 307 | */ 308 | public void setLikeDrawable(Drawable likeDrawable) { 309 | this.likeDrawable = likeDrawable; 310 | 311 | if (iconSize != 0) { 312 | this.likeDrawable = Utils.resizeDrawable(getContext(), likeDrawable, iconSize, iconSize); 313 | } 314 | 315 | if (isChecked) { 316 | icon.setImageDrawable(this.likeDrawable); 317 | } 318 | } 319 | 320 | /** 321 | * This drawable will be shown when the button is in on unLiked state. 322 | * 323 | * @param resId 324 | */ 325 | public void setUnlikeDrawableRes(@DrawableRes int resId) { 326 | unLikeDrawable = ContextCompat.getDrawable(getContext(), resId); 327 | 328 | if (iconSize != 0) { 329 | unLikeDrawable = Utils.resizeDrawable(getContext(), unLikeDrawable, iconSize, iconSize); 330 | } 331 | 332 | if (!isChecked) { 333 | icon.setImageDrawable(unLikeDrawable); 334 | } 335 | } 336 | 337 | /** 338 | * This drawable will be shown when the button is in on unLiked state. 339 | * 340 | * @param unLikeDrawable 341 | */ 342 | public void setUnlikeDrawable(Drawable unLikeDrawable) { 343 | this.unLikeDrawable = unLikeDrawable; 344 | 345 | if (iconSize != 0) { 346 | this.unLikeDrawable = Utils.resizeDrawable(getContext(), unLikeDrawable, iconSize, iconSize); 347 | } 348 | 349 | if (!isChecked) { 350 | icon.setImageDrawable(this.unLikeDrawable); 351 | } 352 | } 353 | 354 | /** 355 | * Sets one of the three icons that are bundled with the library. 356 | * 357 | * @param currentIconType 358 | */ 359 | public void setIcon(IconType currentIconType) { 360 | currentIcon = parseIconType(currentIconType); 361 | setLikeDrawableRes(currentIcon.getOnIconResourceId()); 362 | setUnlikeDrawableRes(currentIcon.getOffIconResourceId()); 363 | icon.setImageDrawable(this.unLikeDrawable); 364 | } 365 | 366 | public void setIcon() { 367 | setLikeDrawableRes(currentIcon.getOnIconResourceId()); 368 | setUnlikeDrawableRes(currentIcon.getOffIconResourceId()); 369 | icon.setImageDrawable(this.unLikeDrawable); 370 | } 371 | 372 | /** 373 | * Sets the size of the drawable/icon that's being used. The views that generate 374 | * the like effect are also updated to reflect the size of the icon. 375 | * 376 | * @param iconSize 377 | */ 378 | 379 | public void setIconSizeDp(int iconSize) { 380 | setIconSizePx((int) Utils.dipToPixels(getContext(), (float) iconSize)); 381 | } 382 | 383 | /** 384 | * Sets the size of the drawable/icon that's being used. The views that generate 385 | * the like effect are also updated to reflect the size of the icon. 386 | * 387 | * @param iconSize 388 | */ 389 | public void setIconSizePx(int iconSize) { 390 | this.iconSize = iconSize; 391 | setEffectsViewSize(); 392 | this.unLikeDrawable = Utils.resizeDrawable(getContext(), unLikeDrawable, iconSize, iconSize); 393 | this.likeDrawable = Utils.resizeDrawable(getContext(), likeDrawable, iconSize, iconSize); 394 | } 395 | 396 | /** 397 | * * Parses the specific icon based on string 398 | * version of its enum. 399 | * These icons are bundled with the library and 400 | * are accessed via objects that contain their 401 | * resource ids and an enum with their name. 402 | * 403 | * @param iconType 404 | * @return Icon 405 | */ 406 | private Icon parseIconType(String iconType) { 407 | List icons = Utils.getIcons(); 408 | 409 | for (Icon icon : icons) { 410 | if (icon.getIconType().name().toLowerCase().equals(iconType.toLowerCase())) { 411 | return icon; 412 | } 413 | } 414 | 415 | throw new IllegalArgumentException("Correct icon type not specified."); 416 | } 417 | 418 | /** 419 | * Parses the specific icon based on it's type. 420 | * These icons are bundled with the library and 421 | * are accessed via objects that contain their 422 | * resource ids and an enum with their name. 423 | * 424 | * @param iconType 425 | * @return 426 | */ 427 | private Icon parseIconType(IconType iconType) { 428 | List icons = Utils.getIcons(); 429 | 430 | for (Icon icon : icons) { 431 | if (icon.getIconType().equals(iconType)) { 432 | return icon; 433 | } 434 | } 435 | 436 | throw new IllegalArgumentException("Correct icon type not specified."); 437 | } 438 | 439 | /** 440 | * Listener that is triggered once the 441 | * button is in a liked or unLiked state 442 | * 443 | * @param likeListener 444 | */ 445 | public void setOnLikeListener(OnLikeListener likeListener) { 446 | this.likeListener = likeListener; 447 | } 448 | 449 | /** 450 | * Listener that is triggered once the 451 | * button animation is completed 452 | * 453 | * @param animationEndListener 454 | */ 455 | public void setOnAnimationEndListener(OnAnimationEndListener animationEndListener) { 456 | this.animationEndListener = animationEndListener; 457 | } 458 | 459 | 460 | /** 461 | * This set sets the colours that are used for the little dots 462 | * that will be exploding once the like button is clicked. 463 | * 464 | * @param primaryColor 465 | * @param secondaryColor 466 | */ 467 | public void setExplodingDotColorsRes(@ColorRes int primaryColor, @ColorRes int secondaryColor) { 468 | dotsView.setColors(ContextCompat.getColor(getContext(), primaryColor), ContextCompat.getColor(getContext(), secondaryColor)); 469 | } 470 | 471 | public void setExplodingDotColorsInt(@ColorInt int primaryColor, @ColorInt int secondaryColor) { 472 | dotsView.setColors(primaryColor, secondaryColor); 473 | } 474 | 475 | public void setCircleStartColorRes(@ColorRes int circleStartColor) { 476 | this.circleStartColor = ContextCompat.getColor(getContext(), circleStartColor); 477 | circleView.setStartColor(this.circleStartColor); 478 | } 479 | 480 | public void setCircleStartColorInt(@ColorInt int circleStartColor) { 481 | this.circleStartColor = circleStartColor; 482 | circleView.setStartColor(circleStartColor); 483 | } 484 | 485 | public void setCircleEndColorRes(@ColorRes int circleEndColor) { 486 | this.circleEndColor = ContextCompat.getColor(getContext(), circleEndColor); 487 | circleView.setEndColor(this.circleEndColor); 488 | } 489 | 490 | /** 491 | * This function updates the dots view and the circle 492 | * view with the respective sizes based on the size 493 | * of the icon being used. 494 | */ 495 | private void setEffectsViewSize() { 496 | if (iconSize != 0) { 497 | dotsView.setSize((int) (iconSize * animationScaleFactor), (int) (iconSize * animationScaleFactor)); 498 | circleView.setSize(iconSize, iconSize); 499 | } 500 | } 501 | 502 | /** 503 | * Sets the initial state of the button to liked 504 | * or unliked. 505 | * 506 | * @param status 507 | */ 508 | public void setLiked(Boolean status) { 509 | if (status) { 510 | isChecked = true; 511 | icon.setImageDrawable(likeDrawable); 512 | } else { 513 | isChecked = false; 514 | icon.setImageDrawable(unLikeDrawable); 515 | } 516 | } 517 | 518 | /** 519 | * Returns current like state 520 | * 521 | * @return current like state 522 | */ 523 | public boolean isLiked() { 524 | return isChecked; 525 | } 526 | 527 | @Override 528 | public void setEnabled(boolean enabled) { 529 | isEnabled = enabled; 530 | } 531 | 532 | /** 533 | * Sets the factor by which the dots should be sized. 534 | */ 535 | public void setAnimationScaleFactor(float animationScaleFactor) { 536 | this.animationScaleFactor = animationScaleFactor; 537 | 538 | setEffectsViewSize(); 539 | } 540 | 541 | } 542 | -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/OnAnimationEndListener.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | public interface OnAnimationEndListener { 4 | void onAnimationEnd(LikeButton likeButton); 5 | } 6 | -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/OnLikeListener.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | /** 4 | * Created by Joel on 23/12/2015. 5 | */ 6 | public interface OnLikeListener { 7 | void liked(LikeButton likeButton); 8 | void unLiked(LikeButton likeButton); 9 | } 10 | -------------------------------------------------------------------------------- /likebutton/src/main/java/com/github/like/Utils.java: -------------------------------------------------------------------------------- 1 | package com.github.like; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.drawable.BitmapDrawable; 8 | import android.graphics.drawable.Drawable; 9 | import android.graphics.drawable.VectorDrawable; 10 | import android.os.Build; 11 | import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; 12 | import android.util.DisplayMetrics; 13 | import android.util.TypedValue; 14 | 15 | import com.github.like.view.R; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * Created by Joel on 23/12/2015. 22 | */ 23 | public class Utils { 24 | public static double mapValueFromRangeToRange(double value, double fromLow, double fromHigh, double toLow, double toHigh) { 25 | return toLow + ((value - fromLow) / (fromHigh - fromLow) * (toHigh - toLow)); 26 | } 27 | 28 | public static double clamp(double value, double low, double high) { 29 | return Math.min(Math.max(value, low), high); 30 | } 31 | 32 | public static List getIcons() { 33 | List icons = new ArrayList<>(); 34 | icons.add(new Icon(R.drawable.heart_on, R.drawable.heart_off, IconType.Heart)); 35 | icons.add(new Icon(R.drawable.star_on, R.drawable.star_off, IconType.Star)); 36 | icons.add(new Icon(R.drawable.thumb_on, R.drawable.thumb_off, IconType.Thumb)); 37 | 38 | return icons; 39 | } 40 | 41 | public static Drawable resizeDrawable(Context context, Drawable drawable, int width, int height) { 42 | Bitmap bitmap = getBitmap(drawable, width, height); 43 | return new BitmapDrawable(context.getResources(), Bitmap.createScaledBitmap(bitmap, width, height, true)); 44 | } 45 | 46 | public static Bitmap getBitmap(Drawable drawable, int width, int height) { 47 | if (drawable instanceof BitmapDrawable) { 48 | return ((BitmapDrawable) drawable).getBitmap(); 49 | } else if (drawable instanceof VectorDrawableCompat) { 50 | return getBitmap((VectorDrawableCompat) drawable, width, height); 51 | } else if (drawable instanceof VectorDrawable) { 52 | return getBitmap((VectorDrawable) drawable, width, height); 53 | } else { 54 | throw new IllegalArgumentException("Unsupported drawable type"); 55 | } 56 | } 57 | 58 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 59 | private static Bitmap getBitmap(VectorDrawable vectorDrawable, int width, int height) { 60 | Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 61 | Canvas canvas = new Canvas(bitmap); 62 | vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 63 | vectorDrawable.draw(canvas); 64 | return bitmap; 65 | } 66 | 67 | private static Bitmap getBitmap(VectorDrawableCompat vectorDrawable, int width, int height) { 68 | Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 69 | Canvas canvas = new Canvas(bitmap); 70 | vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 71 | vectorDrawable.draw(canvas); 72 | return bitmap; 73 | } 74 | 75 | public static float dipToPixels(Context context, float dipValue) { 76 | DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 77 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics); 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /likebutton/src/main/res/drawable/heart_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/likebutton/src/main/res/drawable/heart_off.png -------------------------------------------------------------------------------- /likebutton/src/main/res/drawable/heart_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/likebutton/src/main/res/drawable/heart_on.png -------------------------------------------------------------------------------- /likebutton/src/main/res/drawable/star_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/likebutton/src/main/res/drawable/star_off.png -------------------------------------------------------------------------------- /likebutton/src/main/res/drawable/star_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/likebutton/src/main/res/drawable/star_on.png -------------------------------------------------------------------------------- /likebutton/src/main/res/drawable/thumb_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/likebutton/src/main/res/drawable/thumb_off.png -------------------------------------------------------------------------------- /likebutton/src/main/res/drawable/thumb_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/likebutton/src/main/res/drawable/thumb_on.png -------------------------------------------------------------------------------- /likebutton/src/main/res/layout/likeview.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /likebutton/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /likebutton/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | LikeView 3 | 4 | -------------------------------------------------------------------------------- /screenshot/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/screenshot/1.gif -------------------------------------------------------------------------------- /screenshot/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/screenshot/2.gif -------------------------------------------------------------------------------- /screenshot/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/screenshot/3.gif -------------------------------------------------------------------------------- /screenshot/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/screenshot/4.gif -------------------------------------------------------------------------------- /screenshot/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leiyun-studio/TouYin_DemoClient/c28fee637d9ce7ee869af6d8bfc453fc80d6ffce/screenshot/5.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':likebutton' 2 | --------------------------------------------------------------------------------