diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 96cc43e..61a9130 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,22 +1,6 @@ - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index afaee01..57af74c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,8 +1,10 @@ + diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 788937b..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index f700cde..eef6e0a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ +# 扫码下载最新demo第一时间体验新功能 ## 如有问题可以加群讨论 STV&RxHttp交流群 或者手动加QQ群:688433795 ## Demo示例下载 -![demo下载](https://github.com/lygttpod/AndroidCustomView/blob/master/app/src/main/res/mipmap-xxhdpi/app_download.png?raw=true) +[点击下载demo体验](http://d.firim.top/whez) # [**炫酷的提交按钮**](https://github.com/lygttpod/AndroidCustomView/blob/master/animation_button.md) ![99.gif](http://upload-images.jianshu.io/upload_images/2057501-0d1119721429bf71.gif?imageMogr2/auto-orient/strip) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ea93db..ce59942 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/activity/ClearScreenActivity.kt b/app/src/main/java/com/allen/androidcustomview/activity/ClearScreenActivity.kt new file mode 100644 index 0000000..f6091f0 --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/activity/ClearScreenActivity.kt @@ -0,0 +1,39 @@ +package com.allen.androidcustomview.activity + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.widget.Toast +import com.allen.androidcustomview.R +import com.allen.androidcustomview.widget.ClearScreenMode +import com.allen.androidcustomview.widget.ClearScreenView +import kotlinx.android.synthetic.main.activity_clear_screen.* + +/** + *
+ * author : Allen
+ * date : 2021年1月28日
+ * desc :
+ * 
+ */ +class ClearScreenActivity : AppCompatActivity(), ClearScreenView.OnClearScreenListener { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_clear_screen) + + clear_screen_container.addClearView(iv_clear_content) + + clear_screen_container.setOnClearScreenListener(this) + + btn_quick_clear.setOnClickListener { clear_screen_container.clearScreenMode = ClearScreenMode.QUICK_SCROLL } + btn_slow_clear.setOnClickListener { clear_screen_container.clearScreenMode = ClearScreenMode.SLOW_SCROLL } + } + + override fun onCleared() { + Toast.makeText(this, "清屏了", Toast.LENGTH_SHORT).show() + } + + override fun onRestored() { + Toast.makeText(this, "恢复了", Toast.LENGTH_SHORT).show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/activity/MainActivity.kt b/app/src/main/java/com/allen/androidcustomview/activity/MainActivity.kt index ec7f83c..5df548d 100644 --- a/app/src/main/java/com/allen/androidcustomview/activity/MainActivity.kt +++ b/app/src/main/java/com/allen/androidcustomview/activity/MainActivity.kt @@ -40,6 +40,9 @@ class MainActivity : AppCompatActivity(), BaseQuickAdapter.OnItemClickListener { typeBeans.add(TypeBean("揭露动画", 12)) typeBeans.add(TypeBean("支付宝首页效果", 13)) typeBeans.add(TypeBean("RecyclerView的item动画", 14)) + typeBeans.add(TypeBean("路径path动画", 15)) + typeBeans.add(TypeBean("仿新浪投票控件", 16)) + typeBeans.add(TypeBean("直播侧滑清屏效果", 17)) return typeBeans } @@ -80,6 +83,9 @@ class MainActivity : AppCompatActivity(), BaseQuickAdapter.OnItemClickListener { 12 -> startActivity(Intent(this@MainActivity, RevealAnimationActivity::class.java)) 13 -> startActivity(Intent(this@MainActivity, AliPayHomeActivity::class.java)) 14 -> startActivity(Intent(this@MainActivity, RecyclerViewItemAnimActivity::class.java)) + 15 -> startActivity(Intent(this@MainActivity, PathActivity::class.java)) + 16 -> startActivity(Intent(this@MainActivity, SinaVoteActivity::class.java)) + 17 -> startActivity(Intent(this@MainActivity, ClearScreenActivity::class.java)) } } } diff --git a/app/src/main/java/com/allen/androidcustomview/activity/PathActivity.kt b/app/src/main/java/com/allen/androidcustomview/activity/PathActivity.kt new file mode 100644 index 0000000..736beb2 --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/activity/PathActivity.kt @@ -0,0 +1,25 @@ +package com.allen.androidcustomview.activity + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import com.allen.androidcustomview.R +import kotlinx.android.synthetic.main.activity_path.* + +/** + *
+ * @author : Allen
+ * date : 2019年07月23日
+ * desc :
+ * 
+ */ +class PathActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_path) + start_btn.setOnClickListener { + car_anim_view.startAnim() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/activity/ProgressBarActivity.java b/app/src/main/java/com/allen/androidcustomview/activity/ProgressBarActivity.java index 99b3321..09c91c2 100644 --- a/app/src/main/java/com/allen/androidcustomview/activity/ProgressBarActivity.java +++ b/app/src/main/java/com/allen/androidcustomview/activity/ProgressBarActivity.java @@ -13,6 +13,10 @@ import com.allen.androidcustomview.widget.LoadingLineView; import com.allen.androidcustomview.widget.LoadingView; import com.allen.androidcustomview.widget.ProductProgressBar; +import com.allen.androidcustomview.widget.StudyPlanProgressView; + +import java.util.ArrayList; +import java.util.List; public class ProgressBarActivity extends AppCompatActivity { @@ -24,6 +28,7 @@ public class ProgressBarActivity extends AppCompatActivity { LoadingLineView loadingLineView; Button button; + StudyPlanProgressView studyPlanProgressView; @Override protected void onCreate(Bundle savedInstanceState) { @@ -39,6 +44,7 @@ protected void onCreate(Bundle savedInstanceState) { textView = (TextView) findViewById(R.id.progress_tv); button = (Button) findViewById(R.id.startAnimationBtn); + studyPlanProgressView = findViewById(R.id.study_plan_progress_view); circleProgressBarView.setProgressWithAnimation(60); circleProgressBarView.setProgressListener(new CircleProgressBarView.ProgressListener() { @@ -79,8 +85,25 @@ public void currentProgressListener(float currentProgress) { textView.setText("当前进度:" + currentProgress); } }); + studyPlanProgressView.setData(getPlanData(true)); } }); + studyPlanProgressView.setData(getPlanData(false)); + + } + + private List getPlanData(Boolean isAll) { + List list = new ArrayList(); + list.add("08月10日"); + list.add("08月11日"); + list.add("08月12日"); + list.add("08月13日"); + if (isAll) { + list.add("08月14日"); + list.add("08月15日"); + list.add("08月16日"); + } + return list; } @Override diff --git a/app/src/main/java/com/allen/androidcustomview/activity/SinaVoteActivity.kt b/app/src/main/java/com/allen/androidcustomview/activity/SinaVoteActivity.kt new file mode 100644 index 0000000..2fee8ab --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/activity/SinaVoteActivity.kt @@ -0,0 +1,43 @@ +package com.allen.androidcustomview.activity + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import com.allen.androidcustomview.R +import com.allen.androidcustomview.bean.VoteBean +import com.allen.androidcustomview.bean.VoteOption +import com.allen.androidcustomview.data.getMockData +import com.allen.androidcustomview.widget.vote.VoteLayoutAdapter +import kotlinx.android.synthetic.main.activity_sina_vote.* + +/** + *
+ * @author : Allen
+ * date : 2019年08月06日
+ * desc :
+ * 
+ */ +class SinaVoteActivity : AppCompatActivity(), VoteLayoutAdapter.OnVoteClickListener { + + var voteLayoutAdapter: VoteLayoutAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_sina_vote) + voteLayoutAdapter = VoteLayoutAdapter(vote_ll) + voteLayoutAdapter?.setData(getMockData()) + voteLayoutAdapter?.onVoteClickListener = this + } + + override fun onDestroy() { + super.onDestroy() + voteLayoutAdapter?.onDestroy() + } + + override fun onVoteCommitBtnClick(mainVote: VoteBean?, optionIds: ArrayList, position: Int) { + voteLayoutAdapter?.refreshDataAfterVotedSuccess(position) + } + + override fun onVoteItemClick(mainVote: VoteBean?, voteOption: VoteOption?, position: Int) { + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/bean/VoteBean.kt b/app/src/main/java/com/allen/androidcustomview/bean/VoteBean.kt new file mode 100644 index 0000000..961f14c --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/bean/VoteBean.kt @@ -0,0 +1,25 @@ +package com.allen.androidcustomview.bean + + +/** + *
+ * @author : Allen
+ * date : 2019年08月01日
+ * desc :
+ * 
+ */ + +class VoteBean(val id: Int = 0, + val title: String?, + val choiceType: String?, + val maxSelect: Int?, + var voted: Boolean?, + val sumVoteCount: Int?, + val options: ArrayList? +) + +data class VoteOption(var id: Int?, + var content: String?, + var voteId: Int?, + var showCount: Int?, + var voted: Boolean?) \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/data/VoteData.kt b/app/src/main/java/com/allen/androidcustomview/data/VoteData.kt new file mode 100644 index 0000000..67d4b9c --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/data/VoteData.kt @@ -0,0 +1,49 @@ +package com.allen.androidcustomview.data + +import com.allen.androidcustomview.bean.VoteBean +import com.allen.androidcustomview.bean.VoteOption +import kotlin.random.Random + +/** + *
+ * @author : Allen
+ * date : 2019年08月06日
+ * desc :
+ * 
+ */ + +fun getMockData(): ArrayList { + val list: ArrayList = arrayListOf() + for (i in 0..1) + list.add(getVoteBeanData(i)) + return list +} + +private fun getVoteBeanData(index: Int): VoteBean { + val voteTitle: String = when (index) { + 0 -> "哪吒票房能否突破30亿(多选)" + 1 -> "你觉得谁最火呢?(单选)" + else -> "" + } + return VoteBean(11, voteTitle, if (index == 0) "multiple" else "single", 2, false, Random.nextInt(10000, 20000), getVoteOptionsDatas(index)) +} + +private fun getVoteOptionsDatas(index: Int): java.util.ArrayList? { + var list: ArrayList = arrayListOf() + for (i in 0..3) + list.add(getVoteOptionData(index, i)) + return list + +} + +private fun getVoteOptionData(index: Int, i: Int): VoteOption { + + val voteContent: String = when (i) { + 0 -> if (index == 0) "当然可以" else "蔡徐坤" + 1 -> if (index == 0) "估计不能" else "肖战" + 2 -> if (index == 0) "拭目以待" else "李现" + 3 -> if (index == 0) "保持中立" else "邓伦" + else -> "" + } + return VoteOption(i, voteContent, i, Random.nextInt(6666,8888), false) +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/widget/ArcView.kt b/app/src/main/java/com/allen/androidcustomview/widget/ArcView.kt new file mode 100644 index 0000000..ef06f5f --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/widget/ArcView.kt @@ -0,0 +1,87 @@ +package com.allen.androidcustomview.widget + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import com.allen.androidcustomview.R + +/** + *
+ * @author : Allen
+ * e-mail : lygttpod@163.com
+ * date : 2019年07月07日
+ * desc :
+ * 
+ */ +class ArcView : View { + private var mWidth = 0 + private var mHeight = 0 + /** + * 弧形高度 + */ + private var mArcHeight = 0 + /** + * 背景颜色 + */ + private var mBgColor = Color.WHITE + private var mPaint = Paint() + private var mContext: Context? = null + + private var mPath = Path() + + constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + initView(context, attrs, defStyleAttr) + } + + private fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ArcView) + mArcHeight = typedArray.getDimensionPixelSize(R.styleable.ArcView_arcHeight, 0) + mBgColor = typedArray.getColor(R.styleable.ArcView_bgColor, Color.WHITE) + typedArray.recycle() + + mContext = context + mPaint.style = Paint.Style.FILL_AND_STROKE + mPaint.color = mBgColor + mPaint.isAntiAlias = true + mPaint.strokeWidth = 1f + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mWidth = w + mHeight = h + resetPath() + } + + private fun resetPath() { + mPath.reset() + mPath.moveTo(0f, 0f) + mPath.lineTo(0f, (mHeight - mArcHeight).toFloat()) + mPath.quadTo((mWidth / 2).toFloat(), (mHeight + mArcHeight).toFloat(), mWidth.toFloat(), (mHeight - mArcHeight).toFloat()) + mPath.lineTo(mWidth.toFloat(), 0f) + mPath.close() + } + + override fun onDraw(canvas: Canvas?) { + super.onDraw(canvas) + canvas?.drawPath(mPath, mPaint) + } + + fun setArcViewBgColor(color: Int) { + mBgColor = color + mPaint.color = color + invalidate() + } + + fun getArcViewBgColor() = mBgColor + + fun setArcViewHeight(height: Int) { + if (height == mHeight) return + mHeight = height + resetPath() + invalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/widget/CarMoveView.kt b/app/src/main/java/com/allen/androidcustomview/widget/CarMoveView.kt new file mode 100644 index 0000000..d88eeee --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/widget/CarMoveView.kt @@ -0,0 +1,159 @@ +package com.allen.androidcustomview.widget + +import android.animation.Animator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.allen.androidcustomview.R +import kotlin.math.atan2 + + +/** + *
+ * @author : Allen
+ * date : 2019年07月23日
+ * desc : 小汽车跟随轨迹运动
+ * 
+ */ +class CarMoveView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { + + private var mWidth = 0 + private var mHeight = 0 + + /** + * 背景颜色 + */ + private var mPathColor = Color.RED + private var mPaint = Paint() + private var mCarPaint = Paint() + + private var mRect = Rect() + private var mMovePath = Path() + + private var mStartX = 0f + private var mStartY = 0f + + private var pathMeasure: PathMeasure = PathMeasure() + + private val pos = FloatArray(2) + private val tan = FloatArray(2) + + private var isMoveCar = false + + private var mRectWidth = 30 + + private var mDuration = 5 + private var mCarDrawableRes: Drawable? = null + private var mCarBitmapRes: Bitmap? = null + + init { + initView(context, attrs, defStyleAttr) + initPaint() + mCarBitmapRes = (mCarDrawableRes as? BitmapDrawable)?.bitmap + } + + private fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CarMoveView) + mPathColor = typedArray.getColor(R.styleable.CarMoveView_carMovePathColor, mPathColor) + mDuration = typedArray.getInt(R.styleable.CarMoveView_carMoveDuration, mDuration) + mCarDrawableRes = typedArray.getDrawable(R.styleable.CarMoveView_carMoveDrawableRes) + typedArray.recycle() + } + + private fun initPaint() { + mPaint.color = mPathColor + mPaint.style = Paint.Style.STROKE + mPaint.isAntiAlias = true + + mCarPaint.color = mPathColor + mCarPaint.style = Paint.Style.FILL + mCarPaint.isAntiAlias = true + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mWidth = w + mHeight = h + } + + override fun onDraw(canvas: Canvas?) { + super.onDraw(canvas) + drawPath(canvas) + drawMoveCar(canvas) + } + + private fun drawPath(canvas: Canvas?) { + canvas?.drawPath(mMovePath, mPaint) + } + + private fun drawMoveCar(canvas: Canvas?) { + pathMeasure.setPath(mMovePath, false) + if (isMoveCar) { + // 计算图片旋转角度 + val degrees = (atan2(tan[1].toDouble(), tan[0].toDouble()) * 180.0 / Math.PI).toFloat() + canvas?.rotate(degrees, pos[0], pos[1]) + //小车中心点在运行轨道上 + mRect.set((pos[0] - mRectWidth).toInt(), (pos[1] - mRectWidth).toInt(), (pos[0] + mRectWidth).toInt(), (pos[1] + mRectWidth).toInt()) + //小车轮子在运行轨道上 + //mRect.set((pos[0] - mRectWidth).toInt(), (pos[1] - mRectWidth * 2).toInt(), (pos[0] + mRectWidth).toInt(), (pos[1]).toInt()) + if (mCarBitmapRes == null) { + canvas?.drawRect(mRect, mCarPaint) + } else { + canvas?.drawBitmap(mCarBitmapRes, null, mRect, mPaint) + } + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + mStartX = event.x + mStartY = event.y + mMovePath.reset() + mMovePath.moveTo(mStartX, mStartY) + return true + } + MotionEvent.ACTION_MOVE -> { + val endX = (mStartX + event.x) / 2 + val endY = (mStartY + event.y) / 2 + mMovePath.quadTo(mStartX, mStartY, endX, endY) + mStartX = event.x + mStartY = event.y + invalidate() + return true + } + } + return super.onTouchEvent(event) + } + + fun startAnim() { + isMoveCar = true + val valueAnimator = ValueAnimator.ofFloat(0f, pathMeasure.length) + valueAnimator.duration = mDuration * 1000L + valueAnimator.addUpdateListener { + val distance: Float = it.animatedValue as Float + pathMeasure.getPosTan(distance, pos, tan) + invalidate() + } + valueAnimator.addListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) {} + + override fun onAnimationEnd(animation: Animator?) { + isMoveCar = false + } + + override fun onAnimationCancel(animation: Animator?) { + isMoveCar = false + } + + override fun onAnimationStart(animation: Animator?) {} + + }) + valueAnimator.start() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/widget/ClearScreenView.kt b/app/src/main/java/com/allen/androidcustomview/widget/ClearScreenView.kt new file mode 100644 index 0000000..3b5e0bf --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/widget/ClearScreenView.kt @@ -0,0 +1,371 @@ +package com.allen.androidcustomview.widget + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import kotlin.math.abs + + +/** + *
+ * @author : Allen
+ * e-mail : lygttpod@163.com
+ * date : 2021年01月28日
+ * desc : 滑动清屏view
+ * 
+ */ +enum class ClearScreenType { + LEFT_TO_RIGHT,//从左滑到右清屏 + RIGHT_TO_LEFT //从右滑到左清屏 +} + +enum class ClearScreenStatus { + NORMAL,//正常状态 + CLEARED//已经清屏状态 +} + +enum class ClearScreenMode { + QUICK_SCROLL,//快速滑动才触发清屏 + SLOW_SCROLL//滑动出发清屏 +} + +class ClearScreenView @JvmOverloads constructor(private val mContext: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(mContext, attrs, defStyleAttr) { + + companion object { + /** + * 最小移动距离 + */ + private const val MIN_SCROLL_SIZE = 50 + + /** + * 水平方向最小滑动速度 + */ + private const val MIN_X_VELOCITY = 10 + + /** + * 清屏动画时长 + */ + private const val DURATION = 300L + } + + /** + * 手指按下的x轴位置 + */ + private var mDownX = 0 + + /** + * 手指按下的y轴位置 + */ + private var mDownY = 0 + + /** + * 清屏view清屏时需要在x轴的偏移量 + */ + private var translateX = 0 + + /** + * 清屏view起始偏移量(例如:从无到有迁移量从-width到0) + */ + private var startTranslateX = 0 + + /** + * 滑动速度对象 + */ + private var mVelocityTracker: VelocityTracker? = null + + /** + * 清屏动画对象 + */ + private var mAnimator: ValueAnimator? = null + + /** + * 需要清除的Views + */ + private var listClearViews: ArrayList = ArrayList() + + /** + * 清屏事件 + */ + private var clearScreenListener: OnClearScreenListener? = null + + /** + * 清屏类型 左滑清屏 or 右滑清屏 (默认从左滑到右清屏) + */ + private var clearScreenType = ClearScreenType.LEFT_TO_RIGHT + + /** + * 当前清屏状态 + */ + private var clearScreenStatus = ClearScreenStatus.NORMAL + + /** + * 清屏模式 + */ + var clearScreenMode = ClearScreenMode.QUICK_SCROLL + + /** + * 是否正在处在滑动清屏状态 + */ + private var isScrolling = false + + init { + initView() + initAnim() + } + + private fun initView() { + val view = View(mContext) + view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + view.isClickable = true + addView(view, 0) + } + + private fun initAnim() { + mVelocityTracker = VelocityTracker.obtain() + mAnimator = ValueAnimator.ofFloat(0f, 1.0f).setDuration(DURATION) + mAnimator?.addUpdateListener { + val value = it.animatedValue as Float + val translate = startTranslateX + value * translateX + translateChild(translate) + } + mAnimator?.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + when (clearScreenStatus) { + ClearScreenStatus.CLEARED -> { + clearScreenStatus = ClearScreenStatus.NORMAL + clearScreenListener?.onRestored() + } + ClearScreenStatus.NORMAL -> { + clearScreenStatus = ClearScreenStatus.CLEARED + clearScreenListener?.onCleared() + } + } + isScrolling = false + } + }) + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + if (clearScreenListener?.isForbidClearScreen() == true) { + return super.onInterceptTouchEvent(ev) + } + val x = ev.x.toInt() + val y = ev.y.toInt() + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + mDownX = x + mDownY = y + } + } + return isInterceptClearScreenEvent(x, y) + } + + private fun isInterceptClearScreenEvent(x: Int, y: Int): Boolean { + return when (clearScreenMode) { + ClearScreenMode.QUICK_SCROLL -> { + val isIntercept = isMoveForHorizontal(x, y) && !isAnimRunning() && isGreaterThanMinSize(mDownX, x) + requestDisallowInterceptTouchEvent(isIntercept) + isIntercept + } + ClearScreenMode.SLOW_SCROLL -> { + val isIntercept = isMoveForHorizontal(x, y) && !isAnimRunning() || isScrolling + requestDisallowInterceptTouchEvent(isIntercept) + isIntercept + } + } + } + + /** + * 是否水平方向滑动 + */ + private fun isMoveForHorizontal(x: Int, y: Int): Boolean { + return abs(x - mDownX)> abs(y - mDownY) + } + + private fun isAnimRunning(): Boolean { + return mAnimator?.isRunning == true + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (isAnimRunning()) return true + when (clearScreenMode) { + ClearScreenMode.QUICK_SCROLL -> { + handleQuickScrollMode(event) + } + ClearScreenMode.SLOW_SCROLL -> { + handleSlowScrollMode(event) + } + } + return true + } + + private fun handleQuickScrollMode(event: MotionEvent) { + mVelocityTracker?.addMovement(event) + when (event.action) { + MotionEvent.ACTION_UP -> { + mVelocityTracker?.computeCurrentVelocity(10) + val xVelocity = mVelocityTracker?.xVelocity ?: 0f + when { + //从左往右滑动(是正数)速度大于阈值 + xVelocity> MIN_X_VELOCITY -> { + setLeft2RightMoveTranslateX() + } + //从右往左滑动(是负数)速度大于阈值 + xVelocity < -MIN_X_VELOCITY -> { + setRight2LeftMoveTranslateX() + } + else -> { + translateX = 0 + } + } + if (translateX != 0) { + mAnimator?.start() + } + } + } + } + + private fun handleSlowScrollMode(event: MotionEvent) { + val x = event.x.toInt() + when (event.action) { + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + when (clearScreenStatus) { + ClearScreenStatus.NORMAL -> { + if (startTranslateX>= width / 2) { + translateX = width - startTranslateX + clearScreenStatus = ClearScreenStatus.NORMAL + } else { + translateX = -startTranslateX + clearScreenStatus = ClearScreenStatus.CLEARED + } + mAnimator?.start() + } + ClearScreenStatus.CLEARED -> { + if (startTranslateX>= width / 2) { + translateX = width - startTranslateX + clearScreenStatus = ClearScreenStatus.NORMAL + } else { + translateX = (-startTranslateX) + clearScreenStatus = ClearScreenStatus.CLEARED + } + mAnimator?.start() + } + } + } + MotionEvent.ACTION_MOVE -> { + translateX = 0 + val move = x - mDownX + when (clearScreenStatus) { + ClearScreenStatus.NORMAL -> { + val translate = if (x <= mDownX) 0 else move + startTranslateX = translate + if (translate != 0) { + translateChild(translate.toFloat()) + } + } + ClearScreenStatus.CLEARED -> { + val translate = if (x> mDownX) 0 else mDownX - x + startTranslateX = width - translate + if (startTranslateX != 0) { + translateChild(startTranslateX.toFloat()) + } + } + } + } + } + } + + private fun setLeft2RightMoveTranslateX() { + when (clearScreenType) { + ClearScreenType.LEFT_TO_RIGHT -> { + startTranslateX = 0 + translateX = if (clearScreenStatus == ClearScreenStatus.NORMAL) width else 0 + } + ClearScreenType.RIGHT_TO_LEFT -> { + startTranslateX = -width + translateX = if (clearScreenStatus == ClearScreenStatus.NORMAL) 0 else width + } + } + } + + private fun setRight2LeftMoveTranslateX() { + when (clearScreenType) { + ClearScreenType.LEFT_TO_RIGHT -> { + startTranslateX = width + translateX = if (clearScreenStatus == ClearScreenStatus.NORMAL) 0 else -width + } + ClearScreenType.RIGHT_TO_LEFT -> { + startTranslateX = 0 + translateX = if (clearScreenStatus == ClearScreenStatus.NORMAL) -width else 0 + } + } + } + + /** + * 是否大于清屏方向的最小值 + */ + private fun isGreaterThanMinSize(downX: Int, moveX: Int): Boolean { + return when (clearScreenType) { + ClearScreenType.LEFT_TO_RIGHT -> { + when (clearScreenStatus) { + ClearScreenStatus.NORMAL -> moveX - downX> MIN_SCROLL_SIZE + ClearScreenStatus.CLEARED -> downX - moveX> MIN_SCROLL_SIZE + } + } + ClearScreenType.RIGHT_TO_LEFT -> { + when (clearScreenStatus) { + ClearScreenStatus.NORMAL -> downX - moveX> MIN_SCROLL_SIZE + ClearScreenStatus.CLEARED -> moveX - downX> MIN_SCROLL_SIZE + } + } + } + } + + private fun translateChild(translate: Float) { + isScrolling = true + for (view in listClearViews) { + view.translationX = translate + } + } + + fun addClearViews(views: List) { + for (view in views) { + if (!listClearViews.contains(view)) { + listClearViews.add(view) + } + } + } + + fun addClearView(view: View) { + if (!listClearViews.contains(view)) { + listClearViews.add(view) + } + } + + fun removeClearViews(views: List) { + for (view in views) { + listClearViews.remove(view) + } + } + + fun removeAllClearViews() { + listClearViews.clear() + } + + fun setOnClearScreenListener(listener: OnClearScreenListener?) { + this.clearScreenListener = listener + } + + interface OnClearScreenListener { + fun onCleared() + fun onRestored() + fun isForbidClearScreen(): Boolean = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/widget/StudyPlanProgressView.kt b/app/src/main/java/com/allen/androidcustomview/widget/StudyPlanProgressView.kt new file mode 100644 index 0000000..468cf0f --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/widget/StudyPlanProgressView.kt @@ -0,0 +1,163 @@ +package com.allen.androidcustomview.widget + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import com.allen.androidcustomview.R +import kotlin.math.min + +/** + *
+ * @author : Allen
+ * date : 2019年08月12日
+ * desc : 学习计划view
+ * 
+ */ +class StudyPlanProgressView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { + private var mWidth = 0 + private var mHeight = 0 + + private var iconPaint: Paint? = null + private var linePaint: Paint = Paint() + private var textPaint: Paint = Paint() + private var iconRect = Rect() + private var textRectF = Rect() + + private val ALL_POINT_SIZE = 7 + private var cellWidth = 0 + private var iconSize = 0 + private var textSize = 0 + private var textColor = 0 + private var progressWidth = 0 + private var uncheckedProgressColor = Color.GRAY + private var checkedProgressColor = Color.YELLOW + + private var iconUncheckedBitmapRes: Bitmap? = null + private var iconCheckedBitmapRes: Bitmap? = null + + private var dates: MutableList = mutableListOf() + + private var marginLeftAndRight = 0 + + init { + marginLeftAndRight = dp2px(27f).toInt() + iconSize = dp2px(14f).toInt() + initAttr(context, attrs, defStyleAttr) + initPaint() + } + + private fun initAttr(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.StudyPlanProgressView) + iconUncheckedBitmapRes = (typedArray.getDrawable(R.styleable.StudyPlanProgressView_sppv_iconUnchecked) as? BitmapDrawable)?.bitmap + iconCheckedBitmapRes = (typedArray.getDrawable(R.styleable.StudyPlanProgressView_sppv_iconChecked) as? BitmapDrawable)?.bitmap + iconSize = typedArray.getDimensionPixelOffset(R.styleable.StudyPlanProgressView_sppv_iconSize, dp2px(14f).toInt()) + progressWidth = typedArray.getDimensionPixelOffset(R.styleable.StudyPlanProgressView_sppv_progressWidth, dp2px(2f).toInt()) + uncheckedProgressColor = typedArray.getColor(R.styleable.StudyPlanProgressView_sppv_uncheckedProgressColor, uncheckedProgressColor) + checkedProgressColor = typedArray.getColor(R.styleable.StudyPlanProgressView_sppv_checkedProgressColor, checkedProgressColor) + textSize = typedArray.getDimensionPixelSize(R.styleable.StudyPlanProgressView_sppv_textSize, sp2px(11)) + textColor = typedArray.getColor(R.styleable.StudyPlanProgressView_sppv_textColor, checkedProgressColor) + + typedArray.recycle() + } + + private fun initPaint() { + iconPaint = Paint(Paint.ANTI_ALIAS_FLAG) + iconPaint?.isFilterBitmap = true + iconPaint?.isDither = true + + linePaint.strokeWidth = progressWidth.toFloat() + linePaint.color = uncheckedProgressColor + linePaint.isAntiAlias = true + linePaint.style = Paint.Style.FILL_AND_STROKE + + textPaint.textSize = textSize.toFloat() + textPaint.color = textColor + textPaint.textAlign = Paint.Align.CENTER + textPaint.isAntiAlias = true + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mWidth = w + mHeight = h + + cellWidth = (mWidth - marginLeftAndRight * 2 - iconSize * ALL_POINT_SIZE) / (ALL_POINT_SIZE - 1) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + drawProgressLine(canvas) + drawProgress(canvas) + } + + private fun drawProgressLine(canvas: Canvas) { + val lineStart = (marginLeftAndRight + iconSize / 2).toFloat() + linePaint.color = uncheckedProgressColor + canvas.drawLine(lineStart, (mHeight / 2).toFloat(), (mWidth - marginLeftAndRight - iconSize / 2).toFloat(), (mHeight / 2).toFloat(), linePaint) + if (getDataSize() == 0) return + val lineEnd = lineStart + (getDataSize() - 1) * (cellWidth + iconSize) + linePaint.color = checkedProgressColor + canvas.drawLine(lineStart, (mHeight / 2).toFloat(), lineEnd, (mHeight / 2).toFloat(), linePaint) + } + + private fun drawProgress(canvas: Canvas) { + if (iconUncheckedBitmapRes == null || iconCheckedBitmapRes == null) return + for (i in 0 until ALL_POINT_SIZE) { + val left = marginLeftAndRight + (cellWidth + iconSize) * i + iconRect.set(left, (mHeight - iconSize) / 2, left + iconSize, (mHeight + iconSize) / 2) + val bitmap = if (isFinished(i)) iconCheckedBitmapRes else iconUncheckedBitmapRes + canvas.drawBitmap(bitmap, null, iconRect, iconPaint) + drawText(canvas, i) + } + } + + private fun isFinished(position: Int): Boolean { + return if (position <= getDataSize() - 1) { + dates[position].isFinished + } else false + } + + private fun drawText(canvas: Canvas, position: Int) { + if (getDataSize() == 0) return + if (position < getDataSize()) { + val textWidth = textPaint.measureText(dates[position].content).toInt() + val isTop = position % 2 == 0 + val left = marginLeftAndRight + iconSize / 2 + (cellWidth + iconSize) * position - textWidth / 2 + textRectF.left = left + textRectF.right = left + textWidth + textRectF.top = if (isTop) 0 else (mHeight + iconSize) / 2 + textRectF.bottom = if (isTop) (mHeight - iconSize) / 2 else mHeight + + val fontMetrics = textPaint.fontMetricsInt + val baseline = (textRectF.bottom + textRectF.top - fontMetrics.bottom - fontMetrics.top) / 2 + //文字绘制到整个布局的中心位置 + canvas.drawText(dates[position].content, textRectF.centerX().toFloat(), baseline.toFloat(), textPaint) + } + } + + fun setData(date: List?) { + dates.clear() + val timeList = date?.toMutableList() ?: mutableListOf() + timeList.forEach { + dates.add(ProgressData(it, true)) + } + invalidate() + } + + private fun getDataSize() = min(dates.size, 7) + + fun dp2px(dpVal: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + dpVal, resources.displayMetrics) + } + + fun sp2px(spVal: Int): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + spVal.toFloat(), resources.displayMetrics).toInt() + } + + class ProgressData(var content: String, var isFinished: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteContainerView.kt b/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteContainerView.kt new file mode 100644 index 0000000..9cf7f2c --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteContainerView.kt @@ -0,0 +1,283 @@ +package com.allen.androidcustomview.widget.vote + +import android.content.Context +import android.graphics.Outline +import android.os.Build +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewOutlineProvider +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.Toast +import com.allen.androidcustomview.R +import com.allen.androidcustomview.bean.VoteBean +import com.allen.androidcustomview.bean.VoteOption +import kotlinx.android.synthetic.main.widget_vote_layout.view.* +import java.lang.ref.WeakReference + +/** + *
+ * @author : Allen
+ * date : 2019年08月01日
+ * desc :
+ * 
+ */ +class VoteContainerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { + + private var voteViewHolders: ArrayList = arrayListOf() + + var onVoteClickListener: OnVoteClickListener? = null + + companion object { + val VOTE_TYPE_MULTIPLE = "multiple" + val VOTE_TYPE_SINGLE = "single" + } + + private var optionIds = arrayListOf() + + private var mData: VoteBean? = null + + init { + initView() + } + + private fun initView() { + LayoutInflater.from(context).inflate(R.layout.widget_vote_layout, this) + + vote_container_vote_btn.setOnClickListener { + if (mData == null) return@setOnClickListener + onVoteClickListener?.onVoteCommitBtnClick(mData, optionIds) + } + } + + fun setVoteData(data: VoteBean?) { + if (data == null || data.options.isNullOrEmpty()) { + visibility = View.GONE + } else { + mData = data + setVoteTitle(data.title) + voteViewHolders.clear() + optionIds.clear() + setVoteStatus(data) + setVoteBtnStatus() + vote_item_ll.removeAllViews() + data.options?.forEachIndexed { index, voteOption -> + val viewHolder = onCreateViewHolder() + viewHolder.bind(index, voteOption, data) + voteViewHolders.add(viewHolder) + vote_item_ll.addView(viewHolder.voteView) + } + } + } + + private fun setVoteTitle(title: String?) { + vote_container_title.text = title ?: "" + } + + private fun setVoteStatus(vote: VoteBean) { + val isVoteMulti = vote.choiceType == VOTE_TYPE_MULTIPLE + val voteResult = vote.sumVoteCount ?: 0 + val voted = vote.voted + vote_container_vote_btn.visibility = if (voted != true && isVoteMulti) View.VISIBLE else View.GONE + vote_container_vote_result.visibility = if (voted == true) View.VISIBLE else View.GONE + setVoteResult("共${voteResult}人参与了投票") + } + + private fun addOptionIds(id: Int) { + optionIds.add(id) + } + + + private fun removeOptionIds(id: Int) { + optionIds.remove(id) + } + + private fun getOptionIdsSize(): Int { + return optionIds.size + } + + private fun setVoteBtnStatus() { + val clickable = getOptionIdsSize()> 0 + if (clickable) { + vote_container_vote_btn.setBackgroundResource(R.drawable.shape_bg_clickable) + } else { + vote_container_vote_btn.setBackgroundResource(R.drawable.shape_bg_un_clickable) + } + vote_container_vote_btn.isClickable = clickable + } + + fun refreshDataAfterVoteSuccess() { + vote_container_vote_result.visibility = View.VISIBLE + vote_container_vote_btn.visibility = View.GONE + refreshVoteResult() + startProgressAnim() + } + + fun refreshDataAfterVoteFailed() { + voteViewHolders.forEach { + it.resetDataAfterSingleVoteFailed() + } + } + + fun onDestroy() { + voteViewHolders.forEach { + it.onVoteDestroy() + } + } + + private fun refreshVoteResult() { + val voteResult = (mData?.sumVoteCount ?: 0) + setVoteResult("共${(voteResult + 1)}人参与了投票") + } + + private fun setVoteResult(result: String?) { + vote_container_vote_result.text = result ?: "" + } + + private fun startProgressAnim() { + voteViewHolders.forEach { + it.setProgress() + } + } + + private fun onCreateViewHolder(): VoteItemViewHolder { + return VoteItemViewHolder(getVoteView(context), this) + } + + class VoteItemViewHolder(var voteView: VoteView, voteContainerView: VoteContainerView) { + + private val voteContainerViewRef = WeakReference(voteContainerView) + + private fun ref(): VoteContainerView? { + return voteContainerViewRef.get() + } + + private var data: VoteOption? = null + private var mainVote: VoteBean? = null + + var isVoteMulti = true + + fun bind(position: Int, voteOption: VoteOption, mainVote: VoteBean) { + this.data = voteOption + this.mainVote = mainVote + + isVoteMulti = mainVote.choiceType == VOTE_TYPE_MULTIPLE + + val voteResultCount = voteOption.showCount ?: 0 + + voteView.setVoteIsSelected(voteOption.voted ?: false) + .setVoteContent(voteOption.content) + .setVoteResultText("${voteResultCount}人").refreshView() + + if (isHaveVoted()) { + val sum = mainVote?.sumVoteCount ?: 0 + val showCount = data?.showCount ?: 0 + val progress = if (sum == 0) 0f else showCount.toFloat() / sum.toFloat() + + voteView.setProgress(progress) + } + + voteView.setOnClickListener { + if (isHaveVoted()) return@setOnClickListener + if (isVoteMulti) { + setMultiChoice(voteView, voteOption) + ref()?.onVoteClickListener?.onVoteItemClick(mainVote, data) + } else { + if (ref()?.getOptionIdsSize() ?: 0> 0) return@setOnClickListener + setSingleChoice(voteView, voteOption) + ref()?.onVoteClickListener?.onVoteItemClick(mainVote, data) + } + } + + } + + private fun isHaveVoted(): Boolean { + return mainVote?.voted ?: false + } + + fun setProgress() { + val sum = mainVote?.sumVoteCount ?: 0 + var showCount = data?.showCount ?: 0 + val realShowCount = if (data?.voted == true) showCount + 1 else showCount + voteView.setVoteResultText("${realShowCount}人") + mainVote?.voted = true + val progress = if (sum == 0) 0f else realShowCount.toFloat() / sum.toFloat() + voteView.setProgressWithAnim(progress) + } + + fun resetDataAfterSingleVoteFailed() { + if (isVoteMulti) return + data?.voted = false + ref()?.removeOptionIds(data?.id ?: 0) + voteView.setVoteIsSelected(data?.voted ?: false).refreshView() + } + + fun onVoteDestroy() { + voteView.onDestroy() + } + + private fun setMultiChoice(voteView: VoteView, voteOption: VoteOption) { + if (voteOption.voted == true) { + voteOption.voted = false + ref()?.removeOptionIds(voteOption.id ?: 0) + } else { + val optionsIdSize = ref()?.optionIds?.size ?: 0 + val maxSelect = mainVote?.maxSelect ?: 0 + if (optionsIdSize < maxSelect) { + voteOption.voted = true + ref()?.addOptionIds(voteOption.id ?: 0) + } else { + Toast.makeText(ref()?.context, "最多可选${maxSelect}个", Toast.LENGTH_SHORT).show() + } + } + ref()?.setVoteBtnStatus() + voteView.setVoteIsSelected(voteOption.voted ?: false).refreshView() + } + + private fun setSingleChoice(voteView: VoteView, voteOption: VoteOption) { + voteOption.voted = true + ref()?.addOptionIds(voteOption.id ?: 0) + voteView.setVoteIsSelected(voteOption.voted ?: false).refreshView() + ref()?.onVoteClickListener?.onVoteCommitBtnClick(mainVote, ref()?.optionIds + ?: arrayListOf()) + } + + } + + private fun getVoteView(context: Context): VoteView { + val voteView = VoteView(context) + voteView.setVoteTextSize(voteView.sp2px(15)) + .setVoteUncheckedContentTextColor(resources.getColor(R.color.unchecked_content_text_color)) + .setVoteCheckedContentTextColor(resources.getColor(R.color.checked_content_text_color)) + .setVoteUncheckedResultTextColor(resources.getColor(R.color.unchecked_result_text_color)) + .setVoteCheckedResultTextColor(resources.getColor(R.color.checked_result_text_color)) + .setVoteUncheckedProgressColor(resources.getColor(R.color.unchecked_progress_color)) + .setVoteCheckedProgressColor(resources.getColor(R.color.checked_progress_color)) + .setVoteCheckedIcon(resources.getDrawable(R.mipmap.icon_vote_check)) + .setVoteBorderRadius(voteView.dp2px(3f)) + .setVoteBorderColor(resources.getColor(R.color.border_color)) + .setVoteRightIconSize(voteView.dp2px(18f).toInt()) + .setVoteAnimDuration(2000L) + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.LOLLIPOP) { + voteView.clipToOutline = true + voteView.outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, view.width, view.height, voteView.dp2px(3f)) + } + } + } + + val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, voteView.dp2px(40f).toInt()) + layoutParams.bottomMargin = voteView.dp2px(12f).toInt() + voteView.layoutParams = layoutParams + return voteView + } + + + interface OnVoteClickListener { + fun onVoteCommitBtnClick(mainVote: VoteBean?, optionIds: ArrayList) + fun onVoteItemClick(mainVote: VoteBean?, voteOption: VoteOption?) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteLayoutAdapter.kt b/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteLayoutAdapter.kt new file mode 100644 index 0000000..95a7eca --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteLayoutAdapter.kt @@ -0,0 +1,89 @@ +package com.allen.androidcustomview.widget.vote + +import android.view.View +import android.view.ViewGroup +import com.allen.androidcustomview.bean.VoteBean +import com.allen.androidcustomview.bean.VoteOption +import java.lang.ref.WeakReference + +/** + *
+ * @author : Allen
+ * date : 2019年08月03日
+ * desc :
+ * 
+ */ +class VoteLayoutAdapter(private val viewGroup: ViewGroup) { + + private var viewHolders = mutableListOf() + + var onVoteClickListener: OnVoteClickListener? = null + + fun setData(vote: ArrayList?) { + viewGroup.removeAllViews() + viewHolders.clear() + if (vote == null || vote.size <= 0) { + viewGroup.visibility = View.GONE + } else { + viewGroup.visibility = View.VISIBLE + val size = vote.size + for (i in 0 until size) { + val viewHolder = onCreateViewHolder(viewGroup, i) + viewHolder.bind(vote[i]) + viewHolders.add(viewHolder) + viewGroup.addView(viewHolder.voteContainerView) + } + } + } + + fun refreshDataAfterVotedSuccess(position: Int) { + viewHolders[position].voteContainerView.refreshDataAfterVoteSuccess() + } + + fun refreshDataAfterVotedFailed(position: Int) { + viewHolders[position].voteContainerView.refreshDataAfterVoteFailed() + } + + fun onDestroy() { + viewHolders.forEach { + it.voteContainerView.onDestroy() + } + } + + private fun onCreateViewHolder(viewGroup: ViewGroup, position: Int): VoteViewHolder { + val view = VoteContainerView(viewGroup.context) + return VoteViewHolder(view, this, position) + } + + class VoteViewHolder(view: VoteContainerView, adapter: VoteLayoutAdapter, var position: Int) { + + private val adapterRef = WeakReference(adapter) + private fun ref(): VoteLayoutAdapter? { + return adapterRef.get() + } + + var voteContainerView = view + private var mMainVote: VoteBean? = null + + fun bind(mainVote: VoteBean) { + mMainVote = mainVote + voteContainerView.setVoteData(mainVote) + voteContainerView.onVoteClickListener = object : VoteContainerView.OnVoteClickListener { + override fun onVoteCommitBtnClick(mainVote: VoteBean?, optionIds: ArrayList) { + ref()?.onVoteClickListener?.onVoteCommitBtnClick(mainVote, optionIds, position) + } + + override fun onVoteItemClick(mainVote: VoteBean?, voteOption: VoteOption?) { + ref()?.onVoteClickListener?.onVoteItemClick(mainVote, voteOption, position) + } + + } + } + + } + + interface OnVoteClickListener { + fun onVoteCommitBtnClick(mainVote: VoteBean?, optionIds: ArrayList, position: Int) + fun onVoteItemClick(mainVote: VoteBean?, voteOption: VoteOption?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteView.kt b/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteView.kt new file mode 100644 index 0000000..7a0845c --- /dev/null +++ b/app/src/main/java/com/allen/androidcustomview/widget/vote/VoteView.kt @@ -0,0 +1,419 @@ +package com.allen.androidcustomview.widget.vote + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import android.view.animation.DecelerateInterpolator +import com.allen.androidcustomview.R +import kotlin.math.max + +/** + *
+ * @author : Allen
+ * date : 2019年07月30日
+ * desc :
+ * 
+ */ +class VoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { + + private var mWidth = 0 + private var mHeight = 0 + + private var bgRectF = RectF() + private var progressRectF = RectF() + private var voteContentRectF = Rect() + private var voteResultRectF = Rect() + private var voteRightIconRectF = Rect() + + private var voteResultBaseline = 0 + private var voteContentBaseline = 0 + + private var progressPaint: Paint? = null + private var iconPaint: Paint? = null + private var bgPaint: Paint? = null + private var borderPaint: Paint? = null + + private var voteContentTextPaint: Paint? = null + private var voteResultTextPaint: Paint? = null + + private var animDuration = 1000L + + private var mScale = 1f + + private var mProgress = -1f + private var mVoteContent: String? = null + private var mVoteResult: String? = null + + private var valueAnimator: ValueAnimator? = null + + private var textMarginLeft = 0 + private var voteResultMarginRight = 0 + + private var textPaintSize: Int = 0 + + private var rightCheckedBitmapRes: Bitmap? = null + + private var rightIconWidth = 0 + private var rightIconHeight = 0 + + private var checkedProgressColor = 0 + private var unCheckedProgressColor = 0 + + private var checkedContentTextColor = 0 + private var uncheckedContentTextColor = 0 + + private var checkedResultTextColor = 0 + private var uncheckedResultTextColor = 0 + + private var borderColor = 0 + private var borderRadius = 0f + + private var isVoteChecked = false + private var textWidth = 0 + + private val defaultCheckedProgressColor = Color.argb(1, 255, 124, 5) + private val defaultUncheckedProgressColor = Color.parseColor("#F3F3F3") + private val defaultCheckedTextColor = Color.parseColor("#FF7C05") + private val defaultUncheckedTextColor = Color.parseColor("#1a1a1a") + private val defaultBorderColor = Color.parseColor("#e6e6e6") + + init { + + textMarginLeft = dp2px(15f).toInt() + voteResultMarginRight = dp2px(15f).toInt() + + initAttr(context, attrs, defStyleAttr) + + initPaint() + + initVoteRightIcon() + + initColor() + } + + private fun initColor() { + voteContentTextPaint?.color = if (isVoteChecked) checkedContentTextColor else uncheckedContentTextColor + voteResultTextPaint?.color = if (isVoteChecked) checkedResultTextColor else uncheckedResultTextColor + progressPaint?.color = if (isVoteChecked) checkedProgressColor else unCheckedProgressColor + bgPaint?.color = if (isVoteChecked) checkedProgressColor else unCheckedProgressColor + } + + private fun initVoteRightIcon() { + if (rightCheckedBitmapRes != null) { + if (rightIconWidth == 0 || rightIconHeight == 0) { + rightIconWidth = dp2px(36f).toInt() + rightIconHeight = dp2px(36f).toInt() + } + } + } + + private fun initAttr(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.VoteView) + + checkedProgressColor = typedArray.getColor(R.styleable.VoteView_voteCheckedProgressColor, defaultCheckedProgressColor) + unCheckedProgressColor = typedArray.getColor(R.styleable.VoteView_voteUncheckedProgressColor, defaultUncheckedProgressColor) + + checkedContentTextColor = typedArray.getColor(R.styleable.VoteView_voteCheckedContentTextColor, defaultCheckedTextColor) + uncheckedContentTextColor = typedArray.getColor(R.styleable.VoteView_voteUncheckedContentTextColor, defaultUncheckedTextColor) + + checkedResultTextColor = typedArray.getColor(R.styleable.VoteView_voteCheckedResultTextColor, defaultCheckedTextColor) + uncheckedResultTextColor = typedArray.getColor(R.styleable.VoteView_voteUncheckedResultTextColor, defaultUncheckedTextColor) + + textPaintSize = typedArray.getDimensionPixelSize(R.styleable.VoteView_voteTextSize, sp2px(15)) + + borderColor = typedArray.getColor(R.styleable.VoteView_voteBorderColor, defaultBorderColor) + borderRadius = typedArray.getDimensionPixelOffset(R.styleable.VoteView_voteBorderRadius, dp2px(1f).toInt()).toFloat() + + animDuration = typedArray.getInt(R.styleable.VoteView_voteAnimDuration, 500).toLong() + + rightCheckedBitmapRes = (typedArray.getDrawable(R.styleable.VoteView_voteCheckedIcon) as? BitmapDrawable)?.bitmap + + rightIconWidth = typedArray.getDimensionPixelOffset(R.styleable.VoteView_voteRightIconWidth, 0) + rightIconHeight = typedArray.getDimensionPixelOffset(R.styleable.VoteView_voteRightIconHeight, 0) + + typedArray.recycle() + } + + private fun initPaint() { + iconPaint = Paint(Paint.ANTI_ALIAS_FLAG) + iconPaint?.isFilterBitmap = true + iconPaint?.isDither = true + + bgPaint = getPaint(dp2px(0.5f), Color.WHITE, Paint.Style.FILL) + progressPaint = getPaint(dp2px(0.5f), unCheckedProgressColor, Paint.Style.FILL) + borderPaint = getPaint(dp2px(0.5f), borderColor, Paint.Style.STROKE) + + voteContentTextPaint = getTextPaint(uncheckedContentTextColor, textPaintSize.toFloat()) + voteResultTextPaint = getTextPaint(uncheckedResultTextColor, textPaintSize.toFloat()) + } + + private fun getPaint(strokeWidth: Float, color: Int, style: Paint.Style): Paint { + val paint = Paint(Paint.ANTI_ALIAS_FLAG) + paint.strokeWidth = strokeWidth + paint.color = color + paint.isAntiAlias = true + paint.style = style + return paint + } + + private fun getTextPaint(color: Int, textSize: Float): Paint { + val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) + textPaint.textSize = textSize + textPaint.color = color + textPaint.textAlign = Paint.Align.CENTER + textPaint.isAntiAlias = true +// textPaint.typeface = Typeface.DEFAULT_BOLD + return textPaint + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mWidth = w + mHeight = h + + setBgRect() + setProgressRect() + + setVoteResultRect() + setVoteContentRect() + setVoteRightIconRect() + + } + + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + drawBg(canvas) + drawProgress(canvas) + drawBorder(canvas) + + drawVoteContentText(canvas) + drawVoteResultText(canvas) + drawVoteRightIcon(canvas) + } + + + private fun setBgRect() { + bgRectF.set(0f, 0f, mWidth.toFloat(), mHeight.toFloat()) + } + + private fun setProgressRect() { + progressRectF.set(0f, 0f, 0f, mHeight.toFloat()) + } + + private fun setVoteResultRect() { + if (mVoteResult.isNullOrBlank()) return + voteResultTextPaint!!.getTextBounds(mVoteResult, 0, mVoteResult!!.length, voteResultRectF) + + voteResultRectF.top = 0 + voteResultRectF.bottom = mHeight + + val fontMetrics = voteResultTextPaint!!.fontMetricsInt + voteResultBaseline = (voteResultRectF.bottom + voteResultRectF.top - fontMetrics.bottom - fontMetrics.top) / 2 + } + + private fun setVoteContentRect() { + if (mVoteContent.isNullOrBlank()) return + voteContentTextPaint!!.getTextBounds(mVoteContent, 0, mVoteContent!!.length, voteContentRectF) + + textWidth = (voteContentRectF.right - voteContentRectF.left) + voteContentRectF.top = 0 + voteContentRectF.bottom = mHeight + voteContentRectF.left = (mWidth - textWidth) / 2 + voteContentRectF.right = voteContentRectF.left + textWidth + + val fontMetrics = voteContentTextPaint!!.fontMetricsInt + voteContentBaseline = (voteContentRectF.bottom + voteContentRectF.top - fontMetrics.bottom - fontMetrics.top) / 2 + } + + private fun setVoteRightIconRect() { + voteRightIconRectF.set(voteContentRectF.right + voteResultMarginRight, (mHeight - rightIconHeight) / 2, voteContentRectF.right + voteResultMarginRight + rightIconWidth, (mHeight + rightIconHeight) / 2) + } + + + private fun drawBg(canvas: Canvas) { + if (mProgress != -1f) { + bgPaint?.color = Color.WHITE + } + canvas.drawRoundRect(bgRectF, 0f, 0f, bgPaint!!) + } + + private fun drawProgress(canvas: Canvas) { + if (mProgress == -1f) return + canvas.drawRoundRect(getProgressRectF(), 0f, 0f, progressPaint!!) + } + + private fun drawBorder(canvas: Canvas) { + borderPaint?.color = borderColor + canvas.drawRoundRect(bgRectF, borderRadius, borderRadius, borderPaint) + } + + private fun drawVoteContentText(canvas: Canvas) { + if (mVoteContent.isNullOrBlank()) return + //文字绘制到整个布局的中心位置 + if (mProgress == -1f) { + voteContentRectF.left = (mWidth - textWidth) / 2 + voteContentRectF.right = voteContentRectF.left + textWidth + } else { + voteContentRectF.left = max(((1 - mScale) * (mWidth - textWidth) / 2).toInt(), textMarginLeft) + voteContentRectF.right = voteContentRectF.left + textWidth + } + + canvas.drawText(mVoteContent, voteContentRectF.centerX().toFloat(), voteContentBaseline.toFloat(), voteContentTextPaint) + } + + private fun drawVoteResultText(canvas: Canvas) { + if (mProgress == -1f || mVoteResult.isNullOrBlank()) return + //文字绘制到整个布局的中心位置 + voteResultTextPaint?.alpha = (255 * mScale).toInt() + canvas.drawText(mVoteResult, mWidth - voteResultMarginRight - voteResultRectF.centerX().toFloat(), voteResultBaseline.toFloat(), voteResultTextPaint) + } + + private fun drawVoteRightIcon(canvas: Canvas) { + if (rightCheckedBitmapRes != null && isVoteChecked) { + voteRightIconRectF.left = voteContentRectF.right + voteResultMarginRight + voteRightIconRectF.right = voteRightIconRectF.left + rightIconWidth + canvas.drawBitmap(rightCheckedBitmapRes!!, null, voteRightIconRectF, iconPaint) + } + } + + private fun getProgressRectF(): RectF { +// val currentProgress = mProgress * mWidth * mScale / 100 + val currentProgress = mProgress * mWidth * mScale + progressRectF.set(0f, 0f, currentProgress, mHeight.toFloat()) + return progressRectF + } + + + fun setVoteCheckedProgressColor(color: Int): VoteView { + this.checkedProgressColor = color + return this + } + + fun setVoteUncheckedProgressColor(color: Int): VoteView { + this.unCheckedProgressColor = color + return this + } + + fun setVoteBorderRadius(radius: Float): VoteView { + this.borderRadius = radius + return this + } + + fun setVoteBorderColor(color: Int): VoteView { + this.borderColor = color + return this + } + + fun setVoteCheckedContentTextColor(color: Int): VoteView { + this.checkedContentTextColor = color + return this + } + + fun setVoteUncheckedContentTextColor(color: Int): VoteView { + this.uncheckedContentTextColor = color + return this + } + + + fun setVoteCheckedResultTextColor(color: Int): VoteView { + this.checkedResultTextColor = color + return this + } + + fun setVoteUncheckedResultTextColor(color: Int): VoteView { + this.uncheckedResultTextColor = color + return this + } + + fun setVoteCheckedIcon(iconBitmap: Drawable): VoteView { + this.rightCheckedBitmapRes = (iconBitmap as? BitmapDrawable)?.bitmap + return this + } + + fun setVoteRightIconSize(width_height: Int): VoteView { + this.rightIconWidth = width_height + this.rightIconHeight = width_height + return this + } + + fun setVoteTextSize(textSize: Int): VoteView { + this.textPaintSize = textSize + return this + } + + fun setVoteAnimDuration(duration: Long): VoteView { + this.animDuration = duration + return this + } + + fun setVoteContent(content: String?): VoteView { + mVoteContent = content ?: "" + setVoteContentRect() + return this + } + + fun setVoteResultText(voteResult: String?): VoteView { + mVoteResult = voteResult ?: "" + setVoteResultRect() + return this + } + + fun refreshView() { + initColor() + invalidate() + } + + + fun setProgress(progress: Float) { + mProgress = progress + if (mProgress != -1f) { + invalidate() + } + } + + fun setProgressWithAnim(progress: Float) { + mProgress = progress + startAnim() + } + + private fun startAnim() { + valueAnimator?.cancel() + if (valueAnimator == null) { + valueAnimator = ValueAnimator.ofFloat(0f, 1f) + } + valueAnimator?.duration = animDuration + valueAnimator?.interpolator = DecelerateInterpolator() + valueAnimator?.addUpdateListener { + mScale = it.animatedValue as Float + invalidate() + } + valueAnimator?.start() + } + + fun onDestroy() { + valueAnimator?.cancel() + valueAnimator = null + } + + fun setVoteIsSelected(isVoteSelected: Boolean): VoteView { + this.isVoteChecked = isVoteSelected + return this + } + + fun dp2px(dpVal: Float): Float { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + dpVal, resources.displayMetrics) + } + + fun sp2px(spVal: Int): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + spVal.toFloat(), resources.displayMetrics).toInt() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_bg_clickable.xml b/app/src/main/res/drawable/shape_bg_clickable.xml new file mode 100644 index 0000000..5994224 --- /dev/null +++ b/app/src/main/res/drawable/shape_bg_clickable.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_bg_un_clickable.xml b/app/src/main/res/drawable/shape_bg_un_clickable.xml new file mode 100644 index 0000000..902bdea --- /dev/null +++ b/app/src/main/res/drawable/shape_bg_un_clickable.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_vote_item_bg.xml b/app/src/main/res/drawable/shape_vote_item_bg.xml new file mode 100644 index 0000000..594eaf2 --- /dev/null +++ b/app/src/main/res/drawable/shape_vote_item_bg.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_clear_screen.xml b/app/src/main/res/layout/activity_clear_screen.xml new file mode 100644 index 0000000..675b590 --- /dev/null +++ b/app/src/main/res/layout/activity_clear_screen.xml @@ -0,0 +1,40 @@ + + + + + + + + + +