本来上周六晚上出去散步的时候就随便想了下,当时的想法是ViewGroup要实现内部控件的滚动,1. 最终效果肯定就是子控件的重绘对吧? 2. 重绘肯定就涉及到onLayout重新定位的处理对吧? 重新定位+重新绘制理论上就是实现滚动的原理了吧。

基于上述猜测,小白以为我们只要在onLayout中重新刷新控件的位置不就可以实现滚动了么?没错,小白实践了,可以滴?--需要了解如下知识:

Invalidate:

To farce a view to draw,call invalidate().——摘自View类源码

从上面这句话看出,invalidate方法会执行draw过程,重绘View树。

当View的appearance发生改变,比如状态改变(enable,focus),背景改变,隐显改变等,这些都属于appearance范畴,都会引起invalidate操作。

所以当我们改变了View的appearance,需要更新界面显示,就可以直接调用invalidate方法。

View(非容器类)调用invalidate方法只会重绘自身,ViewGroup调用则会重绘整个View树。

RequestLayout:

To initiate a layout, call requestLayout(). This method is typically called by a view on itself when it believes that it can no longer fit within its current bounds.——摘自View源码

从上面这句话看出,当View的边界,也可以理解为View的宽高,发生了变化,不再适合现在的区域,可以调用requestLayout方法重新对View布局。

View执行requestLayout方法,会向上递归到顶级父View中,再执行这个顶级父View的requestLayout,所以其他View的onMeasure,onLayout也可能会被调用。

requestLayout()方法就是我们请求重新测量和定位的方式。

1.比如我们实现一个简单的效果有问题的上下滑动的ViewGroup

1.1 首先获取上下滑动事件处理

1.2 获得上下滑动分别的距离

1.3 滑动的距离分别附加到onlayout的leftTop上,也就是原来控件的摆放的初始位置全部加上这个偏移量(该偏移量如果是向上滑动为负数, 向下自然就是整数;该便宜是累计的,在onTouchEvent中做简单计算哟)

So, 看简单的偏移量的计算....应该不难吧...

private float x1, x2;

private float y1, y2;

private float swing = 0;

private float totalswings = 0;

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()){

case MotionEvent.ACTION_DOWN:

x1 = event.getX();

y1 = event.getY();

break;

case MotionEvent.ACTION_MOVE:

x2 = event.getX();

y2 = event.getY();

///< 5作为阀值就可以了,可以根据效果调整

if(y1 - y2 > 5) { ///< 上

swing = y1 - y2;

totalswings += swing;

requestLayout();

Log.e("test", "上滑动");

} else if(y2 - y1 > 5) { ///< 下

swing = -(y2 - y1);

totalswings += swing;

requestLayout();

Log.e("test", "下滑动");

} else if(x1 - x2 > 50) { ///< 左

} else if(x2 - x1 > 50) { ///< 右

}

x1 = x2;

y1 = y2;

break;

case MotionEvent.ACTION_UP:

break;

}

return true;

}

Then, 拿到偏移累计后totalswings,就可以在onLayout中进行偏移添加:

效果就是这么粗糙啦(上下错了这些不要在意,关键是我们知道这样可以做,具体效果估计要搞一些好的算法处理才行)..

2. 或许我们可以像上面那样做,但是感觉有点麻烦的样子...再查看一些个资料,发现其实大部分码友的做法是调用系统的scroll相关的方法 - 看官方

scrollBy

added in API level 1

public void scrollBy (int x,

int y)

Move the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

Parameters

xint: the amount of pixels to scroll by horizontally

yint: the amount of pixels to scroll by vertically

scrollTo

added in API level 1

public void scrollTo (int x,

int y)

Set the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

Parameters

xint: the x position to scroll to

yint: the y position to scroll to

scrollBy、scrollTo,两个来自View的方法。ViewGroup继承自View,自然也可以直接调用该方法。

scrollBy - 滚动的距离(用这个来实现滚动的效果即可,每次onTouch我们都计算了滑动偏移量)

scrollTo - 滚动到哪里

So, 我们改造下onTouchEvent方法:

private float x1, x2;

private float y1, y2;

private float swing = 0;

