Android View事件分发机制 一(View)
来源:互联网 发布:linux zip 压缩 编辑:程序博客网 时间:2024/06/05 14:42
下面我们来讨论Android事件分发机制.
一、点击事件小例子分析
为了分析安卓事件分发机制,我们先分析一个小例子。项目名叫做ClickExample1.下载地址:
1.项目源码
界面如下:
布局文件如下,布局中1个LinearLayout中有1个Button按钮。
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/my_linearLayout"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Click me!" android:id="@+id/my_btn" android:layout_gravity="center_vertical"/></LinearLayout>
Activity如下 ,实现了 OnTouchListenser和OnClickListener两个接口。
public class MainActivity extends Activity implements View.OnTouchListener,View.OnClickListener{ private LinearLayout mLayout; private Button mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mLayout =(LinearLayout)this.findViewById(R.id.my_linearLayout); mButton=(Button)this.findViewById(R.id.my_btn); mLayout.setOnTouchListener(this); mButton.setOnTouchListener(this); mLayout.setOnClickListener(this); mButton.setOnClickListener(this); } @Override public void onClick(View v) { Log.i("ClickExample1","OnClickListener--OnClick--"+v); } @Override public boolean onTouch(View v, MotionEvent event) { Log.i("ClickExample1","OnTouchListener--OnTouch--action="+event.getAction()+"--"+v); return false; }}
2.事件分析
主要对onClick和onTouch两个方法进行分析
事件1:点击Button区域时打印如下:
事件2.点击除过Button以外区域的其他地方时打印如下:
事件3.点击Button时按在Button区域,并在buton区域晃动一下松开后的打印如下:
其中onTouch
方法中有一个MotionEvent
参数,该类中定义了点击事件类型,并在事件发生时传递给onTouch方法,事件类型以及它们的的值定义在MotionEvent 类中,常见的值有下面几种:
public static final int ACTION_DOWN = 0; public static final int ACTION_UP = 1; public static final int ACTION_MOVE = 2;
通过对log日志的分析,我们可以得出下面的结论:
事件1:onTouch(ACTION_DOWN)
->onTouch(ACTION_UP)
->onClick()
事件2:onTouch(ACTION_DOWN)
->onTouch(ACTION_UP)
->onClick()
事件3:onTouch(ACTION_DOWN)
->onTouch(ACTION_MOVE
->…….->onTouch(ACTION_MOVE
->onTouch(ACTION_UP)
->onClick()
可以看到,onTouch方法在onClick方法之前执行。
其中onTouch方法的返回值为false
,如果我们将它的返回值改为true
的话,即:
@Override public boolean onTouch(View v, MotionEvent event) { Log.i("ClickExample1","OnTouchListener--OnTouch--action="+event.getAction()+"--"+v); return true; }
再次运行该APP,会发现onClick
方法得不到执行
上面的例子总结起来就是:
1.onTouch
方法先于onClick
方法执行
2.onTouch
方法返回值为true
时,后面的onClick
方法不会继续执行
二、源码分析
首先我们查看两个类View 和ViewGroup的类的继承关系
通过View
和ViewGroup
的继承关系我们知道:所有view控件都是继承自View
类,布局控件ViewGroup
也继承自View
类。与事件分发相关的方法是View
控件中dispatchTouchEvent
方法。
1.dispatchTouchEvent
方法
Android 6.0中View
控件中dispatchTouchEvent
方法如下:
public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (!isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
其中跟onTouch
方法调用相关的关键代码为
... if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } }...
其中ListenerInfo
为View的内部类,它包含多个xxxListener
接口成员变量,其中就包括上一小节实现的OnTouchListener
、OnClickListener
两个接口,mListenerInfo
为ListenerInfo
在View内部的实例,当我们调用setxxxListener
时,会把xxxListener
的实例设置到mListenerInfo
中,例如setOnClickListener
方法的具体实现
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
在onTouch
方法调用相关的关键代码中,我们发现 mListenerInfo
、mOnTouchListener
,以及mViewFlags
的值会影响onTouch
方法的执行,同时onTouch
的返回值会影响后面onTouchEvent
方法的执行,当onTouch
返回true
时,后面的onTouchEvent
不会执行。其中onClick
方法跟onTouchEvent
方法存在联系。
总结起来就是:
1.产生触摸事件时,View
组件的dispatchTouchEvent
方法会被首先调用,在dispatchTouchEvent
方法中,会依次调用onTouch
和onTouchEvent
方法,其中onTouchEvent
方法会引用onClick
方法的调用。
2.当mListenerInfo
、mOnTouchListener
的值为NULL
以或者mViewFlags
的值不为ENABLED
时,会中断onTouch
方法的执行,直接执行onTouchEvent
方法,这时dispatchTouchEvent
的返回值与onTouchEvent
的返回值相同。当onTouch
方法执行后返回值为true
时,会中断onTouchEvent
的执行,dispatchTouchEvent
的返回值为true
。当onTouch
方法执行后返回值为false
时,onTouchEvent
方法会执行,dispatchTouchEvent
的返回值与onTouchEvent
的返回值相同。
2.onTouchEvent方法
在dispatchTouchEvent
方法中,我们知道onTouchEvent
方法与onClick
方法之间存在联系,下面我们来分析onTouchEvent
方法。
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
其中
... if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } ...
部分判断当前view是否DISABLED
,如果是DISABLED
的话,且CLICKABLE
直接消费该事件,返回true
,onClick
方法不会得到执行。如果是DISABLED
,且不是CLICKABLE
则直接返回false
,onClick
方法不会得到执行。总结起来就是当前view是DISABLED
的,onClick
方法不会得到执行,且返回 CLICKABLE
的布尔值。
接着如果View是ENBALE
和UNCLICKABLE
的话,直接返回false
。如果View是ENBALE
和CLICKABLE
的话如根据触摸事件类型进入相应的switch分支中,最后返回true
。
... if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0); } break; ...
其中ACTION_DOWN
事件进入ACTION_DOWN
分支中进行相关处理,ACTION_UP
事件进入 ACTION_UP
中 进行相关的处理,在ACTION_UP
中,会依次获取焦点、设置pressed状态、在pressed状态下创建一个名为PerformClick
的Runnable
接口并post到UI线程消息队列中执行performClick
方法,其中performClick
方法如下
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
在performClick
方法中会判断是否设置了OnClickListener
接口,如果设置了就执行onClick
方法并返回true
,如果没有就返回false
.其中setOnClickListener
方法会同时设置view为ONCLICKABLE
。
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
总结起来就是:
1.onClick
方法会在onTouchEvent
方中的ACTION_UP
方法中触发。
2.当前view是DISABLED
的,执行 onTouchEvent
方法时onClick
方法不会得到执行,且返回 CLICKABLE
的真值。
2.当dispatchTouchEvent
在进行事件分发的时候,只有前一个action
返回true
,才会触发下一个action
。(待验证)
三、小例子验证
我们新建一个ExampleClick2
小例子,自定义实现一个Button
按钮,重写其中跟事件相关的方法
3.1 项目源码
自定义Button
public class TestButton extends Button { public TestButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { Log.i("ClickExample2","onTouchEvent--action="+event.getAction()); return super.onTouchEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction()); return super.dispatchTouchEvent(event); }}
布局文件
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/my_linear" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <com.lengyu.free.clickexample2.TestButton android:id="@+id/my_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button Test!"/></LinearLayout>
MainActivity.java文件代码。
package com.lengyu.free.clickexample2;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.widget.LinearLayout;public class MainActivity extends Activity implements View.OnTouchListener,View.OnClickListener{ private LinearLayout mLayout; private TestButton mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mLayout=(LinearLayout)findViewById(R.id.my_linear); mButton=(TestButton)findViewById(R.id.my_btn); mLayout.setOnTouchListener(this); mButton.setOnTouchListener(this); mLayout.setOnClickListener(this); mButton.setOnClickListener(this); } @Override public void onClick(View v) { Log.i("ClickExample2","onClickListener--onClick--"+v); } @Override public boolean onTouch(View v, MotionEvent event) { Log.i("ClickExample2","onTouchListener--onTouch--action="+event.getAction()+v); return false; }}
3.2 事件分析
事件1:点击Button区域,在Button区域移动后离开。
日志显示:
通过日志分析,我们知道,依次产生了4个事件 :ACTION_DOWN
->ACTION_MOVE
->ACTION_MOVE
->ACTION-UP
,
对于前三项,依次调用dispatchTouchEvent
->onTouch
->onTouchEvent
,返回true。
第四项 dispatchTouchEvent
->onTouch
->onTouchEvent
->onClick
,返回true。
我们对上面的TestButton代码进行如下修改
@Override public boolean onTouchEvent(MotionEvent event) { Log.i("ClickExample2","onTouchEvent--action="+event.getAction()); return true; }
进行下面的事件2
事件2:点击Button区域
日志输出
点击时发现Button没有长按状态且onClick
方法没有执行,这是由于没有调用父类中的onTouch方法所导致的。
接着修改TestButton代码onTouchEvent方法
@Override public boolean onTouchEvent(MotionEvent event) { Log.i("ClickExample2","onTouchEvent--action="+event.getAction()); super.onTouchEvent(event); return true; }
事件3:点击Button区域
日志输出:
和事件1:的日志输出类似
接着修改TestButton代码onTouchEvent方法
@Override public boolean onTouchEvent(MotionEvent event) { Log.i("ClickExample2","onTouchEvent--action="+event.getAction()); return false; }
事件4:点击Button区域
日志输出:
可以看 dispatchTouchEvent
第一次分发ACTION_DOWN
事件返回false
后,后面的事件不再分发,而是触发了父控件LinearLayout
的onTouch
和onTouchEvent
事件。
同理修改TestButton代码onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) { Log.i("ClickExample2","onTouchEvent--action="+event.getAction()); super.onTouchEvent(event); return false; }
事件5:点击Button区域
日志输出
保持之前TestButton代码其它代码不变。修改其中的dispatchTouchEvent
方法如下
public boolean dispatchTouchEvent(MotionEvent event) { Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction()); return true; }
事件6:点击Button区域
日志输出
dispatchTouchEvent
方法不调用父方法的话,其它任何事件都得不到触发。
保持之前TestButton代码其它代码不变。修改其中的dispatchTouchEvent
方法如下
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction()); super.dispatchTouchEvent(event); return true; }
事件7:点击Button区域
日志输出
跟事件1类似。
修改代码dispatchTouchEvent
代码,返回false
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction()); return false; }
事件8:点击Button区域
日志输出
dispatchTouchEvent
代码,返回false
,跟事件4处理流程相似。事件不分发,触发LinearLayout
中的事件,按钮没有按下状态。
接着修改dispatchTouchEvent
中代码,调用父类中的方法,返回false
,如下:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction()); super.dispatchTouchEvent(event); return false; }
事件9:点击Button区域
日志输出
dispatchTouchEvent
代码,返回false
,跟事件4处理流程相似。事件不分发,触发LinearLayout
中的事件,由于调用了父类的方法,按钮有按下状态。
修改dispatchTouchEvent
返回值为true
,onTouchEvent
为false
:
@Override public boolean onTouchEvent(MotionEvent event) { Log.i("ClickExample2","onTouchEvent--action="+event.getAction()); super.onTouchEvent(event); return false; } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction()); super.dispatchTouchEvent(event); return true; }
事件10:点击Button区域
日志输出:
事件仍然得到了传递
修改dispatchTouchEvent
返回值为false
,onTouchEvent
为true
:
事件11:点击Button区域
事件没有得到传递,调用了LinearLayout中的方法
终结起来就是dispatchTouchEvent
派发事件的,如果返回值为false
将停止下次事件派发,如果返回true
将继续下次派发.
四 总结
通过上面的分析,我们对于VIew事件分发机制可以得出以下结论:
1.产生触摸事件时,View 组件的dispatchTouchEvent方法会被首先调用,在dispatchTouchEvent方法中,会依次调用onTouch和onTouchEvent方法,其中onTouchEvent方法会引用onClick方法的调用。
2.当mListenerInfo 、mOnTouchListener的值为NULL以或者mViewFlags的值不为ENABLED
时,会中断onTouch方法的执行,直接执行onTouchEvent方法,这时dispatchTouchEvent的返回值与onTouchEvent的返回值相同。当onTouch方法执行后返回值为true时,会中断onTouchEvent的执行,dispatchTouchEvent的返回值为true。当onTouch方法执行后返回值为false时,onTouchEvent方法会执行,dispatchTouchEvent的返回值与onTouchEvent的返回值相同。
3.onClick
方法会在onTouchEvent
方中的ACTION_UP
方法中触发。但是如果在调用onTouchEvent
方法时,View 是DISENABLED
状态的话,onClick
方法不会得到执行,且返回 CLICKABLE
的布尔值。
4.当dispatchTouchEvent
在进行事件分发的时候,只有前一个action
返回true
,才会触发下一个action
。
参考文章:
http://blog.csdn.net/yanbober/article/details/45887547/
- Android----View事件分发机制(一)
- Android View的事件分发机制(一):View
- Android View事件分发机制 一(View)
- Android View、ViewGroup 事件分发机制(一)
- Android进阶笔记(一)View事件分发机制理解
- 【Android学习】View点击事件的分发机制(一)
- Android View的事件分发机制(一)
- Android自定义View的事件分发机制(一)
- View 事件的分发机制 (一)
- View 的事件分发机制(一)
- View事件分发机制(一)
- 笔记:事件分发机制(一):View的事件分发
- android 事件分发机制(View)
- Android(View)事件分发机制上
- Android----View事件分发机制(二)
- Android事件分发机制(View)
- Android事件分发机制(View篇)
- android View事件分发机制。
- npm install 和npm start各种报错无法解决的问题
- 文件操作
- HDU
- Codeforces Beta Round #51 D
- 视频服务器实现思路
- Android View事件分发机制 一(View)
- GJJ的日常之沉迷数学
- (*´Д`*)超讨厌php的变量定义
- Java练习(方法调用)
- 快速幂求模+矩阵的快速幂
- 过山车
- ConnectionResetError: [Errno 104] Connection reset by peer
- HDU-2017中国大学生程序设计竞赛-网络选拔赛-1005-CaoHaha's staff
- corresponds to your MySQL server version for the right syntax to use near错误原因之一