主頁 > 移動端開發 > TextView繪制流程

TextView繪制流程

2021-02-26 11:24:41 移動端開發

TextView是android提供的一個文本展示ui控制元件,同時也是android開發者最先熟悉的Weight組件,可以配合Html和Spannable進行展示文字、展示html、進行高亮處理,還能通過autolink進行email、tel等功能的識別跳轉,本篇文章將帶你從系統原始碼的角度徹底搞定TextView的繪制流程,

TextView的依賴關系
在這里插入圖片描述
TextView本身是一個自定義View控制元件,所以對于Textview的分析,可以直接按照常用的自定義View繪制流程來分析,

  • onMeasure
  • onLayout
  • onDraw
  • onTouchEvent

onMeasure

在onMeasure中,按照常規對于自定義View的流程,我們主要是確定控制元件本身的寬、高是如何確定出來的,

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       int widthMode = MeasureSpec.getMode(widthMeasureSpec);
       int heightMode = MeasureSpec.getMode(heightMeasureSpec);
       int widthSize = MeasureSpec.getSize(widthMeasureSpec);
       int heightSize = MeasureSpec.getSize(heightMeasureSpec);

       int width;
       int height;

		//BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
		// UNKNOWN_BORING 是一個metrics物件,Metrics物件主要是用來確定文字的繪制,
		//對于Metrics可以參考文章: https://blog.csdn.net/wanggang514260663/article/details/113845402
       BoringLayout.Metrics boring = UNKNOWN_BORING;
       BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
		
		//回傳文字的對齊方式,比如LTR和RTL
       if (mTextDir == null) {
           mTextDir = getTextDirectionHeuristic();
       }
		
       int des = -1;
       boolean fromexisting = false;
       final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
               ?  (float) widthSize : Float.MAX_VALUE;
		// 如果是使用確定值的大小測量方式,則使用測量的確定值
       if (widthMode == MeasureSpec.EXACTLY) {
           width = widthSize;
       } else {
           if (mLayout != null && mEllipsize == null) {
               //如果文字行數>1,則回傳-1,否則回傳該行文字長度
               des = desired(mLayout);
           }
           // 如果行數>1
           if (des < 0) {
               //BoringLayout是Layout的最簡單的實作,主要用于適配單行文字展示,
               //并且只支持從左到右的展示方向,不建議在自己的開發程序中直接使用,
               //如果需要使用的話,首先使用isBoring判斷文字是否符合要求,
               boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
               //BoringLayout.isBoring判斷如果負責BoringLayout要求,則回傳文測量結果Metrics物件,
               //否則回傳null
               if (boring != null) {
                   mBoring = boring;
               }
           } else {
               fromexisting = true;
           }
           //boring == null表示行數==0 并且不支持boringLayout方式
           if (boring == null || boring == UNKNOWN_BORING) {
              //des < 0,則表示文字有多行
               if (des < 0) {
                   des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
                           mTransformed.length(), mTextPaint, mTextDir, widthLimit));
               }
               width = des;
           } else {
               //測量的文字的寬度
               width = boring.width;
           }
			
           final Drawables dr = mDrawables;
           if (dr != null) {
               width = Math.max(width, dr.mDrawableWidthTop);
               width = Math.max(width, dr.mDrawableWidthBottom);
           }

           if (mHint != null) {
               int hintDes = -1;
               int hintWidth;

               if (mHintLayout != null && mEllipsize == null) {
                   hintDes = desired(mHintLayout);
               }

               if (hintDes < 0) {
                   hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                   if (hintBoring != null) {
                       mHintBoring = hintBoring;
                   }
               }

               if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                   if (hintDes < 0) {
                       hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
                               mHint.length(), mTextPaint, mTextDir, widthLimit));
                   }
                   hintWidth = hintDes;
               } else {
                   hintWidth = hintBoring.width;
               }

               if (hintWidth > width) {
                   width = hintWidth;
               }
           }
			
			//寬度需要加上內間距
           width += getCompoundPaddingLeft() + getCompoundPaddingRight();
			
           if (mMaxWidthMode == EMS) {
               width = Math.min(width, mMaxWidth * getLineHeight());
           } else {
               width = Math.min(width, mMaxWidth);
           }

           if (mMinWidthMode == EMS) {
               width = Math.max(width, mMinWidth * getLineHeight());
           } else {
               width = Math.max(width, mMinWidth);
           }

           // Check against our minimum width
           width = Math.max(width, getSuggestedMinimumWidth());

           if (widthMode == MeasureSpec.AT_MOST) {
               width = Math.min(widthSize, width);
           }
       }
		// 文字的真實占用寬度,不包含padding值
       int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
       int unpaddedWidth = want;
	   //如果支持滾動,則文字寬度設定為 VERY_WIDE = 1024 * 1024;
       if (mHorizontallyScrolling) want = VERY_WIDE;
       int hintWant = want;
       int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
       if (mLayout == null) {
          //如果Layout物件為null,則使用makeNewLayout構造出來一個Layout物件
           makeNewLayout(want, hintWant, boring, hintBoring,
                         width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
       } else {
           final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant)
                   || (mLayout.getEllipsizedWidth()
                           != width - getCompoundPaddingLeft() - getCompoundPaddingRight());

           final boolean widthChanged = (mHint == null) && (mEllipsize == null)
                   && (want > mLayout.getWidth())
                   && (mLayout instanceof BoringLayout
                           || (fromexisting && des >= 0 && des <= want));

           final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
			//如果文字發生了變化
           if (layoutChanged || maximumChanged) {
               if (!maximumChanged && widthChanged) {
                   //將Layout的寬度設定為期待的寬度want值
                   mLayout.increaseWidthTo(want);
               } else {
                   //重新計算構建Layout
                   makeNewLayout(want, hintWant, boring, hintBoring,
                           width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
               }
           } else {
               // Nothing has changed
           }
       }  
       if (heightMode == MeasureSpec.EXACTLY) {
           // Parent has told us how big to be. So be it.
           // 如果測量方式為固定值方式,則使用測量出來的值
           height = heightSize;
           mDesiredHeightAtMeasure = -1;
       } else {
           //getDesiredHeight方法會根據text和hint計算出來一個最大的高度
           int desired = getDesiredHeight();
           height = desired;
           mDesiredHeightAtMeasure = desired;
			//如果是at_most方式,則取測量值和desired的相對小的數值
           if (heightMode == MeasureSpec.AT_MOST) {
               height = Math.min(desired, heightSize);
           }
       }
		//回傳不包含padding值的高度
       int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
       //如果多行
       if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
          //mLayout.getLineTop的值是回傳指定行到頂部的高度,也就是對應的指定行的高度
           unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
       }

       /*
        * We didn't let makeNewLayout() register to bring the cursor into view,
        * so do it here if there is any possibility that it is needed.
        */
       if (mMovement != null
               || mLayout.getWidth() > unpaddedWidth
               || mLayout.getHeight() > unpaddedHeight) {
           registerForPreDraw();
       } else {
           scrollTo(0, 0);
       }
       setMeasuredDimension(width, height);
   }
  • TextView#desired
