Android 自绘TextView解决提前换行问题,支持图文混排
先看下效果图:
上面是MTextView,下面是默认的TextView。
一、原因
用最简单的全英文句子为例,如果有一个很长的单词,这一行剩余的空间显示不下了,那么规则就是不打断单词,而是把整个单词丢到下一行开始显示。这样 本来没有错。一是咱们中国人都是方块字,怎么都放得下,不存在英文的这个问题。所以不习惯那个排版。二是如果TextView里面有图片,如图,不知道判断单词的代码是怎么弄得,总之它觉得最后一个啦字和后面的一串表情应该是一个整体,不能分开,就一起丢到第二行了,也就造成了这种难看的排版。要验证这个说法也很简单,自己去QQ里试一试,在每个表情之间都加一个空格,就会发现排版一下子正常了。
二、解决方法
最简单的就是表情之间加空格,如果不想这么做,就只有自己来画啦。
先给初学的朋友解释一下View绘制的流程,首先是onMeasure(int widthMeasureSpec, int heightMeasureSpec),onMeasure执行的时候,就是父View在问你,小朋友,你要占多大的地儿呀?当然,问你的时候,会给你个 限制条件,就是那两参数,以widthMeasureSpec为例,这参数不能直接用,得先拆开,用int widthMode = MeasureSpec.getMode(widthMeasureSpec) 和 int widthSize = MeasureSpec.getSize(widthMeasureSpec);widthMode就三种情况:
MeasureSpec.EXACTLY:你就widthSize那么宽就行了。
MeasureSpec.AT_MOST:你最多只能widthSize那么宽。
MeasureSpec.UNSPECIFIED:未指定,你爱多宽多宽。
当然,其实这只父View给你的建议,遵不遵守你自己看着办,但是自己乱来导致显示不全就不是父View的错了。
最终你听取了建议,思量了一番,觉得自己应该有width那么宽,height那么高,最后就得用setMeasuredDimension(width, height)这个函数真正确定自己的高宽。然后onMeasure()的工作就完了。
然后就是onDraw(Canvas canvas),这个就简单了,canvas就是父View给的一块画布,爱在上面画啥都行,比如写个字drawText(String text,float x, float y, Paint paint),
text是要写的字,paint是写字的笔,值得注意的是x,y坐标是相对于你自己这一小块画布的左上角的。最左上就是0,0右下是width,height
上代码
- /**
- * @author huangwei
- * @version SocialClient 1.2.0
- * @功能 图文混排TextView,请使用{@link #setMText(CharSequence)}
- * @2014年5月27日
- * @下午5:29:27
- */
- public class MTextView extends TextView
- {
- /**
- * 缓存测量过的数据
- */
- private static HashMap<String, SoftReference<MeasuredData>> measuredData = new HashMap<String, SoftReference<MeasuredData>>();
- private static int hashIndex = ;
- /**
- * 存储当前文本内容,每个item为一行
- */
- ArrayList<LINE> contentList = new ArrayList<LINE>();
- private Context context;
- /**
- * 用于测量字符宽度
- */
- private TextPaint paint = new TextPaint();
- // private float lineSpacingMult = 0.5f;
- private int textColor = Color.BLACK;
- //行距
- private float lineSpacing;
- private int lineSpacingDP = ;
- /**
- * 最大宽度
- */
- private int maxWidth;
- /**
- * 只有一行时的宽度
- */
- private int oneLineWidth = -;
- /**
- * 已绘的行中最宽的一行的宽度
- */
- private float lineWidthMax = -;
- /**
- * 存储当前文本内容,每个item为一个字符或者一个SpanObject
- */
- private ArrayList<Object> obList = new ArrayList<Object>();
- /**
- * 是否使用默认{@link #onMeasure(int, int)}和{@link #onDraw(Canvas)}
- */
- private boolean useDefault = false;
- private CharSequence text = "";
- private int minHeight;
- /**
- * 用以获取屏幕高宽
- */
- private DisplayMetrics displayMetrics;
- /**
- * {@link android.text.style.BackgroundColorSpan}用
- */
- private Paint textBgColorPaint = new Paint();
- /**
- * {@link android.text.style.BackgroundColorSpan}用
- */
- private Rect textBgColorRect = new Rect();
- public MTextView(Context context)
- {
- super(context);
- this.context = context;
- paint.setAntiAlias(true);
- lineSpacing = dip2px(context, lineSpacingDP);
- minHeight = dip2px(context, );
- displayMetrics = new DisplayMetrics();
- }
- public MTextView(Context context,AttributeSet attrs)
- {
- super(context,attrs);
- this.context = context;
- paint.setAntiAlias(true);
- lineSpacing = dip2px(context, lineSpacingDP);
- minHeight = dip2px(context, );
- displayMetrics = new DisplayMetrics();
- }
- public static int px2sp(Context context, float pxValue)
- {
- final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
- return (int) (pxValue / fontScale + 0.5f);
- }
- /**
- * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
- */
- public static int dip2px(Context context, float dpValue)
- {
- final float scale = context.getResources().getDisplayMetrics().density;
- return (int) (dpValue * scale + 0.5f);
- }
- @Override
- public void setMaxWidth(int maxpixels)
- {
- super.setMaxWidth(maxpixels);
- maxWidth = maxpixels;
- }
- @Override
- public void setMinHeight(int minHeight)
- {
- super.setMinHeight(minHeight);
- this.minHeight = minHeight;
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- {
- if (useDefault)
- {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- return;
- }
- int width = , height = ;
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
- switch (widthMode)
- {
- case MeasureSpec.EXACTLY:
- width = widthSize;
- break;
- case MeasureSpec.AT_MOST:
- width = widthSize;
- break;
- case MeasureSpec.UNSPECIFIED:
- ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
- width = displayMetrics.widthPixels;
- break;
- default:
- break;
- }
- if (maxWidth > )
- width = Math.min(width, maxWidth);
- paint.setTextSize(this.getTextSize());
- paint.setColor(textColor);
- int realHeight = measureContentHeight((int) width);
- //如果实际行宽少于预定的宽度,减少行宽以使其内容横向居中
- int leftPadding = getCompoundPaddingLeft();
- int rightPadding = getCompoundPaddingRight();
- width = Math.min(width, (int) lineWidthMax + leftPadding + rightPadding);
- if (oneLineWidth > -)
- {
- width = oneLineWidth;
- }
- switch (heightMode)
- {
- case MeasureSpec.EXACTLY:
- height = heightSize;
- break;
- case MeasureSpec.AT_MOST:
- height = realHeight;
- break;
- case MeasureSpec.UNSPECIFIED:
- height = realHeight;
- break;
- default:
- break;
- }
- height += getCompoundPaddingTop() + getCompoundPaddingBottom();
- height = Math.max(height, minHeight);
- setMeasuredDimension(width, height);
- }
- @Override
- protected void onDraw(Canvas canvas)
- {
- if (useDefault)
- {
- super.onDraw(canvas);
- return;
- }
- if (contentList.isEmpty())
- return;
- int width;
- Object ob;
- int leftPadding = getCompoundPaddingLeft();
- int topPadding = getCompoundPaddingTop();
- float height = + topPadding + lineSpacing;
- //只有一行时
- if (oneLineWidth != -)
- {
- height = getMeasuredHeight() / - contentList.get().height / ;
- }
- for (LINE aContentList : contentList)
- {
- //绘制一行
- float realDrawedWidth = leftPadding;
- for (int j = ; j < aContentList.line.size(); j++)
- {
- ob = aContentList.line.get(j);
- width = aContentList.widthList.get(j);
- if (ob instanceof String)
- {
- canvas.drawText((String) ob, realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
- realDrawedWidth += width;
- }
- else if (ob instanceof SpanObject)
- {
- Object span = ((SpanObject) ob).span;
- if(span instanceof ImageSpan)
- {
- ImageSpan is = (ImageSpan) span;
- Drawable d = is.getDrawable();
- int left = (int) (realDrawedWidth);
- int top = (int) height;
- int right = (int) (realDrawedWidth + width);
- int bottom = (int) (height + aContentList.height);
- d.setBounds(left, top, right, bottom);
- d.draw(canvas);
- realDrawedWidth += width;
- }
- else if(span instanceof BackgroundColorSpan)
- {
- textBgColorPaint.setColor(((BackgroundColorSpan) span).getBackgroundColor());
- textBgColorPaint.setStyle(Style.FILL);
- textBgColorRect.left = (int) realDrawedWidth;
- int textHeight = (int) getTextSize();
- textBgColorRect.top = (int) (height + aContentList.height - textHeight - paint.getFontMetrics().descent);
- textBgColorRect.right = textBgColorRect.left+width;
- textBgColorRect.bottom = (int) (height + aContentList.height + lineSpacing - paint.getFontMetrics().descent);
- canvas.drawRect(textBgColorRect, textBgColorPaint);
- canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
- realDrawedWidth += width;
- }
- else//做字符串处理
- {
- canvas.drawText(((SpanObject) ob).source.toString(), realDrawedWidth, height + aContentList.height - paint.getFontMetrics().descent, paint);
- realDrawedWidth += width;
- }
- }
- }
- height += aContentList.height + lineSpacing;
- }
- }
- @Override
- public void setTextColor(int color)
- {
- super.setTextColor(color);
- textColor = color;
- }
- /**
- * 用于带ImageSpan的文本内容所占高度测量
- * @param width 预定的宽度
- * @return 所需的高度
- */
- private int measureContentHeight(int width)
- {
- int cachedHeight = getCachedData(text.toString(), width);
- if (cachedHeight > )
- {
- return cachedHeight;
- }
- // 已绘的宽度
- float obWidth = ;
- float obHeight = ;
- float textSize = this.getTextSize();
- FontMetrics fontMetrics = paint.getFontMetrics();
- //行高
- float lineHeight = fontMetrics.bottom - fontMetrics.top;
- //计算出的所需高度
- float height = lineSpacing;
- int leftPadding = getCompoundPaddingLeft();
- int rightPadding = getCompoundPaddingRight();
- float drawedWidth = ;
- boolean splitFlag = false;//BackgroundColorSpan拆分用
- width = width - leftPadding - rightPadding;
- oneLineWidth = -;
- contentList.clear();
- StringBuilder sb;
- LINE line = new LINE();
- for (int i = ; i < obList.size(); i++)
- {
- Object ob = obList.get(i);
- if (ob instanceof String)
- {
- obWidth = paint.measureText((String) ob);
- obHeight = textSize;
- }
- else if (ob instanceof SpanObject)
- {
- Object span = ((SpanObject) ob).span;
- if(span instanceof ImageSpan)
- {
- Rect r = ((ImageSpan)span).getDrawable().getBounds();
- obWidth = r.right - r.left;
- obHeight = r.bottom - r.top;
- if (obHeight > lineHeight)
- lineHeight = obHeight;
- }
- else if(span instanceof BackgroundColorSpan)
- {
- String str = ((SpanObject) ob).source.toString();
- obWidth = paint.measureText(str);
- obHeight = textSize;
- //如果太长,拆分
- int k= str.length()-;
- while(width - drawedWidth < obWidth)
- {
- obWidth = paint.measureText(str.substring(,k--));
- }
- if(k < str.length()-)
- {
- splitFlag = true;
- SpanObject so1 = new SpanObject();
- so1.start = ((SpanObject) ob).start;
- so1.end = so1.start + k;
- so1.source = str.substring(,k+);
- so1.span = ((SpanObject) ob).span;
- SpanObject so2 = new SpanObject();
- so2.start = so1.end;
- so2.end = ((SpanObject) ob).end;
- so2.source = str.substring(k+,str.length());
- so2.span = ((SpanObject) ob).span;
- ob = so1;
- obList.set(i,so2);
- i--;
- }
- }//做字符串处理
- else
- {
- String str = ((SpanObject) ob).source.toString();
- obWidth = paint.measureText(str);
- obHeight = textSize;
- }
- }
- //这一行满了,存入contentList,新起一行
- if (width - drawedWidth < obWidth || splitFlag)
- {
- splitFlag = false;
- contentList.add(line);
- if (drawedWidth > lineWidthMax)
- {
- lineWidthMax = drawedWidth;
- }
- drawedWidth = ;
- height += line.height + lineSpacing;
- lineHeight = obHeight;
- line = new LINE();
- }
- drawedWidth += obWidth;
- if (ob instanceof String && line.line.size() > && (line.line.get(line.line.size() - ) instanceof String))
- {
- int size = line.line.size();
- sb = new StringBuilder();
- sb.append(line.line.get(size - ));
- sb.append(ob);
- ob = sb.toString();
- obWidth = obWidth + line.widthList.get(size - );
- line.line.set(size - , ob);
- line.widthList.set(size - , (int) obWidth);
- line.height = (int) lineHeight;
- }
- else
- {
- line.line.add(ob);
- line.widthList.add((int) obWidth);
- line.height = (int) lineHeight;
- }
- }
- if (drawedWidth > lineWidthMax)
- {
- lineWidthMax = drawedWidth;
- }
- if (line != null && line.line.size() > )
- {
- contentList.add(line);
- height += lineHeight + lineSpacing;
- }
- if (contentList.size() <= )
- {
- oneLineWidth = (int) drawedWidth + leftPadding + rightPadding;
- height = lineSpacing + lineHeight + lineSpacing;
- }
- cacheData(width, (int) height);
- return (int) height;
- }
- /**
- * 获取缓存的测量数据,避免多次重复测量
- * @param text
- * @param width
- * @return height
- */
- @SuppressWarnings("unchecked")
- private int getCachedData(String text, int width)
- {
- SoftReference<MeasuredData> cache = measuredData.get(text);
- if (cache == null)
- return -;
- MeasuredData md = cache.get();
- if (md != null && md.textSize == this.getTextSize() && width == md.width)
- {
- lineWidthMax = md.lineWidthMax;
- contentList = (ArrayList<LINE>) md.contentList.clone();
- oneLineWidth = md.oneLineWidth;
- StringBuilder sb = new StringBuilder();
- for (int i = ; i < contentList.size(); i++)
- {
- LINE line = contentList.get(i);
- sb.append(line.toString());
- }
- return md.measuredHeight;
- }
- else
- return -;
- }
- /**
- * 缓存已测量的数据
- * @param width
- * @param height
- */
- @SuppressWarnings("unchecked")
- private void cacheData(int width, int height)
- {
- MeasuredData md = new MeasuredData();
- md.contentList = (ArrayList<LINE>) contentList.clone();
- md.textSize = this.getTextSize();
- md.lineWidthMax = lineWidthMax;
- md.oneLineWidth = oneLineWidth;
- md.measuredHeight = height;
- md.width = width;
- md.hashIndex = ++hashIndex;
- StringBuilder sb = new StringBuilder();
- for (int i = ; i < contentList.size(); i++)
- {
- LINE line = contentList.get(i);
- sb.append(line.toString());
- }
- SoftReference<MeasuredData> cache = new SoftReference<MeasuredData>(md);
- measuredData.put(text.toString(), cache);
- }
- /**
- * 用本函数代替{@link #setText(CharSequence)}
- * @param cs
- */
- public void setMText(CharSequence cs)
- {
- text = cs;
- obList.clear();
- ArrayList<SpanObject> isList = new ArrayList<MTextView.SpanObject>();
- useDefault = false;
- if (cs instanceof SpannableString)
- {
- SpannableString ss = (SpannableString) cs;
- CharacterStyle[] spans = ss.getSpans(, ss.length(), CharacterStyle.class);
- for (int i = ; i < spans.length; i++)
- {
- int s = ss.getSpanStart(spans[i]);
- int e = ss.getSpanEnd(spans[i]);
- SpanObject iS = new SpanObject();
- iS.span = spans[i];
- iS.start = s;
- iS.end = e;
- iS.source = ss.subSequence(s, e);
- isList.add(iS);
- }
- }
- //对span进行排序,以免不同种类的span位置错乱
- SpanObject[] spanArray = new SpanObject[isList.size()];
- isList.toArray(spanArray);
- Arrays.sort(spanArray,,spanArray.length,new SpanObjectComparator());
- isList.clear();
- for(int i=;i<spanArray.length;i++)
- {
- isList.add(spanArray[i]);
- }
- String str = cs.toString();
- for (int i = , j = ; i < cs.length(); )
- {
- if (j < isList.size())
- {
- SpanObject is = isList.get(j);
- if (i < is.start)
- {
- Integer cp = str.codePointAt(i);
- //支持增补字符
- if (Character.isSupplementaryCodePoint(cp))
- {
- i += ;
- }
- else
- {
- i++;
- }
- obList.add(new String(Character.toChars(cp)));
- }
- else if (i >= is.start)
- {
- obList.add(is);
- j++;
- i = is.end;
- }
- }
- else
- {
- Integer cp = str.codePointAt(i);
- if (Character.isSupplementaryCodePoint(cp))
- {
- i += ;
- }
- else
- {
- i++;
- }
- obList.add(new String(Character.toChars(cp)));
- }
- }
- requestLayout();
- }
- public void setUseDefault(boolean useDefault)
- {
- this.useDefault = useDefault;
- if (useDefault)
- {
- this.setText(text);
- this.setTextColor(textColor);
- }
- }
- /**
- * 设置行距
- * @param lineSpacingDP 行距,单位dp
- */
- public void setLineSpacingDP(int lineSpacingDP)
- {
- this.lineSpacingDP = lineSpacingDP;
- lineSpacing = dip2px(context, lineSpacingDP);
- }
- /**
- * 获取行距
- * @return 行距,单位dp
- */
- public int getLineSpacingDP()
- {
- return lineSpacingDP;
- }
- /**
- * @author huangwei
- * @version SocialClient 1.2.0
- * @功能: 存储Span对象及相关信息
- * @2014年5月27日
- * @下午5:21:37
- */
- class SpanObject
- {
- public Object span;
- public int start;
- public int end;
- public CharSequence source;
- }
- /**
- * @功能: 对SpanObject进行排序
- * @author huangwei
- * @2014年6月4日
- * @下午5:21:30
- * @version SocialClient 1.2.0
- */
- class SpanObjectComparator implements Comparator<SpanObject>
- {
- @Override
- public int compare(SpanObject lhs, SpanObject rhs)
- {
- return lhs.start - rhs.start;
- }
- }
- /**
- * @author huangwei
- * @version SocialClient 1.2.0
- * @功能: 存储测量好的一行数据
- * @2014年5月27日
- * @下午5:22:12
- */
- class LINE
- {
- public ArrayList<Object> line = new ArrayList<Object>();
- public ArrayList<Integer> widthList = new ArrayList<Integer>();
- public int height;
- @Override
- public String toString()
- {
- StringBuilder sb = new StringBuilder("height:" + height + " ");
- for (int i = ; i < line.size(); i++)
- {
- sb.append(line.get(i) + ":" + widthList.get(i));
- }
- return sb.toString();
- }
- }
- /**
- * @author huangwei
- * @version SocialClient 1.2.0
- * @功能: 缓存的数据
- * @2014年5月27日
- * @下午5:22:25
- */
- class MeasuredData
- {
- public int measuredHeight;
- public float textSize;
- public int width;
- public float lineWidthMax;
- public int oneLineWidth;
- public int hashIndex;
- ArrayList<LINE> contentList;
- }
为方便在ListView中使用(ListView反复上下滑动会多次重新onMeasure),加了缓存,相同的情况下可以不用重复在测量一次。
对于SpannableString,只支持了ImageSpan,有其它需要者可自行扩展
Demo:http://download.csdn.net/detail/yellowcath/7421147 或:https://github.com/yellowcath/MTextView.git (2014/6/4 更新 添加对BackGroundColorSpan的支持,修复一个会导致最后一行最后一个图形显示不全的bug)
代码:这里
Android 自绘TextView解决提前换行问题,支持图文混排的更多相关文章
- Android TextView中图文混排设置行间距导致高度不一致问题解决
最近项目中需要实现一个评论带表情的功能,刚开始一切顺利,非常easy,突然有一天发现文字跟表情混排的时候,TextView中图文高度不一致,excuse...什么鬼,之前明明测试过图文混排,不存在这个 ...
- Android中Textview显示Html,图文混排,支持图片点击放大
本文首发于网易云社区 对于呈现Html文本来说,Android提供的Webview控件可以得到很好的效果,但使用Webview控件的弊端是效率相对比较低,对于呈现简单的html文本的话,杀鸡不必使用牛 ...
- android:怎样在TextView实现图文混排
我们通常在TextView文本中设置文字.但是怎样设置图文混排呢? 我就在这里写一个样例 .我们须要用到一点简单的HTML知识 在TextView中预订了一些类似HTML的标签,通过标签能够使Text ...
- 使用android SpannableStringBuilder实现图文混排
项目开发中需要实现这种效果 多余两行,两行最后是省略号,省略号后面是下拉更多 之前用过的是Html.fromHtml去处理图文混排的,仅仅是文字后图片或者文字颜色字体什么的, 但是这里需要在最后文字的 ...
- 使用android SpannableStringBuilder实现图文混排,看到许多其他
项目开发需要达到这种效果 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZmFuY3lsb3ZlamF2YQ==/font/5a6L5L2T/fontsiz ...
- TextView + Spanned实现图文混排以及图片点击交互
最近要实现图文混排的需求,webview过大,所以想到了用SpannableStringBuilder来实现. 不过参考了大量国内文章,大多数是教你如何实现图文混排,并没有提及图片点击交互的.有翻阅了 ...
- Android自动解析html带图片,实现图文混排
在android中,如何将html代码转换为text,然后显示在textview中呢,有一个简单直接的方法: textView.setText(Html.fromHtml(content)); 然而用 ...
- 仿小米便签图文混排 EditText解决尾部插入文字bug
一直想实现像小米便签那样的图文混排效果,收集网上的办法无非三种: 1.自定义布局,每张图片是一个ImageView,插入图片后插入EditText,缺点是实现复杂,不能像小米便签那样同时选中图片和文字 ...
- Android图文混排-实现EditText图文混合插入上传
前段时间做了一个Android会议管理系统,项目需求涉及到EditText的图文混排,如图: 在上图的"会议详情"中.须要支持文本和图片的混合插入,下图演示输入的演示样例: 当会议 ...
随机推荐
- nginx上传文件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- VS2010远程调试
1, A:调试机. B:远端被调试机. 2, 从A机的VS2010的安装目录里面,找到../Remote Debugger文件,复制到B机. 3, 启动B机上复制过来的目录下的msvsmon.exe ...
- linux make clean
make clean仅仅是清除之前编译的可执行文件及配置文件. 而make distclean要清除所有生成的文件. Makefile 在符合GNU Makefiel惯例的Makefile中,包含了一 ...
- struts2全注解Action配置
- EasyUI –tree、combotree学习总结
EasyUI –tree.combotree学习总结 一. tree总结 (一).tree基本使用 tree控件是web页面中将数据分层一树形结构显示的. 使用$.fn.tree.defaults ...
- [Interview][CodingExam]
這次去Interview, 其中有一個公司 把我列為 2/25的考慮對象, 在Final 的 1/2, 我被刷掉了. 因為第一輪的程式,我稍微google了一下,參考了既有的寫法. 即使第二輪我用完全 ...
- 做了codility网站上一题:CountBoundedSlices
在写上一随笔之前,在Codility网站上还做了一个道题(非Demo题):CountBoundedSlices,得了60分(害臊呀).今天又重新做了一下这个算法,性能提高了不少,但由于此题不是Demo ...
- java 中的this关键字的几种用法
转自:http://blog.csdn.net/anmei2010/article/details/4091227 1. 当成员变量和局部变量重名时,在方法中使用this时,表示的是该方法所在 ...
- ems lite 客户端远程连接mysql server
在本地用ems客户端远程连接虚拟机上的mysql server,弹出客户端没有权限访问mysql server.使用下面方法进行设置:mysql> select host,user,passwo ...
- P1654: [Usaco2006 Jan]The Cow Prom 奶牛舞会
裸的强连通 ; type node=record f,t:longint; end; var n,m,dgr,i,u,v,num,ans:longint; bfsdgr,low,head,f:arra ...