private float totalswings = 0;

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()){

case MotionEvent.ACTION_DOWN:

x1 = event.getX();

y1 = event.getY();

break;

case MotionEvent.ACTION_MOVE:

x2 = event.getX();

y2 = event.getY();

///< 5作为阀值就可以了,可以根据效果调整

if(y1 - y2 > 5) { ///< 上

swing = y1 - y2;

///< 采用刷新onLahyout实现滚动

//totalswings += swing;

//requestLayout();

///< 采用系统View类的滚动方法

scrollBy(0, (int) swing);

Log.e("test", "上滑动");

} else if(y2 - y1 > 5) { ///< 下

swing = -(y2 - y1);

///< 采用刷新onLahyout实现滚动

//totalswings += swing;

//requestLayout();

///< 采用系统View类的滚动方法

scrollBy(0, (int) swing);

Log.e("test", "下滑动");

} else if(x1 - x2 > 50) { ///< 左

} else if(x2 - x1 > 50) { ///< 右

}

x1 = x2;

y1 = y2;

break;

case MotionEvent.ACTION_UP:

break;

}

return true;

}

效果一般般啦:

2.1 稍微看下scrollBy源码吧....

public void scrollBy(int x, int y) {

scrollTo(mScrollX + x, mScrollY + y);

}

最后调用的其实是scrollTo

public void scrollTo(int x, int y) {

if (mScrollX != x || mScrollY != y) {

int oldX = mScrollX;

int oldY = mScrollY;

mScrollX = x;

mScrollY = y;

invalidateParentCaches();

onScrollChanged(mScrollX, mScrollY, oldX, oldY);

if (!awakenScrollBars()) {

postInvalidateOnAnimation();

}

}

}

关注下下面这两个变量哈....

再往每个具体的函数跟,其实还是复杂。我们换个思路,既然最终是靠定位和重绘实现的滚动,那么我们去看看TextView的onDraw:

小白框起来的些个变量,有没有想到一些联系(小白大体跟了下:当调用scrollBy时,会更新对应的mScrollX/Y,然后请求相关控件的重定位,重绘,进而达到滚动效果)。。。其实子控件再考虑绘制的时候已经考虑到了父控件相关的滚动设计。 当然要彻底搞明白整个流程,肯定不是一天两天的事情(对小白来说).

不过对小白来讲,能学到了解到上面些的知识,还是蛮不错的。原理还是有点领悟的....

3.当然如果你搜索网上相关的资料,会发现有些码友用的是Scroller - 所谓的弹性辅助滑动类Scroller | Android Developers

Scroller就是一个滑动帮助类。它并不可以使View真正的滑动,

而是配合scrollTo/ScrollBy让view产生缓慢的滑动,产生动画的效果,其实和属性动画是同一个原理。

在我看来,Scroller跟属性动画的平移的效果是一样的。

我们可以试试(具体的用法和效果可以专门学习下,然后用到我们的自定义ViewGroup中).

使用方式是这样的:

To track the changing positions of the x/y coordinates,

use computeScrollOffset().

The method returns a boolean to indicate whether the scroller is finished.

If it isn't, it means that a fling or programmatic pan operation is still in progress.

You can use this method to find the current offsets of the x and y coordinates, for example:

if (mScroller.computeScrollOffset()) {

// Get current x and y positions

int currX = mScroller.getCurrX();

int currY = mScroller.getCurrY();

...

}

3.1 调用mScroller.startScroll(0, 0, 100, 0);

// Invalidate to request a redraw

invalidate();

3.2. 根据上面解释,此时就会触发computeScroll()方法的回调:

然后就可以进行类似如下的处理scrollTo、scrollBy看你需要:

@Override

public void computeScroll() {

if (mScroller.computeScrollOffset()) {

scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

invalidate();

}

}

比如我们这样的处理,当然效果肯定不对,不过先不管了哟!

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()){

case MotionEvent.ACTION_DOWN:

x1 = event.getX();

y1 = event.getY();

break;

case MotionEvent.ACTION_MOVE:

x2 = event.getX();

y2 = event.getY();

///< 5作为阀值就可以了,可以根据效果调整

if(y1 - y2 > 5) { ///< 上

swing = y1 - y2;

///< 采用刷新onLahyout实现滚动

//totalswings += swing;

//requestLayout();

///< 采用系统View类的滚动方法

//scrollBy(0, (int) swing);

mScroller.startScroll(0, 0, 0, (int) swing);

invalidate();

Log.e("test", "上滑动");

} else if(y2 - y1 > 5) { ///< 下

swing = -(y2 - y1);

///< 采用刷新onLahyout实现滚动

//totalswings += swing;

//requestLayout();

///< 采用系统View类的滚动方法

//scrollBy(0, (int) swing);

mScroller.startScroll(0, 0, 0, (int) swing);

invalidate();

Log.e("test", "下滑动");

} else if(x1 - x2 > 50) { ///< 左

} else if(x2 - x1 > 50) { ///< 右

}

x1 = x2;

y1 = y2;

break;

case MotionEvent.ACTION_UP:

break;

}

return true;

}

@Override

