接下来,我们将完成QQ聊天界面的ListView滑动效果,大家可能都用过ListView,知道ListView是上下滑动的,并不会产生左右滑动的效果,如果想让ListView变成左右滑动的效果,必须对安卓源代码有所了解,如果你想了解源代码,请到http://blog.csdn.net/column/details/core-services.html 该专栏下了解详情。

我的思路就是:

所有的屏幕操作事件由ListView作做拦截,同时把事件传递给SlideView做滑动,这种实现的确可以达到效果,而且代码很简单,根本不需要处理什么复杂的滑动冲突。

QQ效果图与自己实现的效果图对比:

                

1.思路流程

首先我们需要实现自己的ListView来处理截获屏幕的事件,但不是由ListView处理,而是转发给自定义item控件处理,也就是实现的SlideView控件。根据处理手势设置item的状态,也就是说当已经滑动了,这个时候如果不获取item的状态,下次在滑动这个item的时候是不知道这个控件已经滑动了,不然就会有二次滑动,所以必须保存滑动状态。

下面用箭头详细说明今天代码流程:

当手指滑动某个item的时候——>ListView截获滑动事件——>在自定义ListView中实现onTouchEvent()方法,判断当前是哪个item——>然后将事件派发给item自己的SlideView去处理滑动——>处理完成后设置当前item的状态(防止二次滑动,也防止其他item滑动后,该item没有恢复到原来的模样)

2.定义Item容器

代码如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/view_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

    </LinearLayout>

    <LinearLayout
        android:id="@+id/holder"
        android:layout_width="120dp"
        android:layout_height="match_parent"
        android:background="#C5C1AA"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/top"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:gravity="center"
            android:background="#FFD700"
            android:text="@string/slide_holder_top"
            android:textSize="20sp"
            android:layout_weight="1"/>

        <TextView
            android:id="@+id/delete"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:gravity="center"
            android:text="@string/slide_holder_delete"
            android:textSize="20sp"
            android:layout_weight="1"/>

    </LinearLayout>
</merge>

这里需要说明的就是,merge节点在加载的时候会忽略掉视图的层级,也就是说加载是时候会自动忽略掉merge节点,直接加载内部的内容,这里就是两个LinearLayout了。

这里为什么要用这个,因为等会的SlideView会继承LinearLayout,如果用LinearLayout包裹这两个LinearLayout,就会多出一个本身没用LinearLayout,除了增加视力层级,没有任何效果。

第一个LinearLayout宽高都设置为match_parent,目的就是将后一个LinearLayout挤出屏幕之外。好达到有滑动出控件的效果。

3.实现SlideView

我们SlideView会继承自LinearLayout,当然你也可以继承其他的布局,只是LinearLayout考虑的相对简单点。

㈠定义item状态的接口

public interface OnSlideViewOnListener {//滑动的三个状态
    public static final int SLIDE_STATUS_OFF = 0;
    public static final int SLIDE_STATUS_START_SCROLL = 1;
    public static final int SLIDE_STATUS_ON = 2;

    /**
     * 更新滑动的状态
     *
     * @param view
     * @param status
     */
    public void Slide(View view, int status);
}

这个就不用多作解释了,一目了然。

㈡初始化SlideView

public void initView() {//获取上下文
    this.mContext = getContext();
    //初始化滑动对象
    this.mScroller = new Scroller(this.mContext);
    //设置该LinearLayout为横向坐标
    setOrientation(LinearLayout.HORIZONTAL);
    //加载布局
    View.inflate(this.mContext,R.layout.slide_holder,this);
    //获取容器
    this.mLinearLayout = (LinearLayout) findViewById(R.id.view_content);
    //将隐藏容器的宽度根据屏幕调节
    this.mHolderWidth = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.mHolderWidth, getResources().getDisplayMetrics()));
}

㈢处理ListView发送过来的事件