private static int desired(Layout layout) {
    int n= layout.getLineCount();
    CharSequence text = layout.getText();
    float max = 0;

    // if any line was wrapped, we can't use it.
    // but it's ok for the last line not to have a newline
	//如果行數>1,則回傳-1
    for (int i = 0; i < n - 1; i++) {
        //判斷是否每一行的最后為\n換行
        if (text.charAt(layout.getLineEnd(i) - 1) != '\n') {
            return -1;
        }
    }
    for (int i = 0; i < n; i++) {
        //判斷如果行寬度如果>0,則回傳,否則回傳0
        max = Math.max(max, layout.getLineWidth(i));
    }
    return (int) Math.ceil(max);
}
  • TextView#makeNewLayout
public void makeNewLayout(int wantWidth, int hintWidth,
                                 BoringLayout.Metrics boring,
                                 BoringLayout.Metrics hintBoring,
                                 int ellipsisWidth, boolean bringIntoView) {
    //停止掉跑馬燈效果                             
    stopMarquee();

    // Update "old" cached values
    mOldMaximum = mMaximum;
    mOldMaxMode = mMaxMode;

    mHighlightPathBogus = true;

    if (wantWidth < 0) {
        wantWidth = 0;
    }
    if (hintWidth < 0) {
        hintWidth = 0;
    }

    Layout.Alignment alignment = getLayoutAlignment();
    final boolean testDirChange = mSingleLine && mLayout != null
            && (alignment == Layout.Alignment.ALIGN_NORMAL
                    || alignment == Layout.Alignment.ALIGN_OPPOSITE);
    int oldDir = 0;
    if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
    //判斷是否支持展示身略號...
    boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
    //判斷是否展示跑馬燈效果
    final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE
            && mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
    TruncateAt effectiveEllipsize = mEllipsize;
    if (mEllipsize == TruncateAt.MARQUEE
            && mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
        //省略號展示..
        effectiveEllipsize = TruncateAt.END_SMALL;
    }
	
	//獲取文欄位落方向
    if (mTextDir == null) {
        mTextDir = getTextDirectionHeuristic();
    }
	//獲取layout
    mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
            effectiveEllipsize, effectiveEllipsize == mEllipsize);
    if (switchEllipsize) {
        TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE
                ? TruncateAt.END : TruncateAt.MARQUEE;
        //對于跑馬燈效果,單獨保存一個跑馬燈模式的Layout
        mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
                shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
    }

    shouldEllipsize = mEllipsize != null;
    mHintLayout = null;
	//準備計算Hint使用的Layout
    if (mHint != null) {
        if (shouldEllipsize) hintWidth = wantWidth;

        if (hintBoring == UNKNOWN_BORING) {
            hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
                                               mHintBoring);
            if (hintBoring != null) {
                mHintBoring = hintBoring;
            }
        }

        if (hintBoring != null) {
            if (hintBoring.width <= hintWidth
                    && (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
                if (mSavedHintLayout != null) {
                    mHintLayout = mSavedHintLayout.replaceOrMake(mHint, mTextPaint,
                            hintWidth, alignment, mSpacingMult, mSpacingAdd,
                            hintBoring, mIncludePad);
                } else {
                    mHintLayout = BoringLayout.make(mHint, mTextPaint,
                            hintWidth, alignment, mSpacingMult, mSpacingAdd,
                            hintBoring, mIncludePad);
                }

                mSavedHintLayout = (BoringLayout) mHintLayout;
            } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
                if (mSavedHintLayout != null) {
                    mHintLayout = mSavedHintLayout.replaceOrMake(mHint, mTextPaint,
                            hintWidth, alignment, mSpacingMult, mSpacingAdd,
                            hintBoring, mIncludePad, mEllipsize,
                            ellipsisWidth);
                } else {
                    mHintLayout = BoringLayout.make(mHint, mTextPaint,
                            hintWidth, alignment, mSpacingMult, mSpacingAdd,
                            hintBoring, mIncludePad, mEllipsize,
                            ellipsisWidth);
                }
            }
        }
        // TODO: code duplication with makeSingleLayout()
        if (mHintLayout == null) {
            StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
                    mHint.length(), mTextPaint, hintWidth)
                    .setAlignment(alignment)
                    .setTextDirection(mTextDir)
                    .setLineSpacing(mSpacingAdd, mSpacingMult)
                    .setIncludePad(mIncludePad)
                    .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                    .setBreakStrategy(mBreakStrategy)
                    .setHyphenationFrequency(mHyphenationFrequency)
                    .setJustificationMode(mJustificationMode)
                    .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
            if (shouldEllipsize) {
                builder.setEllipsize(mEllipsize)
                        .setEllipsizedWidth(ellipsisWidth);
            }
            mHintLayout = builder.build();
        }
    }

    if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
        registerForPreDraw();
    }

    if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
        if (!compressText(ellipsisWidth)) {
            final int height = mLayoutParams.height;
            // If the size of the view does not depend on the size of the text, try to
            // start the marquee immediately
            if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
                startMarquee();
            } else {
                // Defer the start of the marquee until we know our width (see setFrame())
                mRestartMarquee = true;
            }
        }
    }

    // CursorControllers need a non-null mLayout
    if (mEditor != null) mEditor.prepareCursorControllers();
}
  • TextView#makeSingleLayout
    這個方法
protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
            Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
            boolean useSaved) {
    Layoutresult = null;
    //userDynamicLayout = isTextSelectable() || (mSpannable != null && mPrecomputed == null);
    //如果滿足上面的條件,則使用DynamicLayout
    if (useDynamicLayout()) {
        final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint,
                wantWidth)
                .setDisplayText(mTransformed)
                .setAlignment(alignment)
                .setTextDirection(mTextDir)
                .setLineSpacing(mSpacingAdd, mSpacingMult)
                .setIncludePad(mIncludePad)
                .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                .setBreakStrategy(mBreakStrategy)
                .setHyphenationFrequency(mHyphenationFrequency)
                .setJustificationMode(mJustificationMode)
                .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
                .setEllipsizedWidth(ellipsisWidth);
        result = builder.build();
    } else {
        if (boring == UNKNOWN_BORING) {
            //判斷是否使用BoringLayout
            boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
            if (boring != null) {
                mBoring = boring;
            }
        }
		//bording != null 則表示使用Boringlayout 	
        if (boring != null) {
            if (boring.width <= wantWidth
                    && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
                if (useSaved && mSavedLayout != null) {
                    result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
                            wantWidth, alignment, mSpacingMult, mSpacingAdd,
                            boring, mIncludePad);
                } else {
                    result = BoringLayout.make(mTransformed, mTextPaint,
                            wantWidth, alignment, mSpacingMult, mSpacingAdd,
                            boring, mIncludePad);
                }

                if (useSaved) {
                    mSavedLayout = (BoringLayout) result;
                }
            } else if (shouldEllipsize && boring.width <= wantWidth) {
                if (useSaved && mSavedLayout != null) {
                    result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
                            wantWidth, alignment, mSpacingMult, mSpacingAdd,
                            boring, mIncludePad, effectiveEllipsize,
                            ellipsisWidth);
                } else {
                    result = BoringLayout.make(mTransformed, mTextPaint,
                            wantWidth, alignment, mSpacingMult, mSpacingAdd,
                            boring, mIncludePad, effectiveEllipsize,
                            ellipsisWidth);
                }
            }
        }
    }
    //如果不滿足BoringLayout 并且不滿足 DynamicLayout 則使用StaticLayout
    if (result == null) {
        StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
                0, mTransformed.length(), mTextPaint, wantWidth)
                .setAlignment(alignment)
                .setTextDirection(mTextDir)
                .setLineSpacing(mSpacingAdd, mSpacingMult)
                .setIncludePad(mIncludePad)
                .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                .setBreakStrategy(mBreakStrategy)
                .setHyphenationFrequency(mHyphenationFrequency)
                .setJustificationMode(mJustificationMode)
                .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
        if (shouldEllipsize) {
            builder.setEllipsize(effectiveEllipsize)
                    .setEllipsizedWidth(ellipsisWidth);
        }
        result = builder.build();
    }
    return result;
}

onLayout

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 super.onLayout(changed, left, top, right, bottom);
    if (mDeferScroll >= 0) {
        int curs = mDeferScroll;
        mDeferScroll = -1;
        bringPointIntoView(Math.min(curs, mText.length()));
    }
    // Call auto-size after the width and height have been calculated.
    //這里會判斷如果支持autoSize的話,會重新計算并設定文字的大小
    autoSizeText();
}

onDraw

protected void onDraw(Canvas canvas) {
		
	//重新開啟跑馬燈	
    restartMarqueeIfNeeded();

     // Draw the background for this view
     super.onDraw(canvas);
		 	
     final int compoundPaddingLeft = getCompoundPaddingLeft();
     final int compoundPaddingTop = getCompoundPaddingTop();
     final int compoundPaddingRight = getCompoundPaddingRight();
     final int compoundPaddingBottom = getCompoundPaddingBottom();
     final int scrollX = mScrollX;
     final int scrollY = mScrollY;
     final int right = mRight;
     final int left = mLeft;
     final int bottom = mBottom;
     final int top = mTop;
     final boolean isLayoutRtl = isLayoutRtl();
     final int offset = getHorizontalOffsetForDrawables();
     final int leftOffset = isLayoutRtl ? 0 : offset;
     final int rightOffset = isLayoutRtl ? offset : 0;

     final Drawables dr = mDrawables;
     if (dr != null) {
         /*
          * Compound, not extended, because the icon is not clipped
          * if the text height is smaller.
          */
         int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
         int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;

         // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
         // Make sure to update invalidateDrawable() when changing this code.
         if (dr.mShowing[Drawables.LEFT] != null) {
             canvas.save();
             canvas.translate(scrollX + mPaddingLeft + leftOffset,
                     scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
             //將drawable繪制到螢屏上        
             dr.mShowing[Drawables.LEFT].draw(canvas);
             canvas.restore();
         }
		...這里省略類似的其他方向上drawable繪制
     int color = mCurTextColor;

     if (mLayout == null) {
         assumeLayout();
     }

     Layout layout = mLayout;

     if (mHint != null && mText.length() == 0) {
         if (mHintTextColor != null) {
             color = mCurHintTextColor;
         }
         layout = mHintLayout;
     }

     mTextPaint.setColor(color);
     mTextPaint.drawableState = getDrawableState();

     canvas.save();
     /*  Would be faster if we didn't have to do this. Can we chop the
         (displayable) text so that we don't need to do this ever?
     */

     int extendedPaddingTop = getExtendedPaddingTop();
     int extendedPaddingBottom = getExtendedPaddingBottom();

     final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
     final int maxScrollY = mLayout.getHeight() - vspace;

     float clipLeft = compoundPaddingLeft + scrollX;
     float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
     float clipRight = right - left - getCompoundPaddingRight() + scrollX;
     float clipBottom = bottom - top + scrollY
             - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);

     if (mShadowRadius != 0) {
         clipLeft += Math.min(0, mShadowDx - mShadowRadius);
         clipRight += Math.max(0, mShadowDx + mShadowRadius);

         clipTop += Math.min(0, mShadowDy - mShadowRadius);
         clipBottom += Math.max(0, mShadowDy + mShadowRadius);
     }

     canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);

     int voffsetText = 0;
     int voffsetCursor = 0;

     // translate in by our padding
     /* shortcircuit calling getVerticaOffset() */
     if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
         voffsetText = getVerticalOffset(false);
         voffsetCursor = getVerticalOffset(true);
     }
     canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);

     final int layoutDirection = getLayoutDirection();
     final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
     if (isMarqueeFadeEnabled()) {
         if (!mSingleLine && getLineCount() == 1 && canMarquee()
                 && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
             final int width = mRight - mLeft;
             final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
             final float dx = mLayout.getLineRight(0) - (width - padding);
             canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
         }

         if (mMarquee != null && mMarquee.isRunning()) {
             final float dx = -mMarquee.getScroll();
             canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
         }
     }

     final int cursorOffsetVertical = voffsetCursor - voffsetText;

     Path highlight = getUpdatedHighlightPath();
     if (mEditor != null) {
         //如果是可編輯的文字,使用Editor的onDraw方法
         mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
     } else {
         //非可編輯文字,使用Layout#draw方法
         layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
     }
	 //跑馬燈	
     if (mMarquee != null && mMarquee.shouldDrawGhost()) {
         final float dx = mMarquee.getGhostOffset();
         canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
         layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
     }

     canvas.restore();
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/263850.html

標籤:其他

上一篇:安卓第八趴

下一篇:Android——Messenger

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more