public void computeScroll() {

if (mScroller.computeScrollOffset()) {

scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

invalidate();

}

}

效果:基本滑不动,哈哈~~~先飘过,不管了....

综上我们就大概对这个滚动有了总体的认识、以及对scroll和Scroller的综合理解。作为知识进阶,总得了解更多,虽然远远不够!!

有个疑问需要提下?后面具体研究估计才懂,有懂的可以指教下 - 就是为什么滚动了,但是onLayout并没有进行回调,那怎么实现的这个子控件的重定位,难道仅仅是重绘?

最后看下整体源码吧:CustomViewGroupLastNew.java

package me.heyclock.hl.customcopy;

import android.content.Context;

import android.content.res.TypedArray;

import android.graphics.Canvas;

import android.graphics.Rect;

import android.util.AttributeSet;

import android.util.Log;

import android.view.Gravity;

import android.view.MotionEvent;

import android.view.View;

import android.view.ViewGroup;

import android.widget.Scroller;

/*

*@Description: 自定义ViewGroup + 纵向垂直布局 + 单列 + 粗糙上下滑动

*@Author: hl

*@Time: 2018/10/25 10:18

*/

public class CustomViewGroupLastNew extends ViewGroup {

private Context context;///< 上下文

/**

* 计算子控件的布局位置.

*/

private final Rect mTmpContainerRect = new Rect();

private final Rect mTmpChildRect = new Rect();

/**

* 滚动相关(上下滑动)

*/

private float x1, x2;

private float y1, y2;

private float swing = 0;

private float totalswings = 0;

private Scroller mScroller;

public CustomViewGroupLastNew(Context context) {

this(context, null);

}

public CustomViewGroupLastNew(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr) {

this(context, attrs, 0, 0);

}

public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {

super(context, attrs, defStyleAttr, defStyleRes);

this.context = context;

mScroller = new Scroller(context);

}

/**

* 测量容器的宽高 = 所有子控件的尺寸 + 容器本身的尺寸 -->综合考虑

*

* @param widthMeasureSpec

* @param heightMeasureSpec

*/

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

///< 定义最大宽度和高度

int maxWidth = 0;

int maxHeight = 0;

///< 获取子控件的个数

int count = getChildCount();

for (int i = 0; i < count; ++i) {

View view = getChildAt(i);

///< 子控件如果是GONE - 不可见也不占据任何位置则不进行测量

if (view.getVisibility() != GONE) {

///< 获取子控件的属性 - margin、padding

CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();

///< 调用子控件测量的方法getChildMeasureSpec(先不考虑margin、padding)

///< - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考

final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, layoutParams.leftMargin + layoutParams.rightMargin, layoutParams.width);

final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, layoutParams.topMargin + layoutParams.bottomMargin, layoutParams.height);

///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯

view.measure(childWidthMeasureSpec, childHeightMeasureSpec);

///< 然后再次获取测量后的子控件的属性

layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();

///< 然后获取宽度的最大值、高度的累加

maxWidth = Math.max(maxWidth, view.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin);

maxHeight += view.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;

}

}

///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸

maxWidth = Math.max(maxWidth, getMinimumWidth());

maxHeight = Math.max(maxHeight, getMinimumHeight());

///< 然后根据容器的模式进行对应的宽高设置 - 参考我们之前的自定义View的测试方式

int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);

int wSize = MeasureSpec.getSize(widthMeasureSpec);

int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);

int hSize = MeasureSpec.getSize(heightMeasureSpec);

///< wrap_content的模式

if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {

setMeasuredDimension(

maxWidth + getPaddingLeft() + getPaddingRight(),

maxHeight + getPaddingTop() + getPaddingBottom());

}

///< 精确尺寸的模式

else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {

setMeasuredDimension(wSize, hSize);

}

///< 宽度尺寸不确定,高度确定

else if (wSpecMode == MeasureSpec.UNSPECIFIED) {

setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), hSize);

}

///< 宽度确定,高度不确定

else if (hSpecMode == MeasureSpec.UNSPECIFIED) {

setMeasuredDimension(wSize, maxHeight + getPaddingTop() + getPaddingBottom());

}

}

@Override

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

Log.e("test", "onLayout(" + left + "," + top + "," + right + "," + bottom + ")");

///< 获取范围初始左上角 - 这个决定子控件绘制的位置,我们绘制理论可以从0,0开始,margin容器本身已经考虑过了...所以别和margin混淆了

int leftPos = getPaddingLeft();

int leftTop = getPaddingTop() + (int)totalswings;

///< 获取范围初始右下角 - 如果考虑控件的位置,比如靠右,靠下等可能就要利用右下角范围来进行范围计算了...

