自定义控件从入门到轻生之---解锁新姿势
所有blog局限于博主水平有限,很多不足之處大家可以指出共同探討進(jìn)步。
尊重原創(chuàng)轉(zhuǎn)載請(qǐng)注明:From 倪大葉http://blog.csdn.net/renyi0109 侵權(quán)必究!雖然我不知道具體怎么究,但是看大家都這么寫我也就這么寫吧
上次說(shuō)到View的事件分發(fā)從根本原則上清楚了幾條事件分發(fā)的法……..好好好,上次是說(shuō)到我侄兒拿了一道初一得數(shù)學(xué)題讓我解惑一下,先不說(shuō)這道題的本身,本著程序員看問題從本質(zhì)出發(fā)的角度,我覺得就算我給他把答案寫出來(lái)他下次再碰到類似的題或者同類詭異的題肯定還是做不出來(lái),不能舉一反三講來(lái)何用?所以我本著從源頭上解決的思想告訴他: 以后碰到這種題,寫個(gè)解字可以得一分…
上次講的事件分發(fā)是在現(xiàn)有View層面上的玩意兒,今天我們倒敘回去看看一個(gè)View的從無(wú)到有是怎么生成的也就是常說(shuō)的View繪制流程。在這之前先大概講講Android下的”View 框架”
Android四大組件各司其職,前有Activity賣弄風(fēng)騷,后有service暗度陳倉(cāng),再有BroadCastReceiver左右逢源,最后有ContentProvider不知道在干嘛,而Activity正是負(fù)責(zé)前臺(tái)接待客戶的,所以我們就從Activity開始講講View繪制流程的框架(這部分并不是這篇blog的重點(diǎn),所以就不貼源碼了)雖然Activity是負(fù)責(zé)和用戶交互和顯示視圖的但其實(shí)Activity并不直接控制它們,而是由他內(nèi)部一個(gè)叫window的哥們來(lái)負(fù)責(zé)的,每個(gè)Activity在attach的時(shí)候會(huì)創(chuàng)建一個(gè)唯一的window對(duì)象并把自己作為接口回調(diào)注冊(cè)給window,以用于window將一些Activity關(guān)心的玩意兒丟回來(lái),比如上一篇說(shuō)到的觸摸事件就是由window丟給Activity的。我們要想在Activity顯示什么布局,直接在onCreate中調(diào)用setContentView就可以了,這個(gè)方法其實(shí)就是調(diào)用了window的setContentView,window內(nèi)部會(huì)把我們?cè)O(shè)置的布局文件裝載到一個(gè)叫DecorView的東西上,這個(gè)DecorView就是我們能看到的手機(jī)畫面的根視圖,它是一個(gè)FrameLayout里面只有一個(gè)Linearlayout子View,Linearlayout又分為titleView(就是title布局,actionBar和普通title就是顯示在這里)和ContentView(這就是顯示我們?cè)O(shè)置布局的地方了),這時(shí)候敏感的哥們可能會(huì)問那狀態(tài)欄呢?狀態(tài)欄和我們應(yīng)用可沒什么關(guān)系,它其實(shí)是一個(gè)系統(tǒng)級(jí)的應(yīng)用,最高優(yōu)先級(jí)顯示在手機(jī)上。到這兒雖然已經(jīng)把我們想顯示的視圖設(shè)置給了DecorView,但是這時(shí)候decorView和window其實(shí)還沒有關(guān)聯(lián)起來(lái),要將DecorView添加到window上才能真正的顯示出來(lái),這個(gè)添加過(guò)程就要靠一個(gè)叫ViewRootImpl的類,說(shuō)到這個(gè)類就吊了,基本所有View處理都是它來(lái)完成的,比如我們接下來(lái)要講的measure layout draw的發(fā)起源頭都是在這個(gè)類, ViewRootImpl可以看作是DecorView和window連接的紐帶,DecorView添加到window的大概流程 windowManager(window的管理器,負(fù)責(zé)操作window的)->windowManagerGobal(addView) ->ViewRootImpl 最后就是ViewRootImpl的setView來(lái)完成添加,而這個(gè)方法是一個(gè)IPC過(guò)程,添加的最終實(shí)現(xiàn)是一個(gè)叫Session的遠(yuǎn)程實(shí)現(xiàn)類。都講到這里了 就順便再提一下VIewRootImpl的performTraversals方法,接下來(lái)要講到的View的測(cè)量啊布局什么的源頭都是由該方法發(fā)起的
if (lp.horizontalWeight > 0.0f) {width += (int) ((mWidth - width) lp.horizontalWeight);//生成子View寬的測(cè)量規(guī)格 這個(gè)MeasureSpec是整個(gè)measure的核心數(shù)據(jù)類,子View的onMeasure方法中的測(cè)量規(guī)格都是由父元素給的,//至于有什么用后面講測(cè)量的時(shí)候再細(xì)說(shuō),而這個(gè)規(guī)格就是在這里生成的,當(dāng)然這只是生成的起點(diǎn),每層View拿到這個(gè)規(guī)格都有可能會(huì)根據(jù)//自己的需要做一些修改再傳給子ViewchildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);measureAgain = true;}if (lp.verticalWeight > 0.0f) {height += (int) ((mHeight - height) * lp.verticalWeight);//同上childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);measureAgain = true;}if (measureAgain) {if (DEBUG_LAYOUT) Log.v(TAG,"And hey let's measure once more: width=" + width+ " height=" + height);//發(fā)起測(cè)量,正式開始走View的測(cè)量邏輯performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}//發(fā)起布局 performLayout(lp, desiredWindowWidth, desiredWindowHeight);//發(fā)起繪制performDraw();上面說(shuō)的這些只是最最最最范闊的介紹,里面有很多細(xì)節(jié)值得大家去研究學(xué)習(xí),當(dāng)然要真的弄懂上面這一套View原理框架并不是一件容易的事,好了扯了這么多該吃正餐了
首先先跳出程序的思維來(lái)看下生活中如果我們要畫出一張圖需要哪什么步驟
1.首先不管要畫什么東西得先有張紙
2.有了紙你得知道你要畫得圖像大概有多大
3.你要把這個(gè)圖像畫在什么位置
4.你用什么畫,怎么畫,畫什么
完成上面幾步基本就可以畫出一個(gè)View了,那我們把這幾步映射到Android的View繪制里來(lái), 紙就是我們的canvas,這個(gè)東西不用關(guān)心系統(tǒng)已經(jīng)給我們準(zhǔn)備好了而且也必須畫在系統(tǒng)給我們準(zhǔn)備的這張紙上才能顯示出來(lái),畫多大就是View的測(cè)量方法measure,畫在哪就是layout方法,畫什么就是draw方法
onMeasure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {....if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||....if (cacheIndex < 0 || sIgnoreMeasureCache) {// measure ourselves, this should set the measured dimension flag back//這里就是調(diào)用onMeasure的地方onMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {long value = mMeasureCache.valueAt(cacheIndex);// Casting a long to int drops the high 32 bits, no mask neededsetMeasuredDimensionRaw((int) (value >> 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}....mOldWidthMeasureSpec = widthMeasureSpec;mOldHeightMeasureSpec = heightMeasureSpec;mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension}可能有人還不是很清楚measure和onMeasure的區(qū)別,其實(shí)看看measure的源碼就能看出是一個(gè)測(cè)量的控制中心,比如一些測(cè)量執(zhí)行的判斷,狀態(tài)的記錄等等,當(dāng)然實(shí)際的測(cè)量onMeasure也是由measure來(lái)負(fù)責(zé)控制的,還有各位老板注意看這是一個(gè)final方法,final!!所以不要再問為什么測(cè)量不能復(fù)寫measure方法,同理 layout draw都是Android系統(tǒng)已經(jīng)寫好的,不用你關(guān)心也輪不到你關(guān)心,提供給我們能做的就是 onMeasure這幾個(gè)方法,說(shuō)白了 measure是測(cè)量的邏輯控制,而onMeasure是具體的測(cè)量實(shí)現(xiàn)
在江湖上流傳著一個(gè)說(shuō)法,一個(gè)View的大小實(shí)際是由父View和自身共同決定的,View本身對(duì)自己大小的期望就是來(lái)自我們?cè)诖a或者xml文件中給它設(shè)置的寬高,而父View對(duì)兒子得期望就是measuren的兩個(gè)int參數(shù),這兩個(gè)參數(shù)由父親傳給兒子,兒子又傳給孫子,孫子再傳給曾孫….一直傳到買不起房找不了老婆沒辦法生兒子那一代.. 那么肯定有沒注意看的事兒逼會(huì)問那頂級(jí)父View的參數(shù)又是誰(shuí)傳給他的呢?上面已經(jīng)講了有個(gè)叫ViewRootImpl的上帝用performTraversals方法去生成這兩個(gè)int規(guī)則,至于大小就是屏幕大小。然后由它傳給頂級(jí)父View。 這兩個(gè)int 高2位為mode,低30位為size,至于是什么意思就要先講講今天的配角
MeasureSpec
//提取期望int型低30位為大小 int specSize = MeasureSpec.getSize(measureSpec) //提取期望int型中高兩位為mode int specMode = MeasureSpec.getMode(measureSpec)這個(gè)mode有三種類型: MeasureSpec.EXACTLY , MeasureSpec.AT_MOST , MeasureSpec.UNSPECIFIED(這個(gè)我到現(xiàn)在都不知道有什么鳥用,我們用不到就不講了,想來(lái)應(yīng)該是系統(tǒng)級(jí)控件會(huì)用到吧)
MeasureSpec.EXACTLY:
父View已經(jīng)測(cè)量出子View的大小,你就直接用我給你的size就行,你自己設(shè)置的大小別用了
MeasureSpec.AT_MOST:
父View不強(qiáng)制要求子View多大,子View可以根據(jù)自己的期望來(lái)設(shè)置大小,但是前提是不能超過(guò)我給的Size大小
下面是View源碼的onMeasure邏輯
View只是很簡(jiǎn)單的實(shí)現(xiàn)了最基本的onMeasure邏輯,基本所有View在繼承View的同時(shí)都會(huì)復(fù)寫自己邏輯的onMeasure方法,每個(gè)View基本都不一樣,在這我就不挑某一個(gè)特定View來(lái)看源碼了,但是為了讓大家更深刻的理解”父子共同決定View的大小”,我們自己簡(jiǎn)單寫個(gè)onMeasure邏輯
//childSize: 子View自己希望的大小(來(lái)自xml文件中設(shè)置或者代碼)//parentMeasureSpec: 父View對(duì)子View的期望public int getMeasureHeightSize(int childSize, int parentMeasureSpec) {int result ;//提取父View期望modeint specMode = MeasureSpec.getMode(measureSpec);//提取父View期望大小int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED://父View對(duì)子View不做限制//子View想要多大就多大result = size;break;case MeasureSpec.AT_MOST: //父View不對(duì)子View做具體要求,但是不能大過(guò)父View給的最大值//如果子View想要的大小沒有超過(guò)父View的限定,那么直接用子View想要的大小,如果超過(guò)了,只能取到specSize這個(gè)限定值result = size <= specSize? size : specSize;break;case MeasureSpec.EXACTLY://父View已經(jīng)測(cè)量出子View的精確大小//直接使用父View給的大小就好result = specSize;break;}return result;}這只是很簡(jiǎn)單的一個(gè)小例子,實(shí)際中要考慮的很多,比如獲取自身期望值得時(shí)候還需要加上padding,或者一些特有的屬性對(duì)值的影響,這里我們已經(jīng)看到子View是怎么根據(jù)父View的期望和自身的設(shè)置值來(lái)最終確定大小的。我們來(lái)思考一個(gè)問題,這個(gè)父View的期望是依據(jù)什么得來(lái)的呢?有人會(huì)說(shuō)不是ViewRootImpl傳下來(lái)的,然后再一層一層傳下去的嗎?是這樣沒錯(cuò),可是ViewRootImpl只是簡(jiǎn)單將滿屏寬高作為規(guī)則向下傳遞,我上面說(shuō)過(guò)這個(gè)規(guī)則每層View拿到他都可能根據(jù)自己的實(shí)際情況做修改再傳遞給子View,我們?cè)赩iew里沒看到類似的代碼是因?yàn)橹挥性赩iewGroup系列類下才有合成childMesureSpec的邏輯。在我們ViewGroup中有幾個(gè)方法是用來(lái)計(jì)算合成對(duì)子View規(guī)則的相關(guān)方法,我們依次看看
//遍歷所有child,調(diào)用measureChild去合成對(duì)子View的測(cè)量規(guī)則 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {final int size = mChildrenCount;final View[] children = mChildren;for (int i = 0; i < size; ++i) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {measureChild(child, widthMeasureSpec, heightMeasureSpec);}}} //測(cè)量子View protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {//獲取child的 layoutParams ,里面包括了child對(duì)自己大小的一個(gè)設(shè)置信息final LayoutParams lp = child.getLayoutParams();//調(diào)用getChildMeasureSpec,根據(jù)父View的期望和子View希望值來(lái)合成對(duì)子View的規(guī)則final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);//將合成的規(guī)則傳遞給子View去測(cè)量child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}主要合成對(duì)子View期望規(guī)則的方法就是getChildMeasureSpec,我們進(jìn)去詳細(xì)看看這個(gè)方法public static int getChildMeasureSpec(int spec, int padding, int childDimension) {//提取父View的mode和sizeint specMode = MeasureSpec.getMode(spec);int specSize = MeasureSpec.getSize(spec);//父View的期望size首選得先減去該View的padding,才是實(shí)際給內(nèi)容的大小,這里做個(gè)小于0限制int size = Math.max(0, specSize - padding);//定義對(duì)子View規(guī)則的 size和modeint resultSize = 0;int resultMode = 0;//根據(jù)父View的mode分別進(jìn)行合成switch (specMode) {// Parent has imposed an exact size on uscase MeasureSpec.EXACTLY: 父View有準(zhǔn)確的大小要求if (childDimension >= 0) { //如果子View大小這只為一個(gè)精確值(childDimension大于0為精確數(shù)字,match_parent 和wrap_content都小于0)//直接用子View設(shè)置的值為規(guī)則的大小,mode肯定就為EXACTLY了resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {//如果為填滿父View//直接用父View的size,因?yàn)楦竀iew的大小是確定的所以子mode也是精確的 EXACTLYresultSize = size;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果為包裹內(nèi)容// 也用父View的size,因?yàn)楦竀iew的值是精確的,所以子View的大小不能超過(guò)這個(gè)限定resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// Parent has imposed a maximum size on uscase MeasureSpec.AT_MOST: //父View的mode為限定一個(gè)大小//這里條件和上面一樣,就直接看里面的生成了if (childDimension >= 0) {//直接用子View設(shè)置的值,mode也就為 EXACTLY了resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {//用父View給的大小,因?yàn)楦竀iew大小被限定了,所以子View也跟著被限定resultSize = size;resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {//用父View給的大小,這時(shí)候父View是最大值限定,子View同樣不能超過(guò)父View,也同樣做最大值限定resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// 這個(gè)模式就不多分析了,和上面是一樣的case MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// Child wants a specific size... let him have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size... find out how big it should// beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size.... find out how// big it should beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}看到這里大家應(yīng)該都清楚了onMeasure的測(cè)量邏輯以及測(cè)量規(guī)格的生成了吧,測(cè)量的東西差不多就是這些了,接下來(lái)看看onLayout
onLayout
我們已經(jīng)測(cè)量出View的大小,接下來(lái)就該確定它應(yīng)該所呆得位置了,layout和measure就有點(diǎn)不同了,比如measure是在父View和自身期望下共同決定大小的,但是最終還是View自己去調(diào)用setMeasure方法設(shè)置寬高,也就是說(shuō)最終決定權(quán)還是在View本身,上面說(shuō)過(guò)極端情況下你甚至可以完全不管父View的期望直接設(shè)置寬高都是可行的,而layout就沒有這個(gè)特權(quán)了,該放在那他只能提意見(xml文件設(shè)置或者代碼),最終決定權(quán)在父View,我們看下View中的onLayout:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}是一個(gè)空實(shí)現(xiàn),onLayout和onMeasure也有點(diǎn)不同,我們說(shuō)過(guò)measure是測(cè)量控制,onMeasure是實(shí)現(xiàn)具體設(shè)置寬高,這里onLayout其實(shí)只是用來(lái)控制子View該擺放的位置,具體擺放設(shè)置的代碼卻是layout作為實(shí)現(xiàn),既然能作為父View那么絕逼是個(gè)ViewGroup吧,再去看下它里面的onLayout:
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);這特么不止是空實(shí)現(xiàn)而是一個(gè)抽象方法了,也就是說(shuō)任何ViewGroup系列的子類都必須實(shí)現(xiàn)onLayout方法來(lái)擺放子View的位置并且這個(gè)layout工作系統(tǒng)不會(huì)再像onMeasure這樣幫你做好基本的邏輯了,你要自定義一個(gè)新控件就必須自己去實(shí)現(xiàn)onLayout來(lái)擺放子View。 所以像常用的 RelativeLayout,Linearlayout什么的都是必然有自己的onLayout邏輯,這也是他們的核心功能邏輯, 就不單獨(dú)挑一個(gè)出來(lái)看了,我們來(lái)自己簡(jiǎn)單的自定義一個(gè)玩一下:
/*** 就簡(jiǎn)單的寫個(gè)每個(gè)View占一排,類似豎直的LinearLayout*/ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { /* * 如果有子元素才需要的layout位置*/ if (getChildCount() > 0) { // 用來(lái)記錄已被占有的高度 int tempHeight = 0; // 遍歷子View并對(duì)其進(jìn)行定位布局 for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); /*** 讓child進(jìn)行布局,這layout的四個(gè)參數(shù) left right top bottom 一會(huì)下面畫張圖介紹* 布局的參考坐標(biāo)原點(diǎn)為父View的左上起點(diǎn),因?yàn)槭谴怪辈季?#xff0c;每次起點(diǎn)Y坐標(biāo)會(huì)下移已經(jīng)布局* 的高度*/ child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight); // 改變高度倍增值 tempHeight += child.getMeasuredHeight(); } } }好了一個(gè)自制Linearlayout就完了,你是不是覺得原來(lái)linearlayout太簡(jiǎn)單了?你去看看Linearlayout的源碼會(huì)發(fā)現(xiàn)這特么不是坑爹嗎,其實(shí)吧這例子就是一個(gè)引子,實(shí)際的控件還需要考慮子View的margin,或者一些特定的屬性(比如 RelativeLayout的 在某元素下面,在父元素底部等等),還有父View本身的內(nèi)邊距啥的。很多很多需要考慮的,所以可以看到系統(tǒng)每個(gè)容器View都有大量非常嚴(yán)謹(jǐn)?shù)膌ayout代碼來(lái)確保正確的放置View,所以要寫一個(gè)完全自定義的容器view并不是一件容易的事,這里就不這么詳細(xì)的去做了,因?yàn)槟切┒际且恍┻壿嬏幚砹?#xff0c;我們只介紹基本的原理,大家自己可以下去想個(gè)控件出來(lái)練練手
onDraw
draw的代碼會(huì)有點(diǎn)多就不貼代碼了,主要在我們自己繪制以前系統(tǒng)會(huì)先做一些繪制,比如背景啊,滾動(dòng)條啊,edge啥的,對(duì)我們來(lái)說(shuō)知道就行。 而onDraw方法也是一個(gè)空實(shí)現(xiàn),具體要畫的內(nèi)容完全交給子View自己去實(shí)現(xiàn), onDraw方法會(huì)有一個(gè)canvas參數(shù),這個(gè)玩意兒就是我們上面說(shuō)的畫布了,我們所有畫的東西都必須畫到這個(gè)畫布上才能顯示出來(lái),而且沒錯(cuò)這個(gè)canvas也是ViewRootImpl創(chuàng)建并且傳下來(lái)的。好了 我覺得draw已經(jīng)講完了 哈哈。。。 其實(shí)draw可以說(shuō)是繪制流程最簡(jiǎn)單的也可以說(shuō)是最復(fù)雜的,它完全放權(quán)給View自己愛咋畫咋畫,就像有兩個(gè)人給了他們兩張紙,一個(gè)就畫了個(gè)圓所以在這看來(lái)draw和簡(jiǎn)單,另一個(gè)畫了一幅美女出浴圖在這看來(lái)draw就很難很復(fù)雜很有意思,所以這個(gè)方法沒什么好說(shuō)的,能說(shuō)的就是畫的過(guò)程和用的工具。但是那就是一個(gè)龐大的話題了,不在這系列blog討論范圍之內(nèi)了 ,感興趣的自己去研究吧
好了就講到這,下一篇會(huì)基于這兩篇blog的內(nèi)容做一個(gè)實(shí)用的小例子,當(dāng)我正想關(guān)閉編輯器的時(shí)候我那侄兒又跑過(guò)來(lái)了。。我看著他手上并沒有拿試卷課本啥的心里暗松一口氣。。。 “叔叔,我聽說(shuō)你玩英雄聯(lián)盟很厲害,能不能帶帶我” 呼~~ 果然不是問我怪題了,還好還好。”這個(gè)主要得多練,你現(xiàn)在還小要好好學(xué)習(xí)不能花時(shí)間在玩游戲上,你好好學(xué)習(xí)期末考好了假期我?guī)阃?#xff01;”,”真的?好,我白金1上磚石上了幾次都沒上去,這下有叔叔終于好了!”,”額。。。。。。。”,我想著我黃金3的賬號(hào)又陷入了沉思。。。
總結(jié)
以上是生活随笔為你收集整理的自定义控件从入门到轻生之---解锁新姿势的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 时间序列突变检测_Pyth
- 下一篇: HPM6750系列--第一篇 初识HPM