Android自定義View實(shí)現(xiàn)帶音效和震動(dòng)的SeekBar
需求描述
當(dāng)我們需要做一些帶校準(zhǔn)的功能時(shí),需要調(diào)節(jié)一些值來反映校準(zhǔn)的效果,或者是相機(jī)之類的應(yīng)用,需要設(shè)置焦距,曝光值之類的,為了方便用戶設(shè)置這些值,通常需要用到滑動(dòng)選擇的控件,比如系統(tǒng)提供的SeekBar控件。用戶通過滑動(dòng)屏幕就能設(shè)置值。使用系統(tǒng)的seekBar雖然可以完成這些功能,但是不美觀。一般產(chǎn)品都不會(huì)采納系統(tǒng)的原生控件,所以只能是我們自己來通過自定義view繪制。今天我們要繪制的自定義View如下所示:
然后在第一次的時(shí)候,會(huì)有個(gè)動(dòng)畫提示用戶,如何操作。效果如下:
最后用戶開始操作動(dòng)畫就會(huì)消失,用戶操作時(shí)的效果如下:
本文就是主要介紹如何實(shí)現(xiàn)這樣一個(gè)控件,這個(gè)控件在滑動(dòng)的時(shí)候會(huì)伴隨音效以及手機(jī)的震動(dòng)感。
思路
當(dāng)我們拿到一個(gè)自定義View控件需求的時(shí)候,首先我們需要先分析下這個(gè)自定義控件是否可以使用系統(tǒng)已經(jīng)有的控件組合實(shí)現(xiàn),如果不能,我們?cè)俜治鲞@個(gè)自定義控件是一個(gè)view還是可以放子view的容器(ViewGroup)。如果是一個(gè)容器類的自定義控件,我們就需要繼承自ViewGroup。否則就需要我們繼承自View自己繪制,然后再添加對(duì)應(yīng)的事件處理就行了。本文要實(shí)現(xiàn)的自定義控件屬于需要繼承自View自己繪制的。首先我們要繪制的View,為了方便我們稱為RulerSeekBar。這個(gè)RulerSeekBar由幾部分組成,分別是:提示文本、指示的指針、長短刻度以及數(shù)字。接下來我們需要做的就是計(jì)算出他們的對(duì)應(yīng)坐標(biāo),然后使用繪圖API繪制出來就行了。繪制完View后我們需要做事件處理,比如滑動(dòng)的時(shí)候的吸附效果,慣性滑動(dòng),音效,震動(dòng)處理。而滾動(dòng)的時(shí)候我們使用的是ScrollerView。其實(shí)自定義Android中沒有的view控件就是將需要繪制的View樣式分解成基本圖形,算出每個(gè)需要繪制的基本圖形坐標(biāo),使用繪圖的API將其分別繪制就行了,然后就是處理事件和調(diào)整細(xì)節(jié)。
繪制提示文本
RulerSeekBar的提示文本是支持多色字體的,這里我們主要使用Android系統(tǒng)提供的SpannableString,這個(gè)類運(yùn)行我們定義各種樣式的文本,甚至可以放圖片,特別好用。不了解的小伙伴可以去百度下。這個(gè)類真的很炫。但是我們是繼承自View的,所以繪制SpannableString需要借助DynamicLayout的幫助。否則無法繪制出不同樣式的文本。
指示指針
指示指針包括兩部分,一個(gè)圖標(biāo),一個(gè)帶漸變的小圓矩形指針。我們算出他們的坐標(biāo)后使用繪圖API繪制出來就行了
長短刻度和數(shù)字
刻度分為長刻度和短刻度,為了不混淆,我使用的是兩個(gè)畫筆繪制分別繪制。然后每個(gè)刻度的坐標(biāo)計(jì)算,我們可以使用當(dāng)前控件的寬除以每個(gè)刻度的間隔大小就能得出當(dāng)前的寬可以繪制多少個(gè)刻度。而對(duì)于數(shù)字,我們可以根據(jù)設(shè)置的最大值和最小值,刻度間的間隔,當(dāng)前的位置等信息,計(jì)算每個(gè)刻度的數(shù)字的坐標(biāo)并繪制,這里處理的時(shí)候?qū)⒚總€(gè)刻度放大十倍處理,這樣可以防止在計(jì)算過程中精度的丟失,回調(diào)數(shù)據(jù)的時(shí)候再縮小10倍將值給到用戶
陰影效果繪制
我們仔細(xì)觀察可以發(fā)現(xiàn),當(dāng)我們的RulerSeekBar的兩邊刻度有個(gè)陰影效果,當(dāng)我們左滑或者右滑的時(shí)候,會(huì)出現(xiàn)一個(gè)漸變,給人一種漸漸消失的感覺,這種效果我們主要通過改變畫筆的透明度實(shí)現(xiàn)的。具體的看代碼
吸附效果和慣性滑動(dòng)
當(dāng)我們滑動(dòng)RulerSeekBar控件選擇數(shù)值時(shí),有時(shí)候會(huì)滑動(dòng)到兩個(gè)刻度之間,當(dāng)我們放開手的時(shí)候,控件會(huì)自動(dòng)吸附到兩個(gè)刻度中的一個(gè)。這種判斷就是當(dāng)滑動(dòng)的距離超過了一個(gè)閾值后就選擇后面的一個(gè)刻度,否則回彈回上一個(gè)刻度。而慣性滑動(dòng)就是我們所說的Fling,指的是我們?cè)谄聊簧峡焖倩瑒?dòng)然后突然停止后,由于慣性,還會(huì)滑動(dòng)一段距離,這里我們需要借助于速度跟蹤器:VelocityTracker和Scroller實(shí)現(xiàn),具體見代碼
音效震動(dòng)處理
當(dāng)滑動(dòng)的時(shí)候有個(gè)音效感覺會(huì)好很多,這時(shí)候如果能加上震動(dòng)效果就會(huì)更好,這里我們使用的是系統(tǒng)的 Vibrator實(shí)現(xiàn)震動(dòng),SoundPool實(shí)現(xiàn)音效播放。
提示動(dòng)畫的實(shí)現(xiàn)
因?yàn)閯?dòng)畫只是一個(gè)橫向反復(fù)平移。所以我們可以借助于屬性動(dòng)畫的ValueAnimator計(jì)算出值,然后調(diào)用View的invalidate()方法觸發(fā)view繪制需要?jiǎng)赢嫷膶?duì)象就行,本文中需要?jiǎng)赢嫷膶?duì)象是(小手圖標(biāo))
代碼解析
初始化
在初始化的時(shí)候我們將自定義的屬性解析出來并賦給當(dāng)前類的成員變量,并且初始化畫筆和一些值
public RulerSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 初始化自定義屬性 initAttrs(context, attrs); // 滑動(dòng)的閾值,后面會(huì)通過它去判斷當(dāng)前的是操作是滑動(dòng)還是觸摸操作 ViewConfiguration viewConfiguration = ViewConfiguration.get(context); TOUCH_SLOP = viewConfiguration.getScaledTouchSlop(); // 速度追蹤器的初始化 MIN_FLING_VELOCITY = viewConfiguration.getScaledMinimumFlingVelocity(); MAX_FLING_VELOCITY = viewConfiguration.getScaledMaximumFlingVelocity(); // 將距離值轉(zhuǎn)換成數(shù)字 convertValueToNumber(); // 畫筆等成員變量的初始化 init(context); }
在convertValueToNumber中我們將距離轉(zhuǎn)換成對(duì)應(yīng)的數(shù)字
private void convertValueToNumber() { mMinNumber = (int) (minValue * 10); mMaxNumber = (int) (maxValue * 10); mCurrentNumber = (int) (currentValue * 10); mNumberUnit = (int) (gradationUnit * 10); mCurrentDistance = (float) (mCurrentNumber - mMinNumber) / mNumberUnit * gradationGap; mNumberRangeDistance = (float) (mMaxNumber - mMinNumber) / mNumberUnit * gradationGap; if (mWidth != 0) { mWidthRangeNumber = (int) (mWidth / gradationGap * mNumberUnit); } Log.d(TAG, "convertValueToNumber: mMinNumber: " + mMinNumber + " ,mMaxNumber: " + mMaxNumber + " ,mCurrentNumber: " + mCurrentNumber + " ,mNumberUnit: " + + mNumberUnit + " ,mCurrentDistance: " + mCurrentDistance + " ,mNumberRangeDistance: " + + mNumberRangeDistance + " ,mWidthRangeNumber: " + mWidthRangeNumber); + }
在init函數(shù)中,主要是對(duì)各種畫筆和震動(dòng)音效的成員變量的初始化工作
private void init(Context context) { // 短刻度畫筆 mShortGradationPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mShortGradationPaint.setStrokeWidth(shortLineWidth); mShortGradationPaint.setColor(gradationColor); mShortGradationPaint.setStrokeWidth(shortLineWidth); mShortGradationPaint.setColor(gradationColor); mShortGradationPaint.setStrokeCap(Paint.Cap.ROUND); // 長刻度畫筆 mLongGradationPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mLongGradationPaint.setStrokeWidth(longLineWidth); mLongGradationPaint.setStrokeCap(Paint.Cap.ROUND); mLongGradationPaint.setColor(Color.parseColor("#FF4AA5FD")); // 指針畫筆,這里用到了LinearGradient ,主要是實(shí)現(xiàn)一種漸變效果。 int[] colors = new int[]{0x011f8d8, 0xff0ef4cb, 0x800cf2c3}; LinearGradient linearGradient = new LinearGradient( 0, 0, 100, 100, colors, null, Shader.TileMode.CLAMP ); mIndicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mIndicatorPaint.setColor(indicatorLineColor); mIndicatorPaint.setStrokeWidth(indicatorLineWidth); mIndicatorPaint.setStrokeCap(Paint.Cap.ROUND); mIndicatorPaint.setShader(linearGradient); Bitmap originBp = BitmapFactory.decodeResource(getResources(), R.drawable.indicator); indicatorBp = Bitmap.createScaledBitmap(originBp, dp2px(222), dp2px(6.85f), true); originBp.recycle(); // 手勢(shì)圖標(biāo)畫筆 mGestureAniPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 文字畫筆 mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(textGradationSize); mTextPaint.setColor(textGradationColor); mScroller = new Scroller(context); // 數(shù)字畫筆 mNumPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mNumPaint.setTextSize(textGradationSize); mNumPaint.setColor(textGradationColor); mSoundPool = new SoundPool(10,AudioManager.STREAM_MUSIC,0); soundId = mSoundPool.load(getContext(),R.raw.sound,1); // 震動(dòng)效果 vibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); }
控件測(cè)量
在測(cè)量階段主要是決定控件的大小,這里我們只需要處理測(cè)量模式為AT_MOST的情況下的控件的高。這種模式下不做限制會(huì)導(dǎo)致子控件的高度變得異常:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mWidth = calculateSize(true, widthMeasureSpec); mHeight = calculateSize(false, heightMeasureSpec); mHalfWidth = mWidth >> 1; if (mWidthRangeNumber == 0) { mWidthRangeNumber = (int) (mWidth / gradationGap * mNumberUnit); } Log.d(TAG, "onMeasure: mWidthRangeNumber: " + mWidthRangeNumber + " ,mNumberUnit: " + mNumberUnit); setMeasuredDimension(mWidth, mHeight); }
private int calculateSize(boolean isWidth, int measureSpec) { final int mode = MeasureSpec.getMode(measureSpec); final int size = MeasureSpec.getSize(measureSpec); int realSize = size; if (mode == MeasureSpec.AT_MOST) { if (!isWidth) { int defaultSize = dp2px(74); realSize = Math.min(realSize, defaultSize); } } Log.d(TAG, "mode: " + mode + " ,size: " + size + " ,realSize: " + realSize); return realSize; }
控件繪制
繪制階段主要是繪制背景,然后繪制刻度和數(shù)字,最后繪制指針,然后動(dòng)畫是根據(jù)變量isPlayTipAnim來決定是否繪制的,當(dāng)用戶不點(diǎn)擊控件的時(shí)候,動(dòng)畫會(huì)一直播放,用戶點(diǎn)擊了后停止對(duì)動(dòng)畫的繪制
@Override protected void onDraw(Canvas canvas) { // 繪制背景 canvas.drawColor(bgColor); // 繪制刻度和數(shù)字 drawGradation(canvas); // 繪制指針 drawIndicator(canvas); // 繪制動(dòng)畫的圖標(biāo) if (isPlayTipAnim) { drawGestureAniIcon(canvas); } }
提示動(dòng)畫繪制
繪制動(dòng)畫的時(shí)候我們可以使用一個(gè)ValueAnimator屬性動(dòng)畫來確定一個(gè)動(dòng)畫的范圍,當(dāng)我們開始動(dòng)畫的時(shí)候,這個(gè)類會(huì)給們計(jì)算變化的值,我們把這個(gè)值設(shè)置成小手圖標(biāo)的X坐標(biāo),保持Y坐標(biāo)不變,然后這個(gè)值每改變一次,就觸發(fā)一次重繪,這樣就完成了提示動(dòng)畫的效果了,代碼如下所示:
private void drawGestureAniIcon(Canvas canvas) { if (mGestureTipBp == null) { Bitmap originBp = BitmapFactory.decodeResource(getResources(), R.drawable.ic_gesture_tip); mGestureTipBp = Bitmap.createScaledBitmap(originBp, dp2px(46), dp2px(47), true); mGestureAniTransX = mHalfWidth - (float) mGestureTipBp.getWidth() / 2 + dp2px(2); originBp.recycle(); valueAnimator = ValueAnimator.ofFloat( mHalfWidth - 11 * gradationGap, mHalfWidth + 7 * gradationGap); // 此處做動(dòng)畫的范圍。按照真實(shí)情況合理調(diào)整。 valueAnimator.addUpdateListener(animation -> { mGestureAniTransX = (float) animation.getAnimatedValue(); // Log.d(TAG, "zhongxj111: mGestureAniTransX: " + mGestureAniTransX); invalidate(); }); valueAnimator.setDuration(2000); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.setRepeatMode(ValueAnimator.REVERSE); valueAnimator.start(); } canvas.drawBitmap(mGestureTipBp, mGestureAniTransX, stopLongGradationY - (float) mGestureTipBp.getHeight() / 2 - dp2px(15), mGestureAniPaint ); }
漸變效果的繪制
當(dāng)繪制刻度的時(shí)候,我們需要去實(shí)現(xiàn)繪制漸變效果,就是我們的控件兩邊,如果用戶左右滑動(dòng)的時(shí)候,我們的刻度有漸變的效果,感覺好像是慢慢消失一樣,這里有的讀者可能會(huì)想到讓UI切一張透明的背景,這種方法如果控件的背景是黑色的時(shí)候可行,但是控件的背景是其他的顏色的時(shí)候就會(huì)發(fā)現(xiàn)這個(gè)透明的背景很突兀,感興趣的讀者也可以去嘗試下。我的實(shí)現(xiàn)方式是通過用戶滑動(dòng)的距離換算成透明度設(shè)置給刻度的畫筆,這樣用戶滑動(dòng)的時(shí)候,距離是在變化的,或是變大,或是變小,這時(shí)候再把這個(gè)距離映射成透明的值即可。 我們的Paint的API設(shè)置透明值是一個(gè)整型的數(shù),范圍是0~255
我們只要保證設(shè)置的值在這個(gè)區(qū)間即可。 我們滑動(dòng)的時(shí)候會(huì)得到一個(gè)刻度距離最左邊或者最右邊的距離值,這個(gè)值正好可以用于換算成顏色值,注意:如果刻度間距離設(shè)置得很大,需要重新映射,這里我默認(rèn)刻度在11dp下的,滑動(dòng)的距離剛好在0~255之間 關(guān)鍵代碼如下:
// 給控件開始的6個(gè)刻度做漸變效果 if (distance < 6 * gradationGap) { Log.d(TAG, "distance==>" + distance + " ,curPosIndex=>" + curPosIndex + " ,perUnitCount: " + perUnitCount + " ,factor: " + factor + " ,6*gradationGap: " + 6 * gradationGap); //計(jì)算開始部分的透明值 int startAlpha = Math.abs((int) (distance)); mLongGradationPaint.setAlpha(startAlpha); mShortGradationPaint.setAlpha(startAlpha); mNumPaint.setAlpha(startAlpha); // 給控件的結(jié)尾做漸變效果 } else if (distance > mWidth - 6 * gradationGap) { // 計(jì)算結(jié)束的透明值 int endAlpha = Math.abs((int) ((mWidth + gradationGap) - distance)); // Log.d(TAG, "zhongxj: endAlpha: " + endAlpha); mLongGradationPaint.setAlpha(endAlpha); mShortGradationPaint.setAlpha(endAlpha); mNumPaint.setAlpha(endAlpha); } else { { mShortGradationPaint.setAlpha(255); mLongGradationPaint.setAlpha(255); mShortGradationPaint.setColor(gradationColor); mLongGradationPaint.setColor(Color.parseColor("#FF4AA5FD")); } }
這里還有一個(gè)難點(diǎn)就是結(jié)尾處的漸變值如何設(shè)置,因?yàn)榻Y(jié)尾處的距離超過了0~255范圍,而且這個(gè)漸變值需要和開始部分的透明值保持對(duì)應(yīng)并且是逐漸變小,開始處的透明值是逐漸增大的,比如:開始的透明值是1,2,3,4,那么結(jié)尾處的透明值就必須為4,3,2,1。處理的代碼為:
int endAlpha = Math.abs((int) ((mWidth + gradationGap) - distance));
這里我們可以舉個(gè)例子說明下,比如1,2,3,4,5,6,7,8,9,10 當(dāng)處于2的時(shí)候distance為2,7的時(shí)候distance為7,gradationGap為1,mWidth為10,我們想要把7,8,9,10映射成4,3,2,1,只需要使用:(10+1)-distance(7,8,9,10)就行了,讀者可以去計(jì)算試試。
事件的處理
我們滑動(dòng)屏幕時(shí)判斷如果是橫向滑動(dòng),則使用Scroll滾動(dòng)到我們想要滾動(dòng)的刻度。如果有慣性滾動(dòng),那么慣性滾動(dòng)后,再自動(dòng)吸附到最近的一個(gè)刻度上即可:
@Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getAction(); final int x = (int) event.getX(); final int y = (int) event.getY(); Log.d(TAG, "onTouchEvent: " + action); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (action) { case MotionEvent.ACTION_DOWN: mScroller.forceFinished(true); mDownX = x; isMoved = false; isPlayTipAnim = false; if (valueAnimator != null) { valueAnimator.cancel(); } break; case MotionEvent.ACTION_MOVE: final int dx = x - mLastX; //判斷是否已經(jīng)滑動(dòng) if (!isMoved) { final int dy = y - mLastY; // 滑動(dòng)的觸發(fā)條件,水平滑動(dòng)大于垂直滑動(dòng),滑動(dòng)距離大于閾值 if (Math.abs(dx) < Math.abs(dy) || Math.abs(x - mDownX) < TOUCH_SLOP) { break; } isMoved = true; } mCurrentDistance -= dx; calculateValue(); break; case MotionEvent.ACTION_UP: // 計(jì)算速度:使用1000ms 為單位 mVelocityTracker.computeCurrentVelocity(1000, MAX_FLING_VELOCITY); // 獲取速度,速度有方向性,水平方向,左滑為負(fù),右滑為正 int xVelocity = (int) mVelocityTracker.getXVelocity(); // 達(dá)到速度則慣性滑動(dòng),否則緩慢滑動(dòng)到刻度 if (Math.abs(xVelocity) >= MIN_FLING_VELOCITY) { mScroller.fling((int) mCurrentDistance, 0, -xVelocity, 0, 0, (int) mNumberRangeDistance, 0, 0); invalidate(); } else { scrollToGradation(); } break; } mLastX = x; mLastY = y; return true; }
根據(jù)滑動(dòng)的距離計(jì)算處需要滾動(dòng)的刻度即可:
private void scrollToGradation() { mCurrentNumber = mMinNumber + Math.round(mCurrentDistance / gradationGap) * mNumberUnit; // 算出的值邊界設(shè)置,如果當(dāng)前的值小于最小值,則選最小值,如果當(dāng)前的值大于最大值,則取最大值 mCurrentNumber = Math.min(Math.max(mCurrentNumber, mMinNumber), mMaxNumber); mCurrentDistance = (float) (mCurrentNumber - mMinNumber) / mNumberUnit * gradationGap; currentValue = mCurrentNumber / 10f; // 當(dāng)前的值是放大了10倍處理的,所以回調(diào)值的時(shí)候需要 縮小10倍 if (mValueChangedListener != null) { mValueChangedListener.onValueChanged(currentValue); } // 播放音效和震動(dòng)效果 playSoundEffect(); startVibration(); // 觸發(fā)重繪 invalidate(); }
回調(diào)值給用戶
在滾動(dòng)的時(shí)候和計(jì)算值的時(shí)候?qū)⒅祷卣{(diào)給調(diào)用者
/** * 當(dāng)前值變化監(jiān)聽器 */ public interface OnValueChangedListener { void onValueChanged(float value); }
/** * 根據(jù)distance距離,計(jì)算數(shù)值 */ private void calculateValue() { // 限定范圍在最大值與最小值之間 mCurrentDistance = Math.min(Math.max(mCurrentDistance, 0), mNumberRangeDistance); mCurrentNumber = mMinNumber + (int) (mCurrentDistance / gradationGap) * mNumberUnit; // 因?yàn)橹捣糯罅?0倍處理,所以回調(diào)值的時(shí)候需要縮小10倍 currentValue = mCurrentNumber / 10f; Log.d(TAG, "currentValue: " + currentValue + ",mCurrentDistance: " + mCurrentDistance + " ,mCurrentNumber: " + mCurrentNumber); if (mValueChangedListener != null) { mValueChangedListener.onValueChanged(currentValue); } invalidate(); }
private void scrollToGradation() { mCurrentNumber = mMinNumber + Math.round(mCurrentDistance / gradationGap) * mNumberUnit; // 算出的值邊界設(shè)置,如果當(dāng)前的值小于最小值,則選最小值,如果當(dāng)前的值大于最大值,則取最大值 mCurrentNumber = Math.min(Math.max(mCurrentNumber, mMinNumber), mMaxNumber); mCurrentDistance = (float) (mCurrentNumber - mMinNumber) / mNumberUnit * gradationGap; currentValue = mCurrentNumber / 10f; // 當(dāng)前的值是放大了10倍處理的,所以回調(diào)值的時(shí)候需要 // 縮小10倍 if (mValueChangedListener != null) { mValueChangedListener.onValueChanged(currentValue); } // 播放音效和震動(dòng)效果 playSoundEffect(); startVibration(); invalidate(); }
總結(jié)
本文主要介紹了一個(gè)RulerSeekBar的自定義View,文中只介紹了關(guān)鍵的實(shí)現(xiàn)部分,其他細(xì)節(jié)部分讀者感興趣可以閱讀源碼,源碼的地址為:RulerSeekBar 自定義View的地址,控件使用的是Java語言編寫,雖然現(xiàn)在Android開發(fā)中Kotlin是扛把子,但是由于是給只會(huì)使用JAVA的用戶開發(fā)的控件,所以我使用了JAVA語言,但是Kotlin也能使用,并且如果讀者有時(shí)間可以使用kotlin將這個(gè)控件實(shí)現(xiàn)一下,原理基本一樣,就是使用的語法不同而已。
以上就是Android自定義View實(shí)現(xiàn)帶音效和震動(dòng)的SeekBar的詳細(xì)內(nèi)容,更多關(guān)于Android自定義View實(shí)現(xiàn)SeekBar的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android部分手機(jī)拍照后獲取的圖片被旋轉(zhuǎn)問題的解決方法
這篇文章主要為大家詳細(xì)介紹了Android部分手機(jī)拍照后獲取的圖片被旋轉(zhuǎn)問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01Android提高之多級(jí)樹形菜單的實(shí)現(xiàn)方法
這篇文章主要介紹了Android多級(jí)樹形菜單的實(shí)現(xiàn)方法,很實(shí)用的功能,需要的朋友可以參考下2014-08-08Kotlin擴(kuò)展函數(shù)及實(shí)現(xiàn)機(jī)制的深入探索
擴(kuò)展函數(shù)與擴(kuò)展屬性的神奇之處在于,可以在不修改原來類的條件下,使用函數(shù)和屬性,表現(xiàn)得就像是屬于這個(gè)類的一樣。下面這篇文章主要給大家介紹了關(guān)于Kotlin擴(kuò)展函數(shù)及實(shí)現(xiàn)機(jī)制的相關(guān)資料,需要的朋友可以參考下2018-06-06Android 8.0版本更新無法自動(dòng)安裝問題的解決方法
這篇文章主要為大家詳細(xì)介紹了Android 8.0版本更新無法自動(dòng)安裝問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06android使用url connection示例(get和post數(shù)據(jù)獲取返回?cái)?shù)據(jù))
這篇文章主要介紹了android使用URLConnection來get和post數(shù)據(jù)獲取返回的數(shù)據(jù),大家參考使用吧2014-01-01Flutter實(shí)現(xiàn)仿微信分享功能的示例代碼
Flutter 用來快速開發(fā) Android iOS平臺(tái)應(yīng)用,在Flutter 中,通過 fluwx或者fluwx_no_pay 插件可以實(shí)現(xiàn)微信分享功能,本文將具體介紹實(shí)現(xiàn)的示例代碼,需要的可以參考一下2022-01-01Android模擬器"Failed To Allocate memory 8"錯(cuò)誤如何解決
這篇文章主要介紹了Android模擬器"Failed To Allocate memory 8"錯(cuò)誤如何解決的相關(guān)資料,需要的朋友可以參考下2017-03-03