///< 后面我们逐步完善控件的时候用会用到这里...

//int rightPos = right - left - getPaddingRight();

//int rightBottom = bottom - top - getPaddingBottom();

///< 由于我们是垂直布局,并且一律左上角开始绘制的情况下,我们只需要计算出leftPos, leftTop就可以了

int count = getChildCount();

for (int i = 0; i < count; ++i){

View childView = getChildAt(i);

///< 控件占位的情况下进行计算

if (childView.getVisibility() != GONE){

///< 获取子控件的属性 - margin、padding

CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) childView.getLayoutParams();

int childW = childView.getMeasuredWidth();

int childH = childView.getMeasuredHeight();

///< 先不管控件的margin哈!

int cleft = leftPos + layoutParams.leftMargin;

int cright = cleft + childW;

int ctop = leftTop + layoutParams.topMargin;

int cbottom = ctop + childH;

///< 下一个控件的左上角需要向y轴移动上一个控件的高度 - 不然都重叠了!

leftTop += childH + layoutParams.topMargin + layoutParams.bottomMargin;

///< 需要一个范围,然后进行摆放

childView.layout(cleft, ctop, cright, cbottom);

}

}

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

int count = getChildCount();

for (int i = 0; i < count; ++i) {

///< 获取子控件的宽高

View view = getChildAt(i);

Log.e("test", "getPaddingLeft()=" + view.getPaddingLeft());

Log.e("test", "getPaddingRight()=" + view.getPaddingRight());

Log.e("test", "getPaddingTop()=" + view.getPaddingTop());

Log.e("test", "getPaddingBottom()=" + view.getPaddingBottom());

}

}

@Override

public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()){

case MotionEvent.ACTION_DOWN:

x1 = event.getX();

y1 = event.getY();

break;

case MotionEvent.ACTION_MOVE:

x2 = event.getX();

y2 = event.getY();

///< 5作为阀值就可以了,可以根据效果调整

if(y1 - y2 > 5) { ///< 上

swing = y1 - y2;

///< 采用刷新onLahyout实现滚动

//totalswings += swing;

//requestLayout();

///< 采用系统View类的滚动方法

scrollBy(0, (int) swing);

///< 采用Scroller滚动辅助类配合computeScroll实现上下滚动

//mScroller.startScroll(0, 0, 0, (int) swing);

invalidate();

Log.e("test", "上滑动");

} else if(y2 - y1 > 5) { ///< 下

swing = -(y2 - y1);

///< 采用刷新onLahyout实现滚动

//totalswings += swing;

//requestLayout();

///< 采用系统View类的滚动方法

scrollBy(0, (int) swing);

///< 采用Scroller滚动辅助类配合computeScroll实现上下滚动

//mScroller.startScroll(0, 0, 0, (int) swing);

invalidate();

Log.e("test", "下滑动");

} else if(x1 - x2 > 50) { ///< 左

} else if(x2 - x1 > 50) { ///< 右

}

x1 = x2;

y1 = y2;

break;

case MotionEvent.ACTION_UP:

break;

}

return true;

}

@Override

public void computeScroll() {

if (mScroller.computeScrollOffset()) {

scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

invalidate();

}

}

@Override

public CustomViewGroupLastNew.LayoutParams generateLayoutParams(AttributeSet attrs) {

return new CustomViewGroupLastNew.LayoutParams(getContext(), attrs);

}

/**

* 这个是布局相关的属性,最终继承的是ViewGroup.LayoutParams,所以上面我们可以直接进行转换

* --目的是获取自定义属性以及一些使用常量的自定义

*/

public static class LayoutParams extends MarginLayoutParams {

public LayoutParams(Context c, AttributeSet attrs) {

super(c, attrs);

// Pull the layout param values from the layout XML during

// inflation. This is not needed if you don't care about

// changing the layout behavior in XML.

TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);

///< TODO 一些属性的自定义

a.recycle();

}

public LayoutParams(int width, int height) {

super(width, height);

}

public LayoutParams(ViewGroup.LayoutParams source) {

super(source);

}

}

}

Next,我看下点击事件以及后续的滑动冲突的处理....快了,快把自定义流程简单过一遍了,加油,么么哒!