public void onRequiredToEvent(MotionEvent event) {//获取当前X,Y坐标
    int x = (int) event.getX();
    int y = (int) event.getY();
    Log.d(TAG, "x=" + x + " ,y=" + y);
    //getScrollX()获取的值则发生了变化:指调用的控件的水平移动的距离,当未移动的时候,获取的值为0. 当向右移动20,则获取值为 -20,再向右移动10,则获取-30
    int scrollX = getScrollX();
    //根据按键的方式处理相应的事件
    switch (event.getAction()) {//按下时候处理
        case MotionEvent.ACTION_DOWN: {if (!this.mScroller.isFinished()) {this.mScroller.abortAnimation();
            }if (this.mOnSlideViewOnListener != null) {this.mOnSlideViewOnListener.Slide(this, OnSlideViewOnListener.SLIDE_STATUS_START_SCROLL);
            }break;
        }//移动时候处理
        case MotionEvent.ACTION_MOVE: {//x,y各滑动了多少距离
            int deltaX = x - this.mLastX;
            int deltaY = y - this.mLastY;
            Log.d(TAG, "deltaX=" + deltaX + " ,deltaY=" + deltaY);
            if (Math.abs(deltaX) < Math.abs(deltaY) * 2) {//角度大于60度,不符合滑动的条件
                break;
            }int newScrollX = scrollX - deltaX;
            if (deltaX != 0) {if (newScrollX < 0) {newScrollX = 0;
                } else if (newScrollX > this.mHolderWidth) {newScrollX = this.mHolderWidth;
                }this.scrollTo(newScrollX, 0);
            }break;
        }//抬起时候处理
        case MotionEvent.ACTION_UP: {int newScrollX = 0;
            if (scrollX - this.mHolderWidth * 0.75 > 0) {newScrollX = this.mHolderWidth;
            }this.smoothScrollTo(newScrollX, 0);
            if (this.mOnSlideViewOnListener != null) {this.mOnSlideViewOnListener.Slide(this, newScrollX == 0 ? OnSlideViewOnListener.SLIDE_STATUS_OFF : OnSlideViewOnListener.SLIDE_STATUS_ON);
            }break;
        }default:break;
    }//将历史坐标更新
    this.mLastX = x;
    this.mLastY = y;
}

在开始的时候定义了一个Scroller对象,该对象是弹性滑动对象,滑动效果由他实现。

Math.abs(deltaX) < Math.abs(deltaY) * 2的解释:

deltaX为滑动的坐标变量,X向左坐标越小,X向右坐标越大,Y向下坐标越大,Y向上坐标越小,所以有可能相对于上一个坐标变量相减会是负值,所以用Math.abs()取他们的绝对值,因为我们只要保证横向滑动的时候,角度在60度以内就算是横向滑动,大于60度,那就算纵向滑动所以不处理滑动。而tan(63)约等于2,故对边除邻边要<2,所以把变更Y移过去就得到这个值。

当然图像更容易理解,如下所示:

大的直线箭头为X,Y轴,120度的箭头为滑动方向,我们在该图中为左,在矩形ListView的Item这个120度角度内都判断为向左滑动,如果手指超过这个角度,就不是横向滑动了。

getScrollX()指调用的控件的水平移动的距离,当未移动的时候,获取的值为0. 当向右移动20,则获取值为 -20,再向右移动10,则获取-30

this.mScroller.isFinished():

返回scroller是否已完成滚动。

返回值:停止滚动返回true,否则返回false

this.mScroller.abortAnimation():停止动画。Scroller滚动到最终x与y位置时中止动画。

这两句应该这样理解,当我按下某个item的时候,如果我在滑动这个item,抬起手后,他的动画还没有结束,这个时候,我又按下了刚才的item,就必须让他停止动画。又因为正在滑动,所以必须设置这个item的状态为滑动。

newScrollX = scrollX - deltaX;的目的是为了防止滑动越界,因为我可能手指一直向某个方向滑动,如果滑动超过了120dip,那么如果不处理越界,就会有多的空白部分,会导致滑动无止境,有可能滑动的屏幕上没有这个item了。所以当超过120dip的时候,就停止滑动if:newScrollX,else:newScrollX = this.mHolderWidth;当小于0后,也停止滑动:if:newScrollX < 0,else:newScrollX = 0;然后自执行滑动this.scrollTo(newScrollX, 0);

