《简易新闻》源码分析
2016-05-20 21:40
232 查看
0. 前言
本文将对github上liuling开发的基于Material Design和MVP的《简易新闻》源码进行简要分析,通过本文你将学到:
阅读应用源码的步骤
RecyclerView
NavigationView
下拉刷新和上拉加载
Material过渡动画
CollapsingToolbarLayout
1. 寻找入口
分析一个应用就是从MainActivity下手,那么如何找到MainActivity呢?当然还是通过Manifest文件,不过,在进入Manifest文件前,我们先来看看工程的一个结构。1.1 工程总览
工程的目录结构如上图所示,有两个Module,一个是应用本身,还有一个是导入的swipeback库,用于滑动返回,如图:
1.2 Manifest文件
1.2.1 权限声明
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
首先声明了权限,分别是网络相关和位置相关的权限。
1.2.2 应用层
<application ... android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".main.widget.MainActivity" ... > ... </activity> ... </application>
这里Application标签中有一个属性
android:supportsRtl="true"是什么意思呢?这是Android 4.2的一个新特性 layoutRtl,主要是方便开发者去支持阿拉伯语/波斯语等从右到左的阅读习惯。
接下来指明了两个主要的Activity:
- MainActivity
- NewsDetailActivity
2. MainActivity
public class MainActivity extends AppCompatActivity implements MainView
主Activity实现了MainView接口,所以我们来先看看该接口:
*
2.1 MainView接口
接口很简单,包含四个方法声明,分别是主界面的四个拨动页面。public interface MainView { void switch2News(); void switch2Images(); void switch2Weather(); void switch2About(); }
2.2 onCreate()方法
2.2.1 布局文件
首先来看主布局文件,布局可以说是简单易懂,清晰明了。一个DrawerLayout中夹了协调布局包裹的FrameLayout作为主界面和一个NavigationView。其中值得注意的是就是这个NavigationView:
<android.support.design.widget.NavigationView android:id="@+id/navigation_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/navigation_header" app:menu="@menu/navigation_menu" />
app:headerLayout属性: 头布局文件,及抽屉上方的个人头像。说起头像,就要用到CircleImageView,相信也会有读者像我一样曾经好奇过CircleImageView用来干什么,怎么用吧,没错,就是这样用的:
<de.hdodenhof.circleimageview.CircleImageView android:id="@+id/profile_image" android:layout_width="72dp" android:layout_height="72dp" android:layout_marginTop="20dp" android:src="@drawable/protrait" app:border_color="@color/primary_light" app:border_width="2dp" />
app:menu属性: 使用菜单来填充选项,大家就不要以为只可以使用ListView自定义来实现菜单选择咯,但是笔者认为这里有个缺陷就是,抽屉会默认遮住状态栏和Toolbar。
<group android:checkableBehavior="single"> <item android:id="@+id/navigation_item_news" android:icon="@drawable/ic_assessment_white_24dp" android:checked="true" android:title="@string/navigation_news" /> ... </group>
2.2.2 初始化视图
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.drawer_open, R.string.drawer_close); mDrawerToggle.syncState(); mDrawerLayout.setDrawerListener(mDrawerToggle); mNavigationView = (NavigationView) findViewById(R.id.navigation_view); setupDrawerContent(mNavigationView); private void setupDrawerContent(NavigationView navigationView) { navigationView.setNavigationItemSelectedListener( new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem menuItem) { mMainPresenter.switchNavigation(menuItem.getItemId()); menuItem.setChecked(true); mDrawerLayout.closeDrawers(); return true; } }); }
首先实例化了ActionBar开关,同时调用syncState()同步状态,后面对mNavigationView设置了监听,实现了切换选项卡的效果。
// in class MainPresenterImpl @Override public void switchNavigation(int id) { switch (id) { case R.id.navigation_item_news: mMainView.switch2News(); break; case R.id.navigation_item_images: mMainView.switch2Images(); break; case R.id.navigation_item_weather: mMainView.switch2Weather(); break; case R.id.navigation_item_about: mMainView.switch2About(); break; default: mMainView.switch2News(); break; } }
2.2.3 切换
@Override public void switch2News() { getSupportFragmentManager().beginTransaction().replace(R.id.frame_content, new NewsFragment()).commit(); mToolbar.setTitle(R.string.navigation_news); }
四个选项卡切换Fragment即可,那我们依次来看看这几个Fragment。
3. 新闻界面
3.1 布局文件
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.TabLayout android:id="@+id/tab_layout" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:tabIndicatorColor="@color/icons"/> </android.support.design.widget.AppBarLayout> <android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_behavior="@string/appbar_scrolling_view_behavior"/> </android.support.design.widget.CoordinatorLayout>
注意点
tabIndicatorColor: TabLayout所选标签标志颜色,一张图看懂,此处改为蓝色
layout_behavior: 滚动时自动消失Toolbar
3.2 视图初始化
private void setupViewPager(ViewPager mViewPager) { //Fragment中嵌套使用Fragment一定要使用getChildFragmentManager(),否则会有问题 MyPagerAdapter adapter = new MyPagerAdapter(getChildFragmentManager()); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_TOP), getString(R.string.top)); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_NBA), getString(R.string.nba)); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_CARS), getString(R.string.cars)); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_JOKES), getString(R.string.jokes)); mViewPager.setAdapter(adapter); } public static class MyPagerAdapter extends FragmentPagerAdapter { private final List<Fragment> mFragments = new ArrayList<>(); private final List<String> mFragmentTitles = new ArrayList<>(); public MyPagerAdapter(FragmentManager fm) { super(fm); } public void addFragment(Fragment fragment, String title) { mFragments.add(fragment); mFragmentTitles.add(title); } @Override public Fragment getItem(int position) { return mFragments.get(position); } @Override public int getCount() { return mFragments.size(); } @Override public CharSequence getPageTitle(int position) { return mFragmentTitles.get(position); } }
值得学习的是Adapter的写法,它将mFragments和mFragmentTitles两个List整合到了Adapter内部。还有要注意Fragment中嵌套使用Fragment一定要使用getChildFragmentManager(),接下来看其子项Fragment。
3.3 NewsListFragment
3.3.1 布局
布局很简单,一个SwipeRefreshLayout包裹RecyclerView。3.3.2 初始化视图
@Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_newslist, null); mSwipeRefreshWidget = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh_widget); mSwipeRefreshWidget.setColorSchemeResources(R.color.primary, R.color.primary_dark, R.color.primary_light, R.color.accent); mSwipeRefreshWidget.setOnRefreshListener(this); mRecyclerView = (RecyclerView)view.findViewById(R.id.recycle_view); mRecyclerView.setHasFixedSize(true); mLayoutManager = new LinearLayoutManager(getActivity()); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setItemAnimator(new DefaultItemAnimator()); mAdapter = new NewsAdapter(getActivity().getApplicationContext()); mAdapter.setOnItemClickListener(mOnItemClickListener); mRecyclerView.setAdapter(mAdapter); mRecyclerView.addOnScrollListener(mOnScrollListener); onRefresh(); return view; }
值得提的是mSwipeRefreshWidget.setColorSchemeResources()方法可以设置刷新等待条的颜色;mRecyclerView.setItemAnimator()可以设置增加卡片动画。
3.3.3 点击事件
@Override public void onItemClick(View view, int position) { NewsBean news = mAdapter.getItem(position); Intent intent = new Intent(getActivity(), NewsDetailActivity.class); intent.putExtra("news", news); View transitionView = view.findViewById(R.id.ivNews); ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), transitionView, getString(R.string.transition_news_img)); ActivityCompat.startActivity(getActivity(), intent, options.toBundle()); }
点击时,通过带有NewsBean参数的Intent启动新闻详情Activity,此外,在跳转页面的同时会有一个动画,通过以上代码可以实现动画。具体流程是先取得CardView中的ImageView,然后通过ActivityOptionsCompat makeSceneTransitionAnimation()方法取得过渡动画参数,并加在startActivity中。
3.3.4 上拉加载实现
上拉加载更多的实现主要有两个关键部分,一个是滚动事件的监听,另一个是Adapter内的视图创建。private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { private int lastVisibleItem; @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); lastVisibleItem = mLayoutManager.findLastVisibleItemPosition(); } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == mAdapter.getItemCount() && mAdapter.isShowFooter()) { //加载更多 LogUtils.d(TAG, "loading more data"); mNewsPresenter.loadNews(mType, pageIndex + Urls.PAZE_SIZE); } } };
滚动监听中判断三个条件:是否处于滚动暂停状态、当前页面的最后一个条目是否为所有信息中的最后一个条目、是否不处于正在加载新的条目状态。三个条件同时满足的情况下加载新条目。加载完新条目后又会调用mAdapter.isShowFooter(true)。
@Override public int getItemViewType(int position) { // 最后一个item设置为footerView if(!mShowFooter) { return TYPE_ITEM; } if (position + 1 == getItemCount()) { return TYPE_FOOTER; } else { return TYPE_ITEM; } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if(viewType == TYPE_ITEM) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_news, parent, false); ItemViewHolder vh = new ItemViewHolder(v); return vh; } else { View view = LayoutInflater.from(parent.getContext()).inflate( R.layout.footer, null); view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); return new FooterViewHolder(view); } }
onCreateViewHolder()方法是在每个条目创建时都会调用的方法,它用来填充视图,所以需要在这里进行选择需要创建的视图类型,这样便实现了上拉加载。
3.3.5 NewsAdapter
NewsAdapter其实在上文中有所提及,这里再进行一些补充。public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { public TextView mTitle; public TextView mDesc; public ImageView mNewsImg; public ItemViewHolder(View v) { super(v); mTitle = (TextView) v.findViewById(R.id.tvTitle); mDesc = (TextView) v.findViewById(R.id.tvDesc); mNewsImg = (ImageView) v.findViewById(R.id.ivNews); v.setOnClickListener(this); } @Override public void onClick(View view) { if(mOnItemClickListener != null) { mOnItemClickListener.onItemClick(view, this.getPosition()); } } }
首先是内部类ItemViewHolder,在这里有两个细节值得注意:一个是它的成员变量到初始化通过传入构造函数的View就实现了,不需要将每个参数都传入,二是为每个条目的点击事件设立了依赖注入,使其解耦。
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if(holder instanceof ItemViewHolder) { NewsBean news = mData.get(position); if(news == null) { return; } ((ItemViewHolder) holder).mTitle.setText(news.getTitle()); ((ItemViewHolder) holder).mDesc.setText(news.getDigest()); ImageLoaderUtils.display(mContext, ((ItemViewHolder) holder).mNewsImg, news.getImgsrc()); } } public static void display(Context context, ImageView imageView, String url) { if(imageView == null) { throw new IllegalArgumentException("argument error"); } Glide.with(context).load(url).placeholder(R.drawable.ic_image_loading) .error(R.drawable.ic_image_loadfail).crossFade().into(imageView); }
在每个子条目的内容设置中,调用了Glide图片加载库进行图片的加载。关于Glide的使用读者可以参考这篇博客: Google推荐的图片加载库Glide介绍
3.3.6 显示失败消息
@Override public void showLoadFailMsg() { if(pageIndex == 0) { mAdapter.isShowFooter(false); mAdapter.notifyDataSetChanged(); } View view = getActivity() == null ? mRecyclerView.getRootView() : getActivity().findViewById(R.id.drawer_layout); Snackbar.make(view, getString(R.string.load_fail), Snackbar.LENGTH_SHORT).show(); }
调用Snackbar显示消息即可。
4. 新闻详情Activity
4.1 界面布局
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="256dp" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:expandedTitleMarginEnd="64dp" app:expandedTitleMarginStart="48dp" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <ImageView android:id="@+id/ivImage" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:scaleType="centerCrop" android:transitionName="@string/transition_news_img" app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="0.7"/> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ProgressBar android:id="@+id/progress" style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"/> <org.sufficientlysecure.htmltextview.HtmlTextView android:id="@+id/htNewsContent" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="12dp" android:textAppearance="@android:style/TextAppearance.Medium"/> </LinearLayout> </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout>
这个页面布局代码全部都粘贴过来了,大家可想而知这个布局的重要性,让我们来细嚼一下这段布局代码。
4.1.1 CollapsingToolbarLayout
CollapsingToolbarLayout作用是提供了一个可以折叠的Toolbar,它继承至FrameLayout,给它设置layout_scrollFlags,它可以控制包含在CollapsingToolbarLayout中的控件(如:ImageView、Toolbar)在响应
layout_behavior事件时作出相应的scrollFlags滚动事件(移除屏幕或固定在屏幕顶端)。
app相关属性介绍:
在CollapasingToolbarLayout 的属性:
app:contentScrim=”?attr/colorPrimary” — 设置此属性生,CollapsingToolbarLayout完成折叠动画后,Title部分会显示一个普通的颜色,代码中的颜色来自于style文件中的colorPrimary属性
app:expandedTitleMarginStart=”48dp” — 控制文本的边距
app:expandedTitleMarginEnd=”64dp” — 控制文本的边距
app:layout_scrollFlags —设置CollapsingToolbarLayout滚动折叠,关于这个属性需要详细解说,请看以下内容:
scroll -想要滚动就必须设置这个标记
exitUntilCollapsed -向上滚动收缩View,可以一直固定在ToolBar上面
enterAlwaysCollapsed -当你的View已经设置minHeight属性又使用此标志时,你的View只能以最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。
在ImageView控件中属性:
app:layout_collapseMode — 折叠模式 有俩个值
pin —设置这个模式时,当CollapsingToolbarLayout完全收缩后,ImageView显示的内容系统自己决定
parallax –设置这个模式时,当CollapsingToolbalLayout完全收缩后,ImageView显示的内容可以通过设置layout_collapseParallaxMultiplier来决定显示图片的哪部分内容
app:layout_collapseParallaxMultiplier=”0.7” 设置滚动视差,值为0~1。注意这个属性需要layout_collapseMode开启parallax模式后才会有作用,它决定CollapsingToolbarLayout完全折叠显示的内容
CollapsingToolbarLayout配置完成之后,它下面的Layout必须设置layout_behavior属性来响应CollapsingToolbarLayout,如果没有配置layout_behavio,CollapsingToolbarLayout将没有折叠效果。
注意:app:layout_behavior=”@string/appbar_scrolling_view_behavior”如果没有此属性那么CollapsingToolbarLayout将不会有折叠效果。
4.1.2 NestedScrollView
Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性,这便是NestedScrollView,没什么多讲的,感兴趣的读者可以自行研究。4.1.3 HtmlTextView
HtmlTextView是github上的一个开源框架,它是Android TextView控件的一个扩展,可以加载的HTML并将其转换成Spannable用于显示它。这是WebView组件的一个替代。4.2 视图相关
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_news_detail); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); mProgressBar = (ProgressBar) findViewById(R.id.progress); mTVNewsContent = (HtmlTextView) findViewById(R.id.htNewsContent); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); toolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onBackPressed(); } }); mSwipeBackLayout = getSwipeBackLayout(); mSwipeBackLayout.setEdgeSize(ToolsUtil.getWidthInPx(this)); mSwipeBackLayout.setEdgeTrackingEnabled(SwipeBackLayout.EDGE_LEFT); mNews = (NewsBean) getIntent().getSerializableExtra("news"); CollapsingToolbarLayout collapsingToolbar = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); collapsingToolbar.setTitle(mNews.getTitle()); ImageLoaderUtils.display(getApplicationContext(), (ImageView) findViewById(R.id.ivImage), mNews.getImgsrc()); mNewsDetailPresenter = new NewsDetailPresenterImpl(getApplication(), this); mNewsDetailPresenter.loadNewsDetail(mNews.getDocid()); } @Override public void showNewsDetialContent(String newsDetailContent) { mTVNewsContent.setHtmlFromString(newsDetailContent, new HtmlTextView.LocalImageGetter()); }
首先值得一提的是SwipeBackLayout,这是一个滑动返回库,使用方法非常简单:
1. 继承SwipeBackActivity
2. mKeyTrackingMode = getString(R.string.key_tracking_mode);
3. mSwipeBackLayout = getSwipeBackLayout();
4. mSwipeBackLayout.setEdgeTrackingEnabled(edgeFlag);
5. saveTrackingMode(edgeFlag);
还有注意的是,在使用CollapsingToolbarLayout时,设置Toolbar标题要调用collapsingToolbar.setTitle(),而不是Toolbar的set方法,另外,笔者暂时还未发现如何为展开的Toolbar和折叠的Toolbar设置两个不同的标题,不过可以通过collapsingToolbar.setCollapsedTitleTextColor和collapsingToolbar.setExpandedTitleColor设置为不同颜色,并且底层自动处理颜色的过渡与渐变。
5. 新闻业务处理
/** * 加载新闻详情 * @param docid * @param listener */ @Override public void loadNewsDetail(final String docid, final OnLoadNewsDetailListener listener) { String url = getDetailUrl(docid); OkHttpUtils.ResultCallback<String> loadNewsCallback = new OkHttpUtils.ResultCallback<String>() { @Override public void onSuccess(String response) { NewsDetailBean newsDetailBean = NewsJsonUtils.readJsonNewsDetailBeans(response, docid); listener.onSuccess(newsDetailBean); } @Override public void onFailure(Exception e) { listener.onFailure("load news detail info failure.", e); } }; OkHttpUtils.get(url, loadNewsCallback); }
业务层主要包括网络请求和Json数据处理,这些主要通过框架来实现,由于此应用使用的框架在当今不太火热,所以就不作具体分析了。
6. 天气界面
天气界面主要看的就是布局和Json解析,思路容易理解,就不作分析了。
@Override public void loadWeatherData(String cityName, final LoadWeatherListener listener) { try { String url = Urls.WEATHER + URLEncoder.encode(cityName, "utf-8"); OkHttpUtils.ResultCallback<String> callback = new OkHttpUtils.ResultCallback<String>() { @Override public void onSuccess(String response) { List<WeatherBean> lists = WeatherJsonUtils.getWeatherInfo(response); listener.onSuccess(lists); } @Override public void onFailure(Exception e) { listener.onFailure("load weather data failure.", e); } }; OkHttpUtils.get(url, callback); } catch (UnsupportedEncodingException e) { LogUtils.e(TAG, "url encode error.", e); } }
总结
至此,《简易新闻》的代码分析就基本结束了,回顾全文,其实学到的不仅是Material的UI,还包括整个App的架构——MVP模式,该架构体现在每个模块,在代码方面,每个包以功能区分,在抽象方面每个模块又以MVP区分。最后,感谢读者的耐心阅读和App作者的无私奉献,然后祝大家学习进步!参考
android 4.2的新特性layoutRtl,让布局自动从右往左显示Google推荐的图片加载库Glide介绍
[Android] 可以折叠的CollapsingToolbarLayout
Android5.0+(CollapsingToolbarLayout)
支持加载Html内容的TextView:HtmlTextView for Android
相关文章推荐
- 第一个Delphi小程序
- Struts逐步学习
- C++向量
- lightoj 1248 Dice (III)(几何分布+期望)
- mac 搭建react-native环境,无法run-android的问题
- Android(安卓)与PC通过USB线进行短信同步
- PHP CURL CURLOPT参数说明(curl_setopt)
- Flux 应用架构
- Java基础——数组(一维数组,二维数组)
- 如何将oracle数据1000行合并成一行
- Android(安卓)与PC通过USB线进行短信同步
- LeetCode Word Ladder II
- Javascript基础知识盲点总结——继承
- [Win10应用开发] 如何使用Windows通知
- Linux中如何添加/删除FTP用户并设置权限(后续)
- YTU 3023: 树的遍历
- Perl 面向对象编程的两种实现和比较:
- Perl 面向对象编程的两种实现和比较:
- Perl 面向对象编程的两种实现和比较:
- VMWare克隆或复制没有IP