android 滑动取值_Android-自定义ViewGroup-上下滑动整体实践下相关推荐

  1. android 滑动取值_Android中滑屏实现

    前言:  虽然本文标题的有点标题党的感觉,但无论如何,通过这篇文章的学习以及你自己的实践认知,写个简单的滑屏小 Demo还是justso so的. 友情提示: 在继续往下面读之前,希望您对以下知识点有 ...

  2. android 滑动取值_Android View篇之调整字体大小滑杆的实现

    小伙伴们大家好呀,这次介绍一个稍微有点意思的View,在很多阅读类.新闻类的APP上都标配的字体大小调整功能.100多行代码就可以实现,来看看效果吧! 思路分析 1.刻度线代表着每个字体的大小取值,是 ...

  3. android绘制心形_Android自定义View系列(一)——打造一个爱心进度条

    写作原因:Android进阶过程中有一个绕不开的话题--自定义View.这一块是安卓程序员更好地实现功能自主化必须迈出的一步.下面这个系列博主将通过实现几个例子来认识安卓自定义View的方法.从自定义 ...

  4. Android UI设计之十一自定义ViewGroup,打造通用的关闭键盘小控件ImeObser

    2019独角兽企业重金招聘Python工程师标准>>> 转载请注明出处:http://blog.csdn.net/llew2011/article/details/51598682 ...

  5. android指定sqlite路径_Android:自定义Sqlite数据库路径

    默认的sqlite数据库是放在/data/data/database目录下的,今天看腾讯云IM的demo发现再该路径下找不到它存放消息的数据库,找了下后发现居然是放在/data/data/files目 ...

  6. Android开源中国客户端学习 (自定义View)左右滑动控件ScrollLayout

    左右滑动的控件我们使用的也是非常多了,但是基本上都是使用的viewpager 等 android基础的控件,那么我们有么有考虑过查看他的源码进行定制呢?当然,如果你自我感觉非常好的话可以自己定制一个, ...

  7. android canvas绘制圆角_Android自定义View撸一个渐变的温度指示器(TmepView)

    秦子帅明确目标,每天进步一点点..... 作者 |  andy 地址 |  blog.csdn.net/Andy_l1/article/details/82910061 1.概述 自定义View对需要 ...

  8. android录音波浪动画_Android 自定义 view 实现波浪动画进度条

    最近在做项目时需要实现这样一种动画,类似于波浪形的进度动画,粗略的看了一下,发现好像类似于正余弦曲线实现的,但是Android 没有相关的API,所以需要我们动手画出来,所以现在在此记录一下学习过程, ...

  9. android 图片圆角 遮罩_Android 自定义View练手Demo(一)实现圆角遮罩效果

    Android 自定义View系列文章 Android自定义View实现圆角遮罩效果 一图胜千言,有一个遮罩就会凸显出重点区域 1-1.jpg 本文通过两种方式来实现这种效果,来达到自定义View练手 ...

最新文章

  1. 【计算机网络】网络安全 : 实体鉴别 ( 实体鉴别过程 | 不重数机制 | 公钥体质加密不重数 | 中间人攻击 )
  2. Android开发 asmack断线收不到通知的BUG解决
  3. Android 6.0 超级简单的权限申请2 (Permission)
  4. vue 父组件与子组件之间的传值(主动传值)
  5. java学习(96):线程的睡眠
  6. 用了 HTTPS,没想到还是被监控了!
  7. ai端到端_如何使用行为树构建端到端的对话式AI系统
  8. spark学习-58-Spark的EventLoggingListener
  9. three相机在模型上_一步步带你实现web全景看房——three.js
  10. 解决SecureCRT连接linux超时后断开
  11. c语言程序设计自考真题,自学考试《C语言程序设计》随堂试题及答案
  12. 51单片机 wifi模块代码编写的历程 esp8266
  13. el-table 样式设置
  14. 小程序优购商城项目总结
  15. 锁仓怎么解_锁仓和解锁的方法
  16. Excel的筛选功能应用教你在大数据中筛选出需要的数据
  17. win7磁盘管理分区,改变页面文件卷,删除卷就由灰变黑了!
  18. gpd计算机等级,GPD 文件扩展名: 它是什么以及如何打开它?
  19. 指针实现入栈、出栈、取栈顶元素
  20. [499]openstack swift 的UI客户端

热门文章

  1. asp.net微软图表控件MsChart
  2. 【3】数据筛选3 - BeautifulSoup4
  3. 【洛谷 P1070】道路游戏 (DP)
  4. Java基础——Ajax(一)
  5. 数组的去重-----------------------来自大牛的讲解
  6. 编写一个Java项目,定义包,在包下定义包含main方法的类,在main方法中声明8种基本数据类型的变量并赋值,练习数据类型转换。...
  7. ecshop各项功能介绍参考
  8. [CentOS]CentOS下编译CPP文件时报错[undefined reference to `__gxx_personality_v0' collect2: ld]的解决办法...
  9. UML建模工具Visio、Rational Rose、PowerDesign,Visual Paradigm for UML
  10. c语言算法基础第一例