this.scrollTo(newScrollX, 0);是直接从某个坐标在X轴滑动newScrollX的距离,Y轴滑动0,X右为负,左为正,Y下为负,上为正。与坐标系相反。

最后解释一下,当手指抬起后,满足scrollX - this.mHolderWidth * 0.75 > 0,则滑动出隐藏控件。然后滑动。

滑动代码如下:

public void smoothScrollTo(int destX, int destY) {int scrollX = getScrollX();
    int delta = destX - scrollX;
    this.mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 3);
    invalidate();//重绘
}

因为为横向滑动destY是没有意义的,这里这样是可能很容易扩展代码,首先获得手指滑动的距离,然后获得最终item需要滑动的距离delta,然后执行滑动。

public void startScroll (int startX, int startY, int dx, int dy) :以提供的起始点和将要滑动的距离开始滚动。滚动会使用缺省值250ms作为持续时间。

当调用startScroll方法后,scroller并不是直接包办一切,它只是帮助你计算每次移动的偏移量,你需要重写computeScroll方法,并在里面处理移动。代码如下:

//调用startScroll()是不会有滚动效果的,只有在computeScroll()获取滚动情况,做出滚动的响应
@Override
public void computeScroll() {if(this.mScroller.computeScrollOffset()){scrollTo(this.mScroller.getCurrX(),this.mScroller.getCurrY());
        postInvalidate();
    }super.computeScroll();
}

如果滑动出隐藏控件设置状态为ON,否则为OFF。

玩QQ的也应该知道,所有的item只有一个,也只能有一个滑动出现,如果要滑动出另一个控件必须将上个控件关闭。所以还需要将控件关闭的方法,代码如下 :

/**
 * 将当前状态置为关闭
 */
public void shrink(){if(getScrollX()!=0){this.smoothScrollTo(0,0);
    }
}

当滑动另一个控件的时候就调用上一个item控件的shrink()方法。

当然还需要将View加载进来,代码如下:

/**
 * 将View加载进来
 * @param view
 */
public void setContentView(View view){this.mLinearLayout.addView(view);
}

当然还要有设置回调函数的方法:

/**
 * 设置回调函数
 * @param onSlideViewOnListener
 */
public void setmOnSlideViewOnListener(OnSlideViewOnListener onSlideViewOnListener){this.mOnSlideViewOnListener=onSlideViewOnListener;
}

这样SlideView就基本实现了。

4.实现自定义的ListView

如果用系统自己的ListView,将不会将滑动事件发送到SlideView,故需要实现自己的ListView,然而我们并不 是直接继承ListView,而是ListViewCompat,该类也继承自ListView,不过有些扩展的方法可以使用,更加全能。

代码如下:

public class MyListView extends ListViewCompat {private SlideView chooseSlideView;

    public MyListView(Context context, AttributeSet attrs) {super(context, attrs);
    }@Override
    public boolean onTouchEvent(MotionEvent event) {switch(event.getAction()){case MotionEvent.ACTION_DOWN:{int x=(int)event.getX();
                int y=(int)event.getY();
                int position=pointToPosition(x,y);
                if(position!= ListView.INVALID_POSITION){MessageItem item=(MessageItem)getItemAtPosition(position);
                    this.chooseSlideView=item.mSlideView;
                }break;
            }default:break;
        }//将操作提交给SlideView处理
        if(this.chooseSlideView!=null){this.chooseSlideView.onRequiredToEvent(event);
        }return super.onTouchEvent(event);
    }
}

当接收到触摸事件后,根据X,Y坐标,获得item的ID,提供给我们的方法为pointToPosition(x,y),一看就知道该方法的:point点到Position。每个ListView都有一个无效的位置,比如第一行的前一行,最后一行的后一行,需要作一个判断:position!= ListView.INVALID_POSITION。如果是有效的位置,就获得该item的信息,并设置chooseSlideView。然后将触摸事件提交给item的包装View处理(也就是SlideView)。

MessageItem代码如下:

public class MessageItem {public int iconRes;
    public String title;
    public String msg;
    public String time;
    public SlideView mSlideView;
}

5.最后实现Activity代码

启动的Activity需要实现两个接口,一个是SlideView.OnSlideViewOnListener,一个是View.OnClickListener点击事件。

对于ListView需要一个适配器adapter,代码如下:

private class SlideAdapter extends BaseAdapter {private LayoutInflater mInflater;

    SlideAdapter() {super();
        this.mInflater = getLayoutInflater();
    }@Override
    public int getCount() {return mMessageItem.size();
    }@Override
    public Object getItem(int position) {return mMessageItem.get(position);
    }@Override
    public long getItemId(int position) {return position;
    }@Override
    public View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder;
        SlideView slideView = (SlideView) convertView;
        if (slideView == null) {View itemView = this.mInflater.inflate(R.layout.list_item, null);
            slideView = new SlideView(MainActivity.this);
            slideView.setContentView(itemView);
            holder = new ViewHolder(slideView);
            slideView.setmOnSlideViewOnListener(MainActivity.this);
            slideView.setTag(holder);
        } else {holder = (ViewHolder) slideView.getTag();
        }MessageItem item = mMessageItem.get(position);
        item.mSlideView = slideView;
        item.mSlideView.shrink();

        holder.icon.setImageResource(item.iconRes);
        holder.title.setText(item.title);
        holder.msg.setText(item.msg);
        holder.time.setText(item.time);
        holder.delete.setOnClickListener(MainActivity.this);
        holder.top.setOnClickListener(MainActivity.this);
        return slideView;
    }
}

定义LayoutInflater都是用来加载布局文件的,不管是Activity里的setContentView()还是刚才的SlideView的Viiew.inflate其内部实现都是这样做的。

其中list_item代码如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="50dp"
        android:layout_height="50dp"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/icon"
        android:layout_toRightOf="@+id/icon"
        android:orientation="vertical">

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <TextView
        android:id="@+id/time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"/>
</RelativeLayout>

这里定义了一个ViewHolder,为的就是防止二次加载。其代码如下:

public class ViewHolder {public ImageView icon;
    public TextView title;
    public TextView msg;
    public TextView time;
    public TextView delete;
    public TextView top;

    ViewHolder(View view) {this.icon = (ImageView) view.findViewById(R.id.icon);
        this.title = (TextView) view.findViewById(R.id.title);
        this.msg = (TextView) view.findViewById(R.id.msg);
        this.time = (TextView) view.findViewById(R.id.time);
        this.delete = (TextView) view.findViewById(R.id.delete);
        this.top = (TextView) view.findViewById(R.id.top);
    }
}

每次获得MessageItem都调用了item.mSlideView.shrink();将隐藏控件隐藏。其他的代码不用多作解释,ListView基本都是这么用的。

下面我们来看一个接口SlideView.OnSlideViewOnListener实现,代码如下:

@Override
public void Slide(View view, int status) {if (this.mLastSlideViewWithStatusOn != null && this.mLastSlideViewWithStatusOn != view) {this.mLastSlideViewWithStatusOn.shrink();
    }if (status == SlideView.OnSlideViewOnListener.SLIDE_STATUS_ON) {this.mLastSlideViewWithStatusOn = (SlideView) view;
    }
}

我们定义了一个记录打开隐藏控件的SlideView:

private SlideView mLastSlideViewWithStatusOn;

当滑动某个控件的时候,就将上一个打开隐藏控件的item关闭。如果这个控件滑动开了,也就记录这个控件status ==SlideView.OnSlideViewOnListener.SLIDE_STATUS_ON:

this.mLastSlideViewWithStatusOn = (SlideView) view;

在来看看实现点击事件的代码:

@Override
public void onClick(View v) {switch (v.getId()) {case R.id.delete: {int position = this.mListView.getPositionForView(v);
            if (position != ListView.INVALID_POSITION) {this.mMessageItem.remove(position);
                this.adapter.notifyDataSetChanged();
            }break;
        }case R.id.top: {int position = this.mListView.getPositionForView(v);
            if (position != ListView.INVALID_POSITION) {MessageItem item = this.mMessageItem.get(position);
                this.mMessageItem.remove(position);
                this.mMessageItem.add(0, item);
                this.adapter.notifyDataSetChanged();
            }break;
        }default:break;
    }
}

怎么删除item,因为这个代码没有任何解释,有必要说明一下。

为了获取操作屏幕的时值,点击的是那个item的删除按钮,就必须获取他的位置position,我们使用getPositionForView(),position for view,也就是位置根据View获取。接着也是判断获取的position是不是有效的。然后将根据位置删除这个item。mMessageItem:

private List<MessageItem> mMessageItem = new ArrayList<MessageItem>();

是信息的集合。

然后更新ListView:

this.adapter.notifyDataSetChanged();

而置顶思考的思路如下:

获取该item的位置,如上面代码所示,然后获得具体的MessageItem,将集合该位置的MessageItem删除,将具体的MessageItem插入到第一个位置。然后更新ListView如上所示。

置顶后的效果图如下所示:

下面为初始化Activity:

public void initView() {this.mListView = (ListViewCompat) findViewById(R.id.myList);
    for (int i = 0; i < 20; i++) {MessageItem item = new MessageItem();
        if (i < 10) {item.iconRes =imgRes[i];
            item.title = name[i];
            item.msg = author[i];
            item.time = time[i];
        } else {item.iconRes = imgRes[19-i];
            item.title = name[19 - i];
            item.msg = author[19 - i];
            item.time = time[19 - i];
        }this.mMessageItem.add(item);
    }this.adapter = new SlideAdapter();
    this.mListView.setAdapter(this.adapter);
    this.mListView.setOnItemClickListener(this);
}

成员变更如下:

private String[] name = {"吾国之教育病理", "谷物大脑", "中国历代经济变革得失", "了凡四训", "耶路撒冷三千年", "如果这是宋史", "中国近代史", "罗马人的故事", "光荣与梦想", "大秦帝国"};
private String[] author = {"郑也夫", "戴维·珀尔马特", "吴晓波", "袁了凡", "西蒙·蒙蒂菲奥里", "高天流云 ", "王奇生", "盐野七生 ", "威廉·曼彻斯特 ", "孙皓晖"};
private String[] time = {"2013-10-1 ", " 2015-5-20", "2013-8-1 ", "2007", "2013年3月", "2008", "2013年7月22日", "2011 年12月", "2006年", "2012年5月"};
private int[] imgRes = {R.drawable.book1, R.drawable.book2, R.drawable.book3, R.drawable.book4, R.drawable.book5, R.drawable.book6, R.drawable.book7, R.drawable.book8, R.drawable.book9, R.drawable.book10};
private ListViewCompat mListView;
private List<MessageItem> mMessageItem = new ArrayList<MessageItem>();
private SlideAdapter adapter;
// 上次处于打开状态的SlideView
private SlideView mLastSlideViewWithStatusOn;

activity_main.xml代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.liyuanjing.slidelistview.MyListView
        android:id="@+id/myList"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

本文章代码下载地址如下:

http://download.csdn.net/detail/liyuanjinglyj/9119875



仿QQ对话列表滑动删除与置顶的原理及实现相关推荐

  1. Android的SwipeToDismiss第三方开源框架模拟QQ对话列表侧滑删除,置顶,将头像图片圆形化处理。...

      <Android SwipeToDismiss:左右滑动删除ListView条目Item> Android的SwipeToDismiss是github上一个第三方开源框架(github ...

  2. 高仿微信对话列表滑动删除效果

    前言 用过微信的都知道.微信对话列表滑动删除效果是非常不错的,这个效果我们也能够有. 思路事实上非常easy,弄个ListView.然后里面的每一个item做成一个能够滑动的自己定义控件就可以.由于L ...

  3. android微信列表滑动删除,Android仿微信对话列表滑动删除效果

    微信对话列表滑动删除效果很不错的,借鉴了github上SwipeListView(项目地址:https://github.com/likebamboo/SwipeListView),在其上进行了一些重 ...

  4. Android 编程下代码之(QQ消息列表滑动删除)

       这份代码写出来有些时候了,一直没共享,现在把它共享给大家.简单列一下代码中你可以学到的知识点: 自定义控件的实现方式: 事件的拦截分发消费机制: QQ会话列表滑动删除原理: 最后附上源码链接:Q ...

  5. Android开发学习之仿手机QQ消息列表侧滑删除效果

    今天想和大家分享的是手机QQ消息列表侧滑删除效果,这种效果在IOS中被封装为一个列表控件,而手机QQ则是将这个功能移植到了Android上,换言之,这并非是手机QQ的独创.尽管如此,用户体验依然得到了 ...

  6. android 仿qq好友动态,Android UI仿QQ好友列表分组悬浮效果

    本文实例为大家分享了Android UI仿QQ好友列表分组悬浮效果的具体代码,供大家参考,具体内容如下 楼主是在平板上測试的.图片略微有点大,大家看看效果就好 接下来贴源代码: PinnedHeade ...

  7. 仿QQ分组列表(UITableView)

    // // ViewController.m // 仿QQ分组列表 // // Created by lanqs on 15/1/26. // Copyright (c) 2015年 Tanqihon ...

  8. android 仿qq好友列表分组效果及联系人分组效果

     历史记录仿QQ好友列表的动态效果 以及联系人的分组效果 QQ朋友分组的功能做的不错,大家都很认可,那么到底他的分组并且滑动的时候,标题能停留在顶部是如何实现的呢?今天从网上搜索了一下资料,自己运行了 ...

  9. iOS之仿QQ好友列表展开收缩效果的实现

    使用UICollectionView实现 思路 很明显整体它是一个列表,它的分组是一个列表,它里面的好友列表也是一个列表,所以就可以使用组头来设置分组列表,使用cell设置好友列表: 当点击组头的时候 ...

最新文章

  1. (13)中值滤波和双边滤波
  2. 手把手教你估算深度神经网络的最优学习率(附代码教程)
  3. 3.QML布局和输入元素
  4. xp如何快速锁定计算机,Window XP中快速锁定计算机两法
  5. 揭秘 RocketMQ 新特性以及在金融场景下的实践
  6. MVC与MVVM的区别
  7. C++面向对象思想 两条直线交点计算
  8. 【转载】水木算法讨论题
  9. Cookie案例-显示用户的上次访问时间代码实现
  10. mysql5驱动_技术分享 | MySQL 8 和 MySQL 5.7 在小型设备的内存消耗分析
  11. 洛谷 P1896 状压DP
  12. linux内核2.6.3x--Executable file formats / Emulations、 Networking support
  13. stm32c语言写数码管定时器,【程序】STM32F103单片机使用定时器DMA进行全自动8位数码管动态扫描...
  14. 网吧系统快速设置工具
  15. SecureCRT配置详细图文教程
  16. 核心期刊为什么难发?
  17. python-docx页眉横线
  18. mysql复制不了的文字怎么办_网页上的文字不能复制怎么办?
  19. win10突然无法显示图片缩略图怎么办
  20. IntelliJ IDEA(Ultimate版本)的下载、安装和WordCount的初步使用(本地模式和集群模式)...

热门文章

  1. Docker 从入门到入坑。
  2. 解决 npm install 时出现的.git can‘t be found (see https://git.io/Jc3F9)的问题 run `npm fund` for details问题解决
  3. FHC2022食饮魅力 势不可挡
  4. PHP重定向SEO,PHP类网站301重定向实战站长SEO必修课
  5. GitLab用户在组中有五种角色权限说明
  6. go开发之restful等幂性
  7. 【HDOJ】1022 Train Problem I_天涯浪子_新浪博客
  8. Java会被禁吗_关于java:如果字符串对象被禁闭,那么为什么一个更改不会影响其他对象...
  9. qq撤回的消息怎么找回 python_Python实现微信中找回好友、群聊用户撤回的消息功能示例...
  10. 软件工程实践总结(个人)