Android自定義view實(shí)現(xiàn)圓形與半圓形菜單
前不久看到鴻洋大大的圓形菜單,就想開始模仿,因?yàn)閷?shí)在是太酷了,然后自己根據(jù)別人(zw哥)給我講的一些思路、一些分析,就開始改造自己的圓形菜單了。
文章結(jié)構(gòu):1.功能介紹以及展示;2.部分代碼講解;3.大致可以實(shí)現(xiàn)的UI效果展示講解。4.源碼附送。
一、功能介紹以及展示
第一個(gè)展示是本控件的原樣。但是我們可以使用很多技巧去達(dá)到我們的商業(yè)UI效果嘛。
這里給出的是本博客作品demo的展示圖以及第三點(diǎn)的聯(lián)動(dòng)展示,可見是一圓型菜單,相較于鴻洋大大的那個(gè)圓形菜單多了一些需求:
1.到時(shí)候展示只需要半圓的轉(zhuǎn)盤。
2.在規(guī)定的角度不能讓他們自動(dòng)旋轉(zhuǎn)(涉及延伸的一些數(shù)學(xué)計(jì)算,一會(huì)重點(diǎn)講解)。
3.要綁定fragment。
4.一個(gè)緩沖角度,即我們將要固定幾個(gè)位置,而不是任意位置。我們要設(shè)計(jì)一個(gè)可能的角度去自動(dòng)幫他選擇。
二、代碼講解:
結(jié)合實(shí)際使用的方式來講解。分為:1.調(diào)用方式;2.此控件onMeasure方法;3.onLayout方法的作用;4.此控件事件機(jī)制dispatchTouchEvent的使用;5.數(shù)學(xué)計(jì)算—一個(gè)緩沖角度。
(1)調(diào)用方式 :(代碼為展示區(qū)下方的效果代碼)
//采用的是聯(lián)動(dòng),使用Fragment管理器FragmentTransaction去實(shí)現(xiàn)fragment管理 package com.fuzhucheng.circlemenu; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; import android.view.KeyEvent; import android.view.View; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private UpCircleMenuLayout myCircleMenuLayout; //四個(gè)fragment頁面 private HomepageFragment homepageFragment; private SettingFragment settingFragment; private HistoryFragment historyFragment; private FourthFragment fourthFragment; private FifthFragment fifthFragment; private String[] mItemTexts = new String[]{"安全中心 ", "特色服務(wù)", "投資理財(cái)", "轉(zhuǎn)賬匯款", "我的賬戶", "安全中心", "特色服務(wù)", "投資理財(cái)", "轉(zhuǎn)賬匯款", "我的賬戶"}; private int[] mItemImgs = new int[]{R.drawable.home_mbank_1_normal, R.drawable.home_mbank_2_normal, R.drawable.home_mbank_3_normal, R.drawable.home_mbank_4_normal, R.drawable.home_mbank_5_normal, R.drawable.home_mbank_1_normal, R.drawable.home_mbank_2_normal, R.drawable.home_mbank_3_normal, R.drawable.home_mbank_4_normal, R.drawable.home_mbank_5_normal}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //第一次初始化首頁默認(rèn)顯示第一個(gè)fragment initFragment1(); myCircleMenuLayout = (UpCircleMenuLayout) findViewById(R.id.id_mymenulayout); myCircleMenuLayout.setMenuItemIconsAndTexts(mItemImgs);//一句設(shè)置圖片 myCircleMenuLayout.setOnMenuItemClickListener(new UpCircleMenuLayout.OnMenuItemClickListener() { @Override public void itemClick(int pos) { Toast.makeText(MainActivity.this, mItemTexts[pos], Toast.LENGTH_SHORT).show(); switch (pos) { case 0: initFragment1(); setTitle("安全中心"); break; case 1: initFragment2(); setTitle("特色服務(wù)"); break; case 2: initFragment3(); setTitle("投資理財(cái)"); break; case 3: initFragment4(); setTitle("轉(zhuǎn)賬匯款"); break; case 4: initFragment5(); setTitle("我的賬戶"); break; case 5: initFragment1(); setTitle("安全中心"); break; case 6: initFragment2(); setTitle("特色服務(wù)"); break; case 7: initFragment3(); setTitle("投資理財(cái)"); break; case 8: initFragment4(); setTitle("轉(zhuǎn)賬匯款"); break; case 9: initFragment5(); setTitle("我的賬戶"); break; } } @Override public void itemCenterClick(View view) { Toast.makeText(MainActivity.this, "you can do something just like ccb ", Toast.LENGTH_SHORT).show(); } }); } //顯示第一個(gè)fragment private void initFragment1(){ //開啟事務(wù),fragment的控制是由事務(wù)來實(shí)現(xiàn)的 homepageFragment = new HomepageFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,homepageFragment); transaction.addToBackStack(null); transaction.commit(); } //顯示第二個(gè)fragment private void initFragment2(){ //開啟事務(wù),fragment的控制是由事務(wù)來實(shí)現(xiàn)的 settingFragment = new SettingFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,settingFragment); transaction.addToBackStack(null); transaction.commit(); } private void initFragment3(){ //開啟事務(wù),fragment的控制是由事務(wù)來實(shí)現(xiàn)的 historyFragment = new HistoryFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,historyFragment); transaction.addToBackStack(null); transaction.commit(); } private void initFragment4(){ //開啟事務(wù),fragment的控制是由事務(wù)來實(shí)現(xiàn)的 fourthFragment = new FourthFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,fourthFragment); transaction.addToBackStack(null); transaction.commit(); } private void initFragment5(){ //開啟事務(wù),fragment的控制是由事務(wù)來實(shí)現(xiàn)的 fifthFragment = new FifthFragment(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_tv,fifthFragment); transaction.addToBackStack(null); transaction.commit(); } public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { finish(); return true; } return super.onKeyDown(keyCode, event); } }
(2)此控件onMeasure方法講解:重點(diǎn)講解迭代測(cè)量
/** * 設(shè)置布局的寬高,并策略menu item寬高 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int resWidth = 0; int resHeight = 0; double startAngle = mStartAngle; double angle = 360 / 10; //我們傳入了10個(gè)孩子 /** * 根據(jù)傳入的參數(shù),分別獲取測(cè)量模式和測(cè)量值 */ int width = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); /** * 如果寬或者高的測(cè)量模式非精確值 */ if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { // 主要設(shè)置為背景圖的高度 resWidth = getDefaultWidth(); resHeight = (int) (resWidth * DEFAULT_BANNER_HEIGTH / DEFAULT_BANNER_WIDTH); } else { // 如果都設(shè)置為精確值,則直接取小值; resWidth = resHeight = Math.min(width, height); } setMeasuredDimension(resWidth, resHeight); // 獲得直徑 mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight()); // menu item數(shù)量 final int count = getChildCount(); // menu item尺寸 int childSize; // menu item測(cè)量模式 int childMode = MeasureSpec.EXACTLY; // 迭代測(cè)量:根據(jù)孩子的數(shù)量進(jìn)行遍歷,為每一個(gè)孩子測(cè)量大小,設(shè)置監(jiān)聽回調(diào)。 for (int i = 0; i < count; i++) { final View child = getChildAt(i); startAngle = startAngle % 360; if (startAngle > 269 && startAngle < 271 && isTouchUp) { mOnMenuItemClickListener.itemClick(i); //設(shè)置監(jiān)聽回調(diào)。 mCurrentPosition = i; //本次使用mCurrentPosition,只是把他作為一個(gè)temp變量,可以有更多的使用,比如動(dòng)態(tài)設(shè)置每個(gè)孩子相隔的角度 childSize = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION);//設(shè)置大小 } else { childSize = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION);//設(shè)置大小 } if (child.getVisibility() == GONE) { continue; } // 計(jì)算menu item的尺寸;以及和設(shè)置好的模式,去對(duì)item進(jìn)行測(cè)量 int makeMeasureSpec = -1; makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode); child.measure(makeMeasureSpec, makeMeasureSpec); startAngle += angle; } //item容器內(nèi)邊距 mPadding = DensityUtil.dip2px(getContext(), RADIO_MARGIN_LAYOUT); }
onMeasure深入:View在屏幕上顯示出來要先經(jīng)過measure(計(jì)算)和layout(布局)。這方法作用就是計(jì)算出自定義View的寬度和高度。這個(gè)計(jì)算的過程參照父布局給出的大小,以及自己特點(diǎn)算出結(jié)果 。當(dāng)然,還有相關(guān)的尺寸測(cè)量模式。此處奉上一篇好博文:onMeasure理解。此外,我還在這方法里作為監(jiān)聽回調(diào)的設(shè)置?。《鵀榭丶O(shè)置圖片可以直接使用我們下面設(shè)計(jì)的方法:setMenuItemIconsAndTexts一句收工。
(3)onLayout方法的講解:(此處的圓的數(shù)學(xué)計(jì)算布置圖標(biāo)圍繞圓位置可見鴻洋大大的推薦,講得很清楚,當(dāng)然我下面也會(huì)略微講解下)
/** * 設(shè)置menu item的位置 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int layoutRadius = mRadius; // Laying out the child views final int childCount = getChildCount(); int left, top; // menu item 的尺寸 int cWidth; // 根據(jù)menu item的個(gè)數(shù),計(jì)算角度 float angleDelay = 360 / 10; // 遍歷去設(shè)置menuitem的位置 for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); //根據(jù)孩子遍歷,設(shè)置中間頂部那個(gè)的大小以及其他圖片大小。 if (mStartAngle > 269 && mStartAngle < 271 && isTouchUp) { cWidth = DensityUtil.dip2px(getContext(), RADIO_TOP_CHILD_DIMENSION); child.setSelected(true); } else { cWidth = DensityUtil.dip2px(getContext(), RADIO_DEFAULT_CHILD_DIMENSION); child.setSelected(false); } if (child.getVisibility() == GONE) { continue; } //大于360就取余歸于小于360度 mStartAngle = mStartAngle % 360; float tmp = 0; //計(jì)算圖片布置的中心點(diǎn)的圓半徑。就是tmp tmp = layoutRadius / 2f - cWidth / 2 - mPadding; // tmp cosa 即menu item中心點(diǎn)的橫坐標(biāo)。計(jì)算的是item的位置,是計(jì)算位置?。。? left = layoutRadius / 2 + (int) Math.round(tmp * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f * cWidth) + DensityUtil .dip2px(getContext(), 1); // tmp sina 即menu item的縱坐標(biāo) top = layoutRadius / 2 + (int) Math.round(tmp * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f * cWidth) + DensityUtil .dip2px(getContext(), 8); //接著當(dāng)然是布置孩子的位置啦,就是根據(jù)小圓的來布置的 child.layout(left, top, left + cWidth, top + cWidth); // 疊加尺寸 mStartAngle += angleDelay; } }
給出鴻洋大大的計(jì)算小圓的思路圖:
(4)此控件事件機(jī)制dispatchTouchEvent的使用:
//dispatchTouchEvent是處理觸摸事件分發(fā),事件(多數(shù)情況)是從Activity的dispatchTouchEvent開始的。執(zhí)行super.dispatchTouchEvent(ev),事件向下分發(fā)。 //onTouchEvent是View中提供的方法,ViewGroup也有這個(gè)方法,view中不提供onInterceptTouchEvent。view中默認(rèn)返回true,表示消費(fèi)了這個(gè)事件。 @Override public boolean dispatchTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //直接就是獲取x,y值了,還有一個(gè)DownTime(附送) mLastX = x; mLastY = y; mDownTime = System.currentTimeMillis(); mTmpAngle = 0; break; case MotionEvent.ACTION_MOVE: isTouchUp = false; //注意isTouchUp 這個(gè)標(biāo)記量?。?! /** * 獲得開始的角度 */ float start = getAngle(mLastX, mLastY); /** * 獲得當(dāng)前的角度 */ float end = getAngle(x, y); // 如果是一、四象限,則直接end-start,角度值都是正值 if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) { mStartAngle += end - start; mTmpAngle += end - start;//按下到抬起時(shí)旋轉(zhuǎn)的角度 } else // 二、三象限,色角度值是負(fù)值 { mStartAngle += start - end; mTmpAngle += start - end; } // 重新布局 if (mTmpAngle != 0) { requestLayout(); } mLastX = x; mLastY = y; break; case MotionEvent.ACTION_UP: //當(dāng)手指UP啦,就是關(guān)鍵啦,一個(gè)緩沖角度,即我們將要固定幾個(gè)位置,而不是任意位置。我們要設(shè)計(jì)一個(gè)可能的角度去自動(dòng)幫他選擇。 backOrPre(); break; } return super.dispatchTouchEvent(event); }
MotionEvent事件機(jī)制:(此控件我只用了三個(gè))主要的事件類型有:ACTION_DOWN: 表示用戶開始觸摸。ACTION_MOVE: 表示用戶在移動(dòng)(手指或者其他)。ACTION_UP:表示用戶抬起了手指。
(5)數(shù)學(xué)計(jì)算—一個(gè)緩沖角度。
private void backOrPre() { //緩沖的角度。即我們將要固定幾個(gè)位置,而不是任意位置。我們要設(shè)計(jì)一個(gè)可能的角度去自動(dòng)幫他選擇。 isTouchUp = true; float angleDelay = 360 / 10; //這個(gè)是每個(gè)圖形相隔的角度 //我們本來的上半圓的圖片角度應(yīng)該是:18,54,90,126,162。所以我們這里是:先讓當(dāng)前角度把初始的18度減去再取余每個(gè)圖形相隔角度。得到的是什么呢?就是一個(gè)圖片本來應(yīng)該在的那堆角度。所以如果是就直接return了。 if ((mStartAngle-18)%angleDelay==0){ return; } float angle = (float)((mStartAngle-18)%36); //angle就是那個(gè)不是18度開始布局,然后是36度的整數(shù)的多出來的部分角度 //以下就是我們做的緩沖角度處理啦,如果多出來的部分角度大于圖片相隔角度的一半就往前進(jìn)一個(gè),如果小于則往后退一個(gè)。 if (angleDelay/2 > angle){ mStartAngle -= angle; }else if (angleDelay/2<angle){ mStartAngle = mStartAngle - angle + angleDelay; //mStartAngle就是當(dāng)前角度啦,取余36度就是多出來的角度,拿這個(gè)多出來的角度去數(shù)據(jù)處理。 } //然后重新布局onlayout requestLayout(); }
至于其他小的方法詳情,可見源代碼,有詳細(xì)解釋。
源碼傳送門:github地址:Android-自定義view之圓形與“半圓形”菜單 喜歡的可以star或fork啦,謝謝!
好了,Android-自定義view之圓形與“半圓形”菜單講完了。本博客是經(jīng)過仔細(xì)研究鴻洋大大的圓形菜單博客的,并在這里做出進(jìn)一步拓展以及寫出自己的理解。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
導(dǎo)入takephoto庫(kù)編譯失敗與glide庫(kù)沖突應(yīng)排除依賴
今天小編就為大家分享一篇關(guān)于導(dǎo)入takephoto庫(kù)編譯失敗與glide庫(kù)沖突應(yīng)排除依賴的文章,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2018-10-10Android 錄音與播放功能的簡(jiǎn)單實(shí)例
這篇文章主要介紹了 Android 錄音與播放功能的簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06Android編程實(shí)現(xiàn)控件不同狀態(tài)文字顯示不同顏色的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)控件不同狀態(tài)文字顯示不同顏色的方法,涉及Android針對(duì)控件布局文件屬性設(shè)置及狀態(tài)判定等相關(guān)技巧,需要的朋友可以參考下2016-02-02Android實(shí)現(xiàn)調(diào)用攝像頭
本文給大家分享的是,在安卓APP開發(fā)的過程中,經(jīng)常會(huì)需要調(diào)用手機(jī)自身攝像頭拍照的代碼,十分的簡(jiǎn)單實(shí)用,有需要的小伙伴可以參考下。2015-07-07Android界面數(shù)據(jù)懶加載實(shí)現(xiàn)代碼
這篇文章主要為大家分享了Android界面數(shù)據(jù)懶加載實(shí)現(xiàn)代碼,告訴大家怎樣實(shí)現(xiàn)界面即Fragment的懶加載,感興趣的小伙伴們可以參考一下2016-09-09Android列表實(shí)現(xiàn)(2)_游標(biāo)列表案例講解
最近開始學(xué)習(xí)android的ui 游標(biāo)列表實(shí)現(xiàn),先上幾個(gè)相關(guān)的例子,后續(xù)還會(huì)有更新,感興趣的朋友可以研究下2012-12-12Android實(shí)現(xiàn)在TextView文字過長(zhǎng)時(shí)省略部分或滾動(dòng)顯示的方法
這篇文章主要介紹了Android實(shí)現(xiàn)在TextView文字過長(zhǎng)時(shí)省略部分或滾動(dòng)顯示的方法,結(jié)合實(shí)例形式分析了Android中TextView控件文字顯示及滾動(dòng)效果相關(guān)操作技巧,需要的朋友可以參考下2016-10-10