網(wǎng)站首頁 編程語言 正文
前言
個人打算開發(fā)個視頻編輯的APP,然后把一些用上的技術(shù)總結(jié)一下,這次主要是APP的底部菜單欄用到了一個自定義View去繪制實現(xiàn)的,所以這次主要想講講自定義View的一些用到的點和自己如何去DIY一個不一樣的自定義布局。
實現(xiàn)的效果和思路
可以先看看實現(xiàn)的效果
兩個頁面的內(nèi)容還沒做,當前就是一個Demo,可以看到底部的菜單欄是一個繪制出來的不規(guī)則的一個布局,那要如何實現(xiàn)呢。可以先來看看它的一個繪制區(qū)域:
就是一個底部的布局和3個子view,底部的區(qū)域當然也是個規(guī)則的區(qū)域,只不過我們是在這塊區(qū)域上去進行繪制。
可以把整個過程分為幾個步驟:
1. 繪制底部布局
- (1) 繪制矩形區(qū)域
- (2) 繪制外圓形區(qū)域
- (3) 繪制內(nèi)圓形區(qū)域
2. 添加子view進行布局
3. 處理事件分發(fā)的區(qū)域 (底部菜單上邊的白色區(qū)域不觸發(fā)菜單的事件)
4. 寫個動畫意思意思
1. 繪制底部布局
這里做的話就沒必要手動去添加view這些了,直接全部手動繪制就行。
companion object{
const val DIMENS_64 = 64.0
const val DIMENS_96 = 96.0
const val DIMENS_50 = 50.0
const val DIMENS_48 = 48.0
interface OnChildClickListener{
fun onClick(index : Int)
}
}
private var paint : Paint ?= null // 繪制藍色區(qū)域的畫筆
private var paint2 : Paint ?= null // 繪制白色內(nèi)圓的畫筆
private var allHeight : Int = 0 // 總高度,就是繪制的范圍
private var bgHeight : Int = 0 // 背景的高度,就是藍色矩陣的范圍
private var mRadius : Int = 0 // 外圓的高度
private var mChildSize : Int = 0
private var mChildCenterSize : Int = 0
private var mWidthZone1 : Int = 0
private var mWidthZone2 : Int = 0
private var mChildCentre : Int = 0
private var childViews : MutableList<View> = mutableListOf()
private var objectAnimation : ObjectAnimator ?= null
var onChildClickListener : OnChildClickListener ?= null
init {
initView()
}
private fun initView(){
val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
DimensionUtils.dp2px(context, DIMENS_64).toInt())
layoutParams = lp
allHeight = DimensionUtils.dp2px(context, DIMENS_96).toInt()
bgHeight = DimensionUtils.dp2px(context, DIMENS_64).toInt()
mRadius = DimensionUtils.dp2px(context, DIMENS_50).toInt()
mChildSize = DimensionUtils.dp2px(context, DIMENS_48).toInt()
mChildCenterSize = DimensionUtils.dp2px(context, DIMENS_64).toInt()
setWillNotDraw(false)
initPaint()
}
private fun initPaint(){
paint = Paint()
paint?.isAntiAlias = true
paint?.color = context.resources.getColor(R.color.kylin_main_color)
paint2 = Paint()
paint2?.isAntiAlias = true
paint2?.color = context.resources.getColor(R.color.kylin_third_color)
}
上邊是先把一些尺寸給定義好(我這邊是沒有設計圖,自己去直接調(diào)整的,所以可能有些視覺效果不太好,如果有設計師幫忙的話效果肯定會好些),繪制流程就是繪制3個形狀,然后代碼里也加了些注釋哪個變量有什么用,這步應該不難,沒什么可以多解釋的。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val wSize = MeasureSpec.getSize(widthMeasureSpec)
// 拿到子view做操作的,和這步無關(guān),可以先不看
if (childViews.size <= 0) {
for (i in 0 until childCount) {
val cView = getChildAt(i)
initChildView(cView, i)
childViews.add(cView)
if (i == childCount/2){
val ms: Int = MeasureSpec.makeMeasureSpec(mRadius, MeasureSpec.AT_MOST)
measureChild(cView, ms, ms)
}else {
val ms: Int = MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.AT_MOST)
measureChild(cView, ms, ms)
}
}
}
setMeasuredDimension(wSize, allHeight)
}
這步其實也很簡單,就是說給當前自定義view設置高度為allHeight
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 繪制長方形區(qū)域
canvas?.drawRect(left.toFloat(), ((allHeight - bgHeight).toFloat()),
right.toFloat(), bottom.toFloat(), paint!!)
// 繪制圓形區(qū)域
paint?.let {
canvas?.drawCircle(
(width/2).toFloat(), mRadius.toFloat(),
mRadius.toFloat(),
it
)
}
// 繪制內(nèi)圓區(qū)域
paint2?.let {
canvas?.drawCircle(
(width/2).toFloat(), mRadius.toFloat(),
(mRadius - 28).toFloat(),
it
)
}
}
最后進行繪制, 就是上面說的繪制3個圖形,代碼里的注釋也說得很清楚。
2. 添加子view
我這里是外面布局去加子view的,想弄得靈活點(但感覺也不太好,后面還是想改成里面定義一套規(guī)范來弄會好些,如果自由度太高的話去做自定義就很麻煩,而且實際開發(fā)中這種需求也沒必要把擴展性做到這種地步,基本就是整個APP只有一個地方使用)
但是這邊也只是一個Demo先做個演示。
<com.kylin.libkcommons.widget.BottomMenuBar android:id="@+id/bv_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/home" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/video" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/more" /> </com.kylin.libkcommons.widget.BottomMenuBar>
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val wSize = MeasureSpec.getSize(widthMeasureSpec)
if (childViews.size <= 0) {
for (i in 0 until childCount) {
val cView = getChildAt(i)
initChildView(cView, i)
childViews.add(cView)
if (i == childCount/2){
val ms: Int = MeasureSpec.makeMeasureSpec(mRadius, MeasureSpec.AT_MOST)
measureChild(cView, ms, ms)
}else {
val ms: Int = MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.AT_MOST)
measureChild(cView, ms, ms)
}
}
}
setMeasuredDimension(wSize, allHeight)
}
拿到子view進行一個管理,做一些初始化的操作,主要是設點擊事件這些,這里不是很重要。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (mChildCentre == 0){
mChildCentre = width / 6
}
// 輔助事件分發(fā)區(qū)域
if (mWidthZone1 == 0 || mWidthZone2 == 0) {
mWidthZone1 = width / 2 - mRadius / 2
mWidthZone2 = width / 2 + mRadius / 2
}
// 設置每個子view的顯示區(qū)域
for (i in 0 until childViews.size) {
if (i == childCount/2){
childViews[i].layout(mChildCentre*(2*i+1) - mChildCenterSize/2 ,
allHeight/2 - mChildCenterSize/2,
mChildCentre*(2*i+1) + mChildCenterSize/2 ,
allHeight/2 + mChildCenterSize/2)
}else {
childViews[i].layout(mChildCentre*(2*i+1) - mChildSize/2 ,
allHeight - bgHeight/2 - mChildSize/2,
mChildCentre*(2*i+1) + mChildSize/2 ,
allHeight - bgHeight/2 + mChildSize/2)
}
}
}
進行布局,這里比較重要,因為能看出,中間的圖標會更大一些,所以要做一些適配。其實這里就是把寬度分為6塊,然后3個view分別在1,3,5這三個左邊點,y的話就是除中間那個,其它兩個都是bgHeight繪制高度的的一半,中間那個是allHeight總高度的一半,這樣3個view的x和y坐標都能拿到了,再根據(jù)寬高就能算出l,t,r,b四個點,然后布局。
3. 處理事件分發(fā)
可以看出我們的區(qū)域是一個不規(guī)則的區(qū)域,按照我們用抽象的角度去思考,我們希望這個菜單欄的區(qū)域只是顯示藍色的那個區(qū)域,所以藍色區(qū)域上面的白色區(qū)域就算是我們自定義view的范圍,他觸發(fā)的事件也應該是后面的view的事件(Demo中后面的View是一個ViewPager),而不是菜單欄。
// 輔助事件分發(fā)區(qū)域
if (mWidthZone1 == 0 || mWidthZone2 == 0) {
mWidthZone1 = width / 2 - mRadius / 2
mWidthZone2 = width / 2 + mRadius / 2
}
這兩塊是圓外的x的區(qū)域。
/**
* 判斷點擊事件是否在點擊區(qū)域中
*/
private fun isShowZone(x : Float, y : Float) : Boolean{
if (y >= allHeight - bgHeight){
return true
}
if (x >= mWidthZone1 && x <= mWidthZone2){
// 在圓內(nèi)
val relativeX = abs(x - width/2)
val squareYZone = mRadius.toDouble().pow(2.0) - relativeX.toDouble().pow(2.0)
return y >= mRadius - sqrt(squareYZone)
}
return false
}
先判斷y如果在背景的矩陣中(上面說了自定義view分成矩陣,外圓,內(nèi)圓),那肯定是菜單的區(qū)域。如果不在,那就要判斷y在不在圓內(nèi),這里就必須用勾股定理去判斷。
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 點擊區(qū)域進行攔截
if (event?.action == MotionEvent.ACTION_DOWN && isShowZone(event.x, event.y)){
return true
}
return super.onTouchEvent(event)
}
最后做一個事件分發(fā)的攔截。除了計算區(qū)域那可能需要去想想,其它地方我覺得都挺好理解的吧。
4. 做個動畫
給子view設點擊事件讓外部處理,然后給中間的按鈕做個動畫效果。
private fun initChildView(cView : View?, index : Int) {
cView?.setOnClickListener {
if (index == childViews.size/2) {
startAnim(cView)
}else {
onChildClickListener?.onClick(index)
}
}
}
private fun startAnim(view : View){
if (objectAnimation == null) {
objectAnimation = ObjectAnimator.ofFloat(view,
"rotation", 0f, -15f, 180f, 0f)
objectAnimation?.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator) {
}
override fun onAnimationEnd(p0: Animator) {
onChildClickListener?.onClick(childViews.size / 2)
}
override fun onAnimationCancel(p0: Animator) {
onChildClickListener?.onClick(childViews.size / 2)
}
override fun onAnimationRepeat(p0: Animator) {
}
})
objectAnimation?.duration = 1000
objectAnimation?.interpolator = AccelerateDecelerateInterpolator()
}
objectAnimation?.start()
}
注意做釋放操作。
fun onDestroy(){
try {
objectAnimation?.cancel()
objectAnimation?.removeAllListeners()
}catch (e : Exception){
e.printStackTrace()
}finally {
objectAnimation = null
}
}
5. 小結(jié)
其實代碼都挺簡單的,關(guān)鍵是你要去想出一個方法來實現(xiàn)這個場景,然后感覺這個自定義viewgroup也是比較經(jīng)典的,涉及到measure、layout、draw,涉及到動畫,涉及到點擊沖突。
這個Demo表示你要實現(xiàn)怎樣的效果都可以,只要是draw能畫出來的,你都能實現(xiàn),我這個是中間凸出來,你可以實現(xiàn)凹進去,你可以實現(xiàn)波浪的樣子,可以實現(xiàn)復雜的曲線,都行,你用各種基礎圖形去做拼接,或者畫貝塞爾等等,其實都不難,主要是要有個計算和調(diào)試的過程。但是你的形狀要和點擊區(qū)域關(guān)聯(lián)起來,你設計的圖案越復雜,你要適配的點擊區(qū)域計算量就越大。
甚至我還能做得效果更屌的是,那3個子view的圖標,我都能畫出來,就不用ImagerView,直接手動畫出來,這樣做的好處是什么呢?我對子view的圖標能做各種炫酷的屬性動畫,我在切換viewpager時對圖標做屬性動畫,那不得逼格再上一層。 為什么我沒做呢,因為沒有設計,我自己做的話要花大量的時間去調(diào),要是有設計的話他告訴我尺寸啊位置啊這些信息,做起來就很快。我的APP主要是打算實現(xiàn)視頻的編輯為主,所以這些支線就沒打算花太多時間去處理。
原文鏈接:https://juejin.cn/post/7142350663907803144
相關(guān)推薦
- 2022-02-02 去掉chorme瀏覽器自動補全時input框的背景樣式
- 2022-12-06 React超詳細分析useState與useReducer源碼_React
- 2022-10-12 Nginx?504?Gateway?Time-out的兩種最新解決方案_nginx
- 2022-07-31 Jenkins定時構(gòu)建語法規(guī)則及時間設置_相關(guān)技巧
- 2022-06-06 Element UI詳解el-scrollbar 滾動條組件 —— 監(jiān)聽滾動條的滾動,跟隨頁面一起滾
- 2022-05-20 MybatisCodeHelpPro生成持久層代碼
- 2022-01-19 iview-admin 富文本編輯器(wangEditor)菜單無法選中解決方案
- 2022-09-02 Oracle11g調(diào)整SGA方法詳解_oracle
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支