今天撸一个 文字显示不下换行显示的view
首先聊天页面显示文本 有一个最低高度 和最大宽度,这里直接就写死,或者写屏幕尺寸比例均可。
先定义需要的变量如:最大宽度、 view的宽高、画笔、间距、x轴边距等等

    // 显示聊天内容的画笔private lateinit var mTextPaint: TextPaint// 显示时间 和 绘制图标的画笔private lateinit var mPaint: Paint// 显示文本内容private lateinit var staticLayout: StaticLayout// 最大总宽度private val mMaxWidth = 248.dp2Px()// view的宽高private var mWidth = 0private var mHeight = 0// 绘制时间两侧图标的间隔private val space = 5.dp2Px()// 距离左边X轴的边距private var leftX = 0// 距离上边Y轴的边距private var topY = 0// 绘制的文本private var textContent: CharSequence = ""private var textContentClick: CharSequence = ""// 最后一行文本的宽度private var lineWidth: Float = 0f

在设置显示内容时,处理一下表情显示异常问题,还有特殊文本显示问题例如 @某某某,链接等,在绘制的时候还要处理字符加粗还是正常显示,画笔需要自己实现

fun setTimePaint(paint: Paint): ChatTextViewLayout {this.mPaint = paintreturn this
}fun setTextPaint(textPaint: TextPaint): ChatTextViewLayout {this.mTextPaint = textPaintreturn this
}fun setTextContent(text: CharSequence): ChatTextViewLayout {val spannableStringBuilder = SpannableStringBuilder(text.trim())val fontMetrics: Paint.FontMetrics = mTextPaint.fontMetricsval defaultEmojiSize = fontMetrics.descent - fontMetrics.ascentEmojiManager.getInstance().replaceWithImages(context, spannableStringBuilder, defaultEmojiSize)this.textContentClick = spannableStringBuilderthis.textContent = AtUserHelper.parseAtUserLinkJx(spannableStringBuilder,ContextCompat.getColor(context, R.color.color_at), object :                 AtUserLinkOnClickListener {override fun ulrLinkClick(str: String?) {}override fun atUserClick(str: String?) {}override fun phoneClick(str: String?) {}}).trim()return this
}

然后是测量文本内容的宽高,在这里用的是StaticLayout,如果一行可以显示下,就正常显示  在右侧绘制出显示的时间和状态图标,如果显示不下,那么添加一行高度,在最右侧绘制;如果是多行,就计算出最后一行的文本宽度,逻辑如此。

private fun createLayout() {val maxWidth = mMaxWidthval textWidthRect = mTextPaint.measureText(textContent.toString())val staticLayoutWidth = (if (textWidthRect > maxWidth) maxWidth else textWidthRect).toInt()staticLayout =StaticLayout.Builder.obtain(textContent, 0, textContent.length, mTextPaint, staticLayoutWidth).setText(textContent).setAlignment(Layout.Alignment.ALIGN_NORMAL).setLineSpacing(0.0f, 1.0f).setIncludePad(false).build()try {for (i in 0 until staticLayout.lineCount) {try {lineWidth = staticLayout.getLineWidth(i)if (lineWidth >= staticLayoutWidth) {lineWidth = staticLayoutWidth.toFloat()}
//                    textHeight = max(
//                        textHeight.toDouble(),
//                        ceil(staticLayout.getLineBottom(i).toDouble())
//                    ).toInt()} catch (e: Exception) {e.printStackTrace()break}
//                textWidth = max(textWidth.toDouble(), ceil(lineWidth.toDouble())).toInt()}} catch (e: Exception) {e.printStackTrace()}leftX = 0topY = 0// 先计算发送状态的宽度val sendStateWidth =if (!isTopReadState && sendState == 1) readStateBitmap.width + space else 0// 右侧时间发送状态布局的宽度 = 发送状态的宽度 + 时间宽度 + 间距 + 置顶宽度val timeLayoutWidth =sendStateWidth + timeWidth + space * 2 + if (isTopMsg) topBitmap.width + space else 0/*** 总宽度 如果超出一行 那么取最大宽度* 如果是一行 那么计算 总宽度 = 文本 + 右侧时间发送状态布局的宽度*/mWidth = if (staticLayout.lineCount > 1 || lineWidth > mMaxWidth - timeLayoutWidth) {staticLayoutWidth} else {(lineWidth + timeLayoutWidth).toInt()}// 高度取决于最后一行文本的宽度 如果时间和图标显示不下  那么就添加一行高度/*** 显示不下:*         高度 = 文本高度 +  + 上下边距 + (单行文本高度和间距)* 一行显示:*         高度 = 文本高度 + 上下边距*/// 先判断最后一行文本宽度是否能显示下,总宽度 - 左右间距 - 右侧时间和左右图标的宽度间距mHeight = if (lineWidth > mMaxWidth - timeLayoutWidth) {staticLayout.height / staticLayout.lineCount + staticLayout.height - space} else {staticLayout.height}}

剩下的就简单了,计算绘制就可以了

override fun onDraw(canvas: Canvas) {super.onDraw(canvas)createLayout()// 先绘制文本canvas.save()canvas.translate(leftX.toFloat(), topY.toFloat())staticLayout.draw(canvas)if (!isTopReadState && sendState == 1) {// 绘制右侧发送状态的图标leftX = mWidth - readStateBitmap.width// 右侧发送状态图标较大 稍微偏下一点点topY = mHeight - readStateBitmap.height + space / 2canvas.drawBitmap(readStateBitmap, leftX.toFloat(), topY.toFloat(), mPaint)}// 绘制时间leftX = if (leftX == 0) {mWidth - timeWidth.toInt() - space} else {leftX - timeWidth.toInt() - space}topY = mHeightcanvas.drawText(time, 0, time.length, leftX.toFloat(), topY.toFloat(), mPaint)// 如果置顶绘制置顶if (isTopMsg) {leftX = leftX - topBitmap.width - spacetopY = mHeight - topBitmap.heightcanvas.drawBitmap(topBitmap, leftX.toFloat(), topY.toFloat(), mPaint)}}

最后处理点击事件,因为StaticLayout绘制,SpannableStringBuilder样式可以显示,但点击事件并不行(这里我试过好多次,也换好几种方式,都不支持点击事件,不知道是不是我的姿势不对,如果有人实现了那么请@我,留下代码,让我学习学习),因为显示的时候是SpannableStringBuilder,但是点击的时候计算的位置,所以点击处理用的是原始没有处理过的文本数据,然后拆分判断点击的是某个@或链接,(当时都要吐血了) 先正则判断是什么,在进行替换,然后计算字符,响应点击事件。

```
override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {MotionEvent.ACTION_UP -> {if (event.x >= 0f && event.x <= staticLayout.width && event.y >= 0f && event.y <= staticLayout.height) {val line: Int = staticLayout.getLineForVertical(event.y.toInt())val off: Int = staticLayout.getOffsetForHorizontal(line, event.x)// 进行正则匹配[文字](链接)val spannableString = SpannableStringBuilder(textContentClick)clickTextContentUrl(clickTextContentAt(textContentClick, off, spannableString),off,spannableString)}}}return super.onTouchEvent(event)}/*** 处理点击的是At*/private fun clickTextContentAt(text: CharSequence,off: Int,spannableString: SpannableStringBuilder): SpannableStringBuilder {try {val matcherAt = Pattern.compile(AT_PATTERN).matcher(text)var replaceOffsetAt = 0 //每次替换之后matcher的偏移量while (matcherAt.find()) {// 解析链接  格式是[文字](链接)val name = matcherAt.group(0)val uid = name?.substring(2, name.length - 1)// 把匹配成功的串append进结果串中, 并设置点击效果val groupMemberBean = uid?.let { getGroupDb().getAllMemberById(it) }if (groupMemberBean != null) {val atName = "@" + groupMemberBean.name + " "val clickSpanStartAt = matcherAt.start() - replaceOffsetAtval clickSpanEndAt = clickSpanStartAt + atName.lengthspannableString.replace(matcherAt.start() - replaceOffsetAt,matcherAt.end() - replaceOffsetAt,atName)replaceOffsetAt += matcherAt.end() - matcherAt.start() - atName.lengthval clickableSpan = object : ClickableSpan() {override fun onClick(view: View) {// 点击事件并不灵}}spannableString.setSpan(clickableSpan,clickSpanStartAt,clickSpanEndAt,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)// 点击回调if (clickSpanStartAt <= off && off <= clickSpanEndAt) {postDelayed({ onClickListener.invoke(uid, TEXT_TYPE_AT) }, 100)break}}}} catch (e: java.lang.Exception) {e.printStackTrace()}return spannableString}/*** 处理点击的是链接*/private fun clickTextContentUrl(text: CharSequence,off: Int,spannableString: SpannableStringBuilder) {try {//超链接转化val matcher = Pattern.compile(AtUserHelper.URL_PATTERN).matcher(text)var replaceOffset = 0 //每次替换之后matcher的偏移量while (matcher.find()) {// 解析链接  格式是[文字](链接)val name = matcher.group(0)val clickSpanStart = matcher.start() - replaceOffsetval clickSpanEnd = clickSpanStart + (name?.length ?: 0)spannableString.replace(matcher.start() - replaceOffset,matcher.end() - replaceOffset,name)replaceOffset += matcher.end() - matcher.start() - (name?.length ?: 0)val clickableSpan = object : ClickableSpan() {override fun onClick(view: View) {}}spannableString.setSpan(clickableSpan,clickSpanStart,clickSpanEnd,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)if (clickSpanStart <= off && off <= clickSpanEnd) {postDelayed({ onClickListener.invoke(name, TEXT_TYPE_LINK) }, 100)break}}} catch (e: java.lang.Exception) {e.printStackTrace()}}

下面是完整代码

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.text.*
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.StringUtils
import com.vanniktech.emoji.EmojiManager
import com.ym.base.ext.dp2Px
import com.ym.chat.R
import com.ym.chat.db.ChatDao.getGroupDb
import com.ym.chat.ext.ORIENTATION_LEFT
import com.ym.chat.utils.StringExt.AT_PATTERN
import com.ym.chat.widget.ateditview.AtUserHelper
import com.ym.chat.widget.ateditview.AtUserLinkOnClickListener
import java.util.regex.Pattern/***  description:**  @author  Db_z*  @Date    2023/1/16 13:12*/
class ChatTextViewLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {// 显示聊天内容的画笔private lateinit var mTextPaint: TextPaint// 显示时间 和 绘制图标的画笔private lateinit var mPaint: Paint// 显示文本内容private lateinit var staticLayout: StaticLayout// 点击的文本类型companion object {const val TEXT_TYPE_LINK = 1const val TEXT_TYPE_AT = 2
//        const val TEXT_TYPE_PHONE = 3}private lateinit var onClickListener: (str: String?, textType: Int) -> Unit
//    private var textWidth = 0
//    private var textHeight = 0// view的宽高private var mWidth = 0private var mHeight = 0// 最大总宽度private val mMaxWidth = 248.dp2Px()// 绘制时间两侧图标的间隔private val space = 5.dp2Px()// 距离左边X轴的边距private var leftX = 0// 距离上边Y轴的边距private var topY = 0// 发送状态的图标private lateinit var readStateBitmap: Bitmap// 发送的状态private var sendState = 0// 显示已读的状态private var readState = 0// 置顶的图标private lateinit var topBitmap: Bitmap// 是否置顶private var isTopMsg: Boolean = false// 如果是true 隐藏private var isTopReadState = false// 绘制的文本private var textContent: CharSequence = ""private var textContentClick: CharSequence = ""// 最后一行文本的宽度private var lineWidth: Float = 0f// 绘制的时间private var time: String = "00:00"// 时间的文本宽度private var timeWidth: Float = 0ffun setSendState(sendState: Int): ChatTextViewLayout {this.sendState = sendStatereturn this}fun setReadState(readState: Int, isTop: Boolean): ChatTextViewLayout {this.readState = readStateisTopReadState = isTopreadStateBitmap = if (readState == 1) {BitmapFactory.decodeResource(context.resources, R.drawable.iv_text_read)} else {BitmapFactory.decodeResource(context.resources, R.drawable.iv_text_unread)}return this}fun setTimePaint(paint: Paint): ChatTextViewLayout {this.mPaint = paintreturn this}fun setTextPaint(textPaint: TextPaint): ChatTextViewLayout {this.mTextPaint = textPaintreturn this}fun setTextContent(text: CharSequence): ChatTextViewLayout {val spannableStringBuilder = SpannableStringBuilder(text.trim())val fontMetrics: Paint.FontMetrics = mTextPaint.fontMetricsval defaultEmojiSize = fontMetrics.descent - fontMetrics.ascentEmojiManager.getInstance().replaceWithImages(context, spannableStringBuilder, defaultEmojiSize)this.textContentClick = spannableStringBuilderthis.textContent = AtUserHelper.parseAtUserLinkJx(spannableStringBuilder,ContextCompat.getColor(context, R.color.color_at), object : AtUserLinkOnClickListener {override fun ulrLinkClick(str: String?) {}override fun atUserClick(str: String?) {}override fun phoneClick(str: String?) {}}).trim()return this}fun setTime(time: String): ChatTextViewLayout {this.time = timereturn this}fun showTopMsg(isTopMsg: Boolean, orientation: Int = ORIENTATION_LEFT): ChatTextViewLayout {this.isTopMsg = isTopMsgtopBitmap = if (orientation == ORIENTATION_LEFT){BitmapFactory.decodeResource(context.resources, R.drawable.icon_top_grey)} else {BitmapFactory.decodeResource(context.resources, R.drawable.icon_top_blue)}return this}fun setOnClickListener(onClickListener: (str: String?, textType: Int) -> Unit): ChatTextViewLayout {this.onClickListener = onClickListenerreturn this}fun build() {if (StringUtils.isEmpty(time) || StringUtils.isEmpty(textContent)) returntimeWidth = mPaint.measureText(time)createLayout()setWillNotDraw(false)requestLayout()}/*** 处理点击的是At*/private fun clickTextContentAt(text: CharSequence,off: Int,spannableString: SpannableStringBuilder): SpannableStringBuilder {try {val matcherAt = Pattern.compile(AT_PATTERN).matcher(text)var replaceOffsetAt = 0 //每次替换之后matcher的偏移量while (matcherAt.find()) {// 解析链接  格式是[文字](链接)val name = matcherAt.group(0)val uid = name?.substring(2, name.length - 1)// 把匹配成功的串append进结果串中, 并设置点击效果val groupMemberBean = uid?.let { getGroupDb().getAllMemberById(it) }if (groupMemberBean != null) {val atName = "@" + groupMemberBean.name + " "val clickSpanStartAt = matcherAt.start() - replaceOffsetAtval clickSpanEndAt = clickSpanStartAt + atName.lengthspannableString.replace(matcherAt.start() - replaceOffsetAt,matcherAt.end() - replaceOffsetAt,atName)replaceOffsetAt += matcherAt.end() - matcherAt.start() - atName.lengthval clickableSpan = object : ClickableSpan() {override fun onClick(view: View) {}}spannableString.setSpan(clickableSpan,clickSpanStartAt,clickSpanEndAt,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)if (clickSpanStartAt <= off && off <= clickSpanEndAt) {postDelayed({ onClickListener.invoke(uid, TEXT_TYPE_AT) }, 100)break}}}} catch (e: java.lang.Exception) {e.printStackTrace()}return spannableString}/*** 处理点击的是链接*/private fun clickTextContentUrl(text: CharSequence,off: Int,spannableString: SpannableStringBuilder) {try {//超链接转化val matcher = Pattern.compile(AtUserHelper.URL_PATTERN).matcher(text)var replaceOffset = 0 //每次替换之后matcher的偏移量while (matcher.find()) {// 解析链接  格式是[文字](链接)val name = matcher.group(0)val clickSpanStart = matcher.start() - replaceOffsetval clickSpanEnd = clickSpanStart + (name?.length ?: 0)spannableString.replace(matcher.start() - replaceOffset,matcher.end() - replaceOffset,name)replaceOffset += matcher.end() - matcher.start() - (name?.length ?: 0)val clickableSpan = object : ClickableSpan() {override fun onClick(view: View) {}}spannableString.setSpan(clickableSpan,clickSpanStart,clickSpanEnd,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)if (clickSpanStart <= off && off <= clickSpanEnd) {postDelayed({ onClickListener.invoke(name, TEXT_TYPE_LINK) }, 100)break}}} catch (e: java.lang.Exception) {e.printStackTrace()}}@SuppressLint("ClickableViewAccessibility")override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {MotionEvent.ACTION_UP -> {if (event.x >= 0f && event.x <= staticLayout.width && event.y >= 0f && event.y <= staticLayout.height) {val line: Int = staticLayout.getLineForVertical(event.y.toInt())val off: Int = staticLayout.getOffsetForHorizontal(line, event.x)// 进行正则匹配[文字](链接)val spannableString = SpannableStringBuilder(textContentClick)clickTextContentUrl(clickTextContentAt(textContentClick, off, spannableString),off,spannableString)}}}return super.onTouchEvent(event)}private fun createLayout() {val maxWidth = mMaxWidthval textWidthRect = mTextPaint.measureText(textContent.toString())val staticLayoutWidth = (if (textWidthRect > maxWidth) maxWidth else textWidthRect).toInt()staticLayout =StaticLayout.Builder.obtain(textContent, 0, textContent.length, mTextPaint, staticLayoutWidth).setText(textContent).setAlignment(Layout.Alignment.ALIGN_NORMAL).setLineSpacing(0.0f, 1.0f).setIncludePad(false).build()try {for (i in 0 until staticLayout.lineCount) {try {lineWidth = staticLayout.getLineWidth(i)if (lineWidth >= staticLayoutWidth) {lineWidth = staticLayoutWidth.toFloat()}
//                    textHeight = max(
//                        textHeight.toDouble(),
//                        ceil(staticLayout.getLineBottom(i).toDouble())
//                    ).toInt()} catch (e: Exception) {e.printStackTrace()break}
//                textWidth = max(textWidth.toDouble(), ceil(lineWidth.toDouble())).toInt()}} catch (e: Exception) {e.printStackTrace()}leftX = 0topY = 0// 先计算发送状态的宽度val sendStateWidth =if (!isTopReadState && sendState == 1) readStateBitmap.width + space else 0// 右侧时间发送状态布局的宽度 = 发送状态的宽度 + 时间宽度 + 间距 + 置顶宽度val timeLayoutWidth =sendStateWidth + timeWidth + space * 2 + if (isTopMsg) topBitmap.width + space else 0/*** 总宽度 如果超出一行 那么取最大宽度* 如果是一行 那么计算 总宽度 = 文本 + 右侧时间发送状态布局的宽度*/mWidth = if (staticLayout.lineCount > 1 || lineWidth > mMaxWidth - timeLayoutWidth) {staticLayoutWidth} else {(lineWidth + timeLayoutWidth).toInt()}// 高度取决于最后一行文本的宽度 如果时间和图标显示不下  那么就添加一行高度/*** 显示不下:*         高度 = 文本高度 +  + 上下边距 + (单行文本高度和间距)* 一行显示:*         高度 = 文本高度 + 上下边距*/// 先判断最后一行文本宽度是否能显示下,总宽度 - 左右间距 - 右侧时间和左右图标的宽度间距mHeight = if (lineWidth > mMaxWidth - timeLayoutWidth) {staticLayout.height / staticLayout.lineCount + staticLayout.height - space} else {staticLayout.height}}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)setMeasuredDimension(mWidth, mHeight)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)createLayout()// 先绘制文本canvas.save()canvas.translate(leftX.toFloat(), topY.toFloat())staticLayout.draw(canvas)if (!isTopReadState && sendState == 1) {// 绘制右侧发送状态的图标leftX = mWidth - readStateBitmap.width// 右侧发送状态图标较大 稍微偏下一点点topY = mHeight - readStateBitmap.height + space / 2canvas.drawBitmap(readStateBitmap, leftX.toFloat(), topY.toFloat(), mPaint)}// 绘制时间leftX = if (leftX == 0) {mWidth - timeWidth.toInt() - space} else {leftX - timeWidth.toInt() - space}topY = mHeightcanvas.drawText(time, 0, time.length, leftX.toFloat(), topY.toFloat(), mPaint)// 如果置顶绘制置顶if (isTopMsg) {leftX = leftX - topBitmap.width - spacetopY = mHeight - topBitmap.heightcanvas.drawBitmap(topBitmap, leftX.toFloat(), topY.toFloat(), mPaint)}}override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
}
```
```
// 调用
holder.viewBinding.content.setTextPaint(getChatTextPaint(data)).setTimePaint(getChatTimeTextPaint(ORIENTATION_RIGHT)).showTopMsg(data.isTopMsg, ORIENTATION_RIGHT).setSendState(data.sendState).setReadState(data.msgReadState, data.isTop).setTextContent(data.content).setTime(getTimeStr(data.createTime)).setOnClickListener { str, textType ->when (textType) {TEXT_TYPE_LINK -> {if (isLongDown) return@setOnClickListenerulrLinkClick(context, str, data, holder.layoutPosition, onGroupLinkClickListener)}TEXT_TYPE_AT -> {if (isLongDown) return@setOnClickListeneratUserClick(context, str, data)}}}.build()const val ORIENTATION_LEFT = 1
const val ORIENTATION_RIGHT = 2/*** 获取聊天页面显示时间的画笔* @param orientation 方向 左侧和右侧*/
fun getChatTimeTextPaint(orientation: Int = ORIENTATION_LEFT): Paint {return TextPaint(Paint.ANTI_ALIAS_FLAG).apply {isAntiAlias = truetextSize = 12.dp2Px().toFloat()color =if (orientation == ORIENTATION_LEFT) Color.parseColor("#969799") else Color.parseColor("#0081FF")}
}/*** 获取特殊发言限制的画笔*/
fun getChatTextPaint(data: ChatMessageBean): TextPaint {// 先查询发消息的人在群里是不是群管理或者群主  如果是在查询群设置return TextPaint(Paint.ANTI_ALIAS_FLAG).apply {isAntiAlias = truetextSize = 16.dp2Px().toFloat()// 文本是否加粗isFakeBoldText = falsecolor = Color.parseColor("#323233")}
}

基本上就是全部代码了,其中有自己不需要的进行剔除。
好久没更新,等有时间会进行整理,然后在给出git。

Android 自定义View 一行显示不下换行显示相关推荐

  1. Android自定义View实现图片放大,平移和显示大图片

    原文地址 图片如果不显示:点击这里 前言 首先分析一下需求:将一个图片显示在屏幕上,并能够对其放大或者平移. 用 canvas 画 bitmap 主要有这么几个方法. void drawBitmap( ...

  2. android自定义View之(六)------高仿华为荣耀3C的圆形刻度比例图(ShowPercentView)

    为什么写这篇文章: 显示当前的容量所占的比例,表现当前计划的进度,一般都会采用百分比的方式,而图形显示,以其一目了然的直观性和赏心悦目的美观形成为了我们的当然的首选. 在图形表示百分比的方法中,我们有 ...

  3. android多行文字正中间显示,Android自定义View五(绘制文本大小、多行多列居中)...

    一.绘制文本 在Canvas中绘制文本,使用前面文章的坐标系 1.drawText的几种方法 public void drawText (String text, float x, float y, ...

  4. Android 自定义View

    [Android 自定义View] Android 自定义View 自定义View基础 自定义TextView 继承View重写onDraw方法 View的构造方法 自定义属性 创建attrsxml文 ...

  5. android自定义view获取控件,android 自定义控件View在Activity中使用findByViewId得到结果为null...

    转载:http://blog.csdn.net/xiabing082/article/details/48781489 1.  大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...

  6. Android 自定义view完全解析--带你通透了解自定义view

    参考转自郭霖博客带你一步步深入了解View系列 Android LayoutInflater原理分析 相信接触Android久一点的朋友对于LayoutInflater一定不会陌生,都会知道它主要是用 ...

  7. Android 自定义View合集

    http://blog.csdn.net/u011507982/article/details/51199644 自定义控件学习  https://github.com/GcsSloop/Androi ...

  8. Android自定义View精品(CustomCalendar-定制日历控件)

    版权声明:本文为openXu原创文章[openXu的博客],未经博主允许不得以任何形式转载 目录: 文章目录 1.分析 2.自定义属性 3.onMeasure() 4.onDraw() ①.绘制月份 ...

  9. Android 自定义view onMeasure() 方法剖析

    接着上一篇自定义view 相关的,揭秘 Android Graphics2D 实现动态效果之--invalidate()   内容的介绍,这一篇主要介绍下自定义view 中的 onMeasure()方 ...

最新文章

  1. python opencv resize函数_Python OpenCV中的resize()函数的使用
  2. 猎豹产品经理:「全球化产品」应该要避开的几个坑
  3. java实现语法分析器_200 行 JS 代码,带你实现代码编译器
  4. windows server 注意windows的temp目录
  5. What is Proguard?
  6. 经典排序算法(7)——堆排序算法详解
  7. MySQL(8)存储过程和函数
  8. 算法移植优化(一)android 学习笔记
  9. 贺:MSN-.NET 技术交流群荣登群首页
  10. python输入时间_一文搞懂python日期时间处理
  11. libuv在cocos2d-x中的使用
  12. Android apk的安装、卸载、更新升级(通过Eclipse实现静默安装)
  13. html 在线打开word文件,Html打开pdf、word、xls等文件
  14. 基于MK802的应用开发和相关的工具
  15. 黄教头第六周作业 一个基础的反射型xss
  16. DONT_TOUCH约束
  17. python判断三边是否构成三角形并求面积_Python 计算三角形的面积
  18. 星星之火-42:LTE空口协议栈、数据处理流程与LTE的调制技术大全
  19. 健翔语录——意大利对澳大利亚
  20. Java虚拟机如何运行Java字节码?

热门文章

  1. 今天来聊一聊互联网35岁梗,这个行业真的不需要35岁以上从业人员?
  2. csrf漏洞复现(附源码)
  3. ubuntu 软件推荐
  4. c语言里输入12MmAA后回车,Word怎么用语言输入
  5. 抽象类和接口的相同点和不同点
  6. clover EFI bootloaderfor mac(10.12)官方版下载
  7. 计算机镜像怎么恢复,利用系统映像还原Windows7系统的方法
  8. 猴子吃桃,C语言,递归法
  9. Aurora8B10B IP使用 -02- IP功能设计技巧
  10. JFreeChart Hacking-补丁贴