├── res ├── drawable │ ├── icon.png │ ├── search_icon.png │ ├── browser_icon.png │ └── search_progress_bar_background.9.png ├── drawable-hdpi │ ├── icon.png │ └── search_progress_bar_background.9.png ├── drawable-ldpi │ └── icon.png ├── drawable-mdpi │ ├── icon.png │ └── search_progress_bar_background.9.png ├── xml │ └── searchable.xml ├── layout │ ├── progress_bar.xml │ ├── main.xml │ └── default_url_dialog.xml └── values │ └── strings.xml ├── .gitattributes ├── src └── org │ ├── badboy │ └── browser │ │ ├── WebProgressBar.java │ │ ├── BrowserActivity.java │ │ └── Downloads.java │ └── metalev │ └── multitouch │ └── controller │ └── MultiTouchController.java ├── AndroidManifest.xml └── .gitignore /res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable/icon.png -------------------------------------------------------------------------------- /res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /res/drawable/search_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable/search_icon.png -------------------------------------------------------------------------------- /res/drawable/browser_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable/browser_icon.png -------------------------------------------------------------------------------- /res/drawable/search_progress_bar_background.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable/search_progress_bar_background.9.png -------------------------------------------------------------------------------- /res/drawable-hdpi/search_progress_bar_background.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable-hdpi/search_progress_bar_background.9.png -------------------------------------------------------------------------------- /res/drawable-mdpi/search_progress_bar_background.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdtianyu/MyBrowser/master/res/drawable-mdpi/search_progress_bar_background.9.png -------------------------------------------------------------------------------- /res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /src/org/badboy/browser/WebProgressBar.java: -------------------------------------------------------------------------------- 1 | package org.badboy.browser; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.widget.LinearLayout; 6 | import android.widget.ProgressBar; 7 | 8 | public class WebProgressBar extends LinearLayout { 9 | 10 | private ProgressBar bar; 11 | public WebProgressBar(Context context) { 12 | super(context); 13 | LayoutInflater factory = LayoutInflater.from(context); 14 | factory.inflate(R.layout.progress_bar, this); 15 | bar = (ProgressBar)findViewById(R.id.progress_bar); 16 | } 17 | public void setProgress(int newProgress){ 18 | bar.setProgress(newProgress); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /res/layout/progress_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 12 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World, BrowserActivity! 4 | MyBrowser 5 | New web page 6 | Search websites 7 | 输入的网址有误,请重新输入! 8 | http://www.google.com.hk/ 9 | http://www.uc.cn/ 10 | Set Default URL 11 | OK 12 | Cancel 13 | 请输入默认主页网址 14 | SD card unavailable 15 | The SD card is busy. To allow downloads, select \"Turn off USB storage\" in the notification. 16 | An SD card is required to download 17 | SD card unavailable 18 | 网络错误! 19 | Page is loading... 20 | File is loading... 21 | 22 | -------------------------------------------------------------------------------- /res/layout/default_url_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 19 | 26 | 27 | 32 | 37 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | -------------------------------------------------------------------------------- /src/org/badboy/browser/BrowserActivity.java: -------------------------------------------------------------------------------- 1 | package org.badboy.browser; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.FileInputStream; 5 | import java.io.FileOutputStream; 6 | import java.io.InputStream; 7 | import java.net.HttpURLConnection; 8 | import java.net.MalformedURLException; 9 | import java.net.URL; 10 | 11 | import org.metalev.multitouch.controller.MultiTouchController; 12 | import org.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas; 13 | import org.metalev.multitouch.controller.MultiTouchController.PointInfo; 14 | import org.metalev.multitouch.controller.MultiTouchController.PositionAndScale; 15 | 16 | import android.app.Activity; 17 | import android.app.Dialog; 18 | import android.app.ProgressDialog; 19 | import android.app.SearchManager; 20 | import android.content.ContentValues; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.res.Configuration; 24 | import android.net.Uri; 25 | import android.os.Bundle; 26 | import android.view.KeyEvent; 27 | import android.view.Menu; 28 | import android.view.MenuItem; 29 | import android.view.MotionEvent; 30 | import android.view.View; 31 | import android.view.Window; 32 | import android.webkit.DownloadListener; 33 | import android.webkit.URLUtil; 34 | import android.webkit.WebChromeClient; 35 | import android.webkit.WebView; 36 | import android.webkit.WebViewClient; 37 | import android.widget.Button; 38 | import android.widget.EditText; 39 | import android.widget.ProgressBar; 40 | import android.widget.Toast; 41 | 42 | /*** 43 | * 使用WebView对象创建�?���?��网页浏覻器�? 44 | * @author BadBoy 45 | * 46 | */ 47 | public class BrowserActivity extends Activity implements MultiTouchObjectCanvas{ 48 | 49 | //public static final String DOWNLOAD_ACTION = "android.intent.action.DOWNLOAD"; 50 | 51 | private final static int MENU_NEW_WEB = Menu.FIRST; 52 | private final static int MENU_SET_MAIN_PAGE = MENU_NEW_WEB + 1; 53 | 54 | private String defaultGoogleUrl = ""; 55 | private String defaultUCWebUrl = ""; 56 | private String defaultUrl = ""; 57 | private WebView mWebView; 58 | //储存默认网址文件 59 | private String fileUrl = "fileUrl.txt"; 60 | //对话框标�? 61 | private final static int PROGRESS_DIALOG = 110; 62 | private final static int SET_DEFAULT_URL_DIALOG = 111; 63 | //下载进度条标�? 64 | private final static int DOWNLOAD_PROGRESS_DIALOG = 121; 65 | private boolean isDownload = false; 66 | private int downloads = 0; 67 | //显示网页加载进度 68 | private ProgressDialog mDialog; 69 | private ProgressBar bar; 70 | //下载进度条显�? 71 | private ProgressDialog downloadProgress; 72 | //多点触摸 73 | private MultiTouchController mMultiTouchController; 74 | @Override 75 | public void onCreate(Bundle savedInstanceState) { 76 | super.onCreate(savedInstanceState); 77 | //下面四条语句要在setContentView()方法之前调用,否则程序会出错,在本程序中,加入以下的语句貌似还没有什么影�?或�?有我还没发现 78 | this.requestWindowFeature(Window.FEATURE_LEFT_ICON); 79 | this.requestWindowFeature(Window.FEATURE_RIGHT_ICON); 80 | this.requestWindowFeature(Window.FEATURE_PROGRESS); 81 | this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 82 | 83 | setContentView(R.layout.main); 84 | initWebView(); 85 | performSearchIntent(getIntent()); 86 | 87 | 88 | } 89 | 90 | @Override 91 | protected void onNewIntent(Intent intent) { 92 | setIntent(intent); 93 | performSearchIntent(intent); 94 | } 95 | /** 96 | * 执行搜索,打�?���? */ 97 | private void performSearchIntent(Intent intent) { 98 | if(Intent.ACTION_SEARCH.equals(intent.getAction())){ 99 | String query = intent.getStringExtra(SearchManager.QUERY); 100 | //还没有对网址进行严格的解�? 101 | if(!query.startsWith("http://")){ 102 | mWebView.loadUrl("http://"+query); 103 | } 104 | if(query.startsWith("http://")){ 105 | mWebView.loadUrl(query); 106 | } 107 | /*else{ 108 | Toast.makeText(this, R.string.url_error, Toast.LENGTH_SHORT).show(); 109 | }*/ 110 | } 111 | } 112 | 113 | private void initWebView() { 114 | defaultGoogleUrl = this.getResources().getString(R.string.default_google_url); 115 | defaultUCWebUrl = this.getResources().getString(R.string.default_ucweb_url); 116 | setDefaultURL(); 117 | 118 | bar = (ProgressBar)findViewById(R.id.progress_bar); 119 | 120 | mMultiTouchController = new MultiTouchController(this,false); 121 | 122 | mWebView = (WebView)findViewById(R.id.webview); 123 | //使用mWebView的getSettings()方法设置支持JavaScript为true 124 | mWebView.getSettings().setJavaScriptEnabled(true); 125 | mWebView.setScrollBarStyle(0); 126 | //用loadUrl方法加载网址 127 | mWebView.loadUrl(defaultUrl); 128 | //对mWebView设置WebViewClient对象,如果不设置此对象那么当单击mWebView中的链接时将由系�? //默认的Browser来响应链接,即由默认的Browser打开链接,�?不是你自己写的Browser来打�? //故为了mWebView自己处理页面中的�?��链接,则要扩展WebViewClient类,重载shouldOverrideUrlLoading()方法 129 | mWebView.setWebViewClient(new MyWebViewClient()); 130 | // 131 | mWebView.setWebChromeClient(new MyWebChromeClient()); 132 | //实现下载监听 133 | mWebView.setDownloadListener(new DownloadListener(){ 134 | 135 | public void onDownloadStart(String url, String userAgent, String contentDisposition, 136 | String mimetype, long contentLength) { 137 | /*Uri uri = Uri.parse(url); 138 | Intent intent = new Intent(DOWNLOAD_ACTION,uri); 139 | startActivity(intent);*/ 140 | 141 | //downloadFile(url, mimetype, mimetype, mimetype, contentLength); 142 | ContentValues values = new ContentValues(); 143 | values.put(Downloads.COLUMN_URI, url); 144 | } 145 | 146 | }); 147 | } 148 | /* 149 | * 下列代码实现网络文件下载功能,目前下载的文件时存储在/data/data/org.badboy.browser/files目录下的�? * 后续还会更新这个方法,将其下载到SDCard上来。�?路是这样子的:下载文件时先棌查是否由sdcard或udisk外存储设备, 150 | * 若有则直接下载到外存储设备,否则存储�?data/data/org.badboy.browser/files目录中�?�?��还覠增加�?��功能:就�? * 列出已下载文件,以及对文件copy,delete等操作,下载进度条�? 151 | * 2011-3-4 152 | */ 153 | private void downloadFile(String url,String userAgent, String contentDisposition, 154 | String mimetype, long contentLength) { 155 | 156 | /*String filename = URLUtil.guessFileName(url, 157 | contentDisposition, mimetype); 158 | // Check to see if we have an SDCard 159 | String status = Environment.getExternalStorageState(); 160 | if (!status.equals(Environment.MEDIA_MOUNTED)) { 161 | int title; 162 | String msg; 163 | 164 | // Check to see if the SDCard is busy, same as the music app 165 | if (status.equals(Environment.MEDIA_SHARED)) { 166 | msg = getString(R.string.download_sdcard_busy_dlg_msg); 167 | title = R.string.download_sdcard_busy_dlg_title; 168 | } else { 169 | msg = getString(R.string.download_no_sdcard_dlg_msg, filename); 170 | title = R.string.download_no_sdcard_dlg_title; 171 | } 172 | 173 | new AlertDialog.Builder(this) 174 | .setTitle(title) 175 | .setIcon(android.R.drawable.ic_dialog_alert) 176 | .setMessage(msg) 177 | .setPositiveButton(R.string.ok, null) 178 | .show(); 179 | return; 180 | }*/ 181 | isDownload = true; 182 | 183 | 184 | try { 185 | String filename = URLUtil.guessFileName(url, 186 | contentDisposition, mimetype); 187 | URL url2 = new URL(url); 188 | HttpURLConnection conn = (HttpURLConnection) url2.openConnection(); 189 | conn.setDoInput(true); 190 | conn.connect(); 191 | if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { 192 | InputStream is = conn.getInputStream(); 193 | FileOutputStream fos = this.openFileOutput(filename, Context.MODE_APPEND); 194 | int len = 0; 195 | byte[] buf = new byte[1024]; 196 | while ((len = is.read(buf)) != -1) { 197 | fos.write(buf, 0, len); 198 | 199 | } 200 | is.close(); 201 | fos.close(); 202 | isDownload = false; 203 | } else { 204 | Toast.makeText(this, R.string.net_error, Toast.LENGTH_SHORT).show(); 205 | isDownload = false; 206 | } 207 | 208 | } catch (Exception e) { 209 | e.printStackTrace(); 210 | isDownload = false; 211 | } 212 | 213 | } 214 | 215 | private void setDefaultURL() { 216 | try { 217 | //android中使用openFileInput()方法得到文件输入�? 218 | FileInputStream fis = this.openFileInput(fileUrl); 219 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 220 | byte[] buffer = new byte[1024]; 221 | int len = 0; 222 | while((len = fis.read(buffer))!=-1){ 223 | baos.write(buffer, 0, len); 224 | } 225 | //defaultUrl = new String(buffer,0,len); 226 | defaultUrl = baos.toString(); 227 | fis.close(); 228 | baos.close(); 229 | } catch (Exception e) { 230 | e.printStackTrace(); 231 | defaultUrl = defaultGoogleUrl; 232 | } 233 | //如果还是为空(即文件中还没有存储默认网�?,则使用defaultUCWebUrl 234 | if(defaultUrl.equals("")){ 235 | defaultUrl = defaultGoogleUrl;//defaultUCWebUrl; 236 | } 237 | } 238 | /** 239 | * 按键事件处理 240 | */ 241 | @Override 242 | public boolean onKeyDown(int keyCode, KeyEvent event) { 243 | //按返回键可�?回之前浏览过的网�? 244 | if((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()){ 245 | mWebView.goBack(); 246 | return true; 247 | } 248 | //搜索按键 249 | if(keyCode == KeyEvent.KEYCODE_SEARCH){ 250 | onSearchRequested(); 251 | } 252 | return super.onKeyDown(keyCode, event); 253 | } 254 | /** 255 | * 重写WebChromeClient�? */ 256 | private class MyWebChromeClient extends WebChromeClient { 257 | //设置网页加载进度�? @Override 258 | public void onProgressChanged(WebView webview, int newProgress){ 259 | BrowserActivity.this.getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 100); 260 | 261 | bar.setProgress(newProgress); 262 | bar.setVisibility(View.VISIBLE); 263 | if(bar.getProgress()==100){ 264 | bar.setVisibility(View.GONE); 265 | } 266 | super.onProgressChanged(webview, newProgress); 267 | } 268 | } 269 | /** 270 | * 重写 WebViewClient�?由本Browser处理网页中的链接 271 | */ 272 | private class MyWebViewClient extends WebViewClient { 273 | 274 | @Override 275 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 276 | view.loadUrl(url); 277 | return true; 278 | } 279 | 280 | } 281 | /** 282 | * Menu 按键的添加和事件处理 283 | */ 284 | @Override 285 | public boolean onCreateOptionsMenu(Menu menu) { 286 | //打开网址输入�? 287 | menu.add(0,MENU_NEW_WEB,0,R.string.new_web_page) 288 | .setIcon(R.drawable.search_icon); 289 | //设置默认网址 290 | menu.add(0,MENU_SET_MAIN_PAGE,0,R.string.set_default_url) 291 | .setIcon(R.drawable.browser_icon); 292 | return super.onCreateOptionsMenu(menu); 293 | } 294 | @Override 295 | public boolean onMenuItemSelected(int featureId, MenuItem item) { 296 | switch(item.getItemId()){ 297 | case MENU_NEW_WEB: 298 | onSearchRequested(); 299 | return true; 300 | case MENU_SET_MAIN_PAGE: 301 | this.showDialog(SET_DEFAULT_URL_DIALOG); 302 | break; 303 | } 304 | return super.onMenuItemSelected(featureId, item); 305 | } 306 | 307 | /** 308 | * 重写onSearchRequested(),可实现调用Search Bar 309 | */ 310 | @Override 311 | public boolean onSearchRequested() { 312 | 313 | this.startSearch(null, false, null, false); 314 | return true; 315 | } 316 | /** 317 | * 当覠调用this.showDialog(int id)方法来显示Dialog时,要重写下列方�? */ 318 | @Override 319 | protected Dialog onCreateDialog(int id) { 320 | switch(id){ 321 | case PROGRESS_DIALOG: 322 | mDialog = new ProgressDialog(this); 323 | //mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 324 | mDialog.setMessage(getString(R.string.page_is_loading)); 325 | return mDialog; 326 | case SET_DEFAULT_URL_DIALOG: 327 | return createDefaultUrlDialog(); 328 | case DOWNLOAD_PROGRESS_DIALOG: 329 | return createDownloadProgress(); 330 | default: 331 | return super.onCreateDialog(id); 332 | } 333 | 334 | } 335 | private Dialog createDownloadProgress() { 336 | downloadProgress = new ProgressDialog(this); 337 | downloadProgress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 338 | downloadProgress.setMessage(getString(R.string.file_is_loading)); 339 | return downloadProgress; 340 | } 341 | 342 | private Dialog createDefaultUrlDialog() { 343 | final Dialog dialog = new Dialog(this); 344 | dialog.setContentView(R.layout.default_url_dialog); 345 | dialog.setTitle(R.string.dialog_title); 346 | final EditText url = (EditText)dialog.findViewById(R.id.url); 347 | Button btnOK = (Button)dialog.findViewById(R.id.ok_btn); 348 | btnOK.setOnClickListener(new View.OnClickListener() { 349 | 350 | public void onClick(View arg0) { 351 | defaultUrl = url.getText().toString(); 352 | try { 353 | FileOutputStream fos = BrowserActivity.this.openFileOutput(fileUrl, Context.MODE_PRIVATE); 354 | byte[] buffer = defaultUrl.getBytes(); 355 | fos.write(buffer); 356 | fos.close(); 357 | } catch (Exception e) { 358 | e.printStackTrace(); 359 | } 360 | dialog.dismiss(); 361 | mWebView.loadUrl(defaultUrl); 362 | } 363 | }); 364 | Button btnCancel = (Button)dialog.findViewById(R.id.cancel_btn); 365 | btnCancel.setOnClickListener(new View.OnClickListener() { 366 | 367 | public void onClick(View arg0) { 368 | dialog.dismiss(); 369 | } 370 | }); 371 | return dialog; 372 | } 373 | 374 | /** 375 | * 屏幕旋转时,程序会重新调用onCreate()方法,即重新加载程序。当不需要这么做时, 376 | * 可在manifest文件中的Activity标签中加入xndroid:configChanges="orientation|keyboardHidden" 377 | * 属�?,并且重写下列方法�?该方法中可以处理屏幕旋转时发生的动作 378 | */ 379 | @Override 380 | public void onConfigurationChanged(Configuration newConfig) { 381 | 382 | /*if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { 383 | 384 | } else if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { 385 | 386 | }*/ 387 | 388 | super.onConfigurationChanged(newConfig); 389 | } 390 | 391 | //-------------multitouch stuff ------------------------------------ 392 | 393 | private int mCurrentZoom = 0; 394 | private static final double ZOOM_SENSITIVITY = 1.6; 395 | private static final float ZOOM_LOG_BASE_INV = 1.0f / (float) Math.log(2.0 / ZOOM_SENSITIVITY); 396 | private boolean isMultiTouchScale = false; 397 | @Override 398 | public boolean dispatchTouchEvent(MotionEvent event) { 399 | if (mMultiTouchController.onTouchEvent(event)) { 400 | if (!isMultiTouchScale) { 401 | event.setAction(MotionEvent.ACTION_CANCEL); 402 | super.dispatchTouchEvent(event); 403 | } 404 | }else{ 405 | isMultiTouchScale = true; 406 | if (super.dispatchTouchEvent(event)) { 407 | return true; 408 | } 409 | return false; 410 | } 411 | return true; 412 | } 413 | public Object getDraggableObjectAtPoint(PointInfo touchPoint) { 414 | return new Object(); 415 | } 416 | 417 | public void getPositionAndScale(Object obj, 418 | PositionAndScale objPosAndScaleOut) { 419 | objPosAndScaleOut.set(0.0f, 0.0f, true, 1.0f, false, 0.0f, 0.0f, false, 0.0f); 420 | mCurrentZoom = 0; 421 | } 422 | 423 | public void selectObject(Object obj, PointInfo touchPoint) { 424 | 425 | } 426 | 427 | public boolean setPositionAndScale(Object obj, 428 | PositionAndScale newObjPosAndScale, PointInfo touchPoint) { 429 | float newRelativeScale = newObjPosAndScale.getScale(); 430 | int targetZoom = (int) Math.round(Math.log(newRelativeScale) 431 | * ZOOM_LOG_BASE_INV); 432 | while (mCurrentZoom > targetZoom) { 433 | mCurrentZoom--; 434 | mWebView.zoomOut(); 435 | } 436 | while (mCurrentZoom < targetZoom) { 437 | mCurrentZoom++; 438 | mWebView.zoomIn(); 439 | } 440 | return true; 441 | } 442 | 443 | } -------------------------------------------------------------------------------- /src/org/badboy/browser/Downloads.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.badboy.browser; 18 | 19 | import android.net.Uri; 20 | import android.provider.BaseColumns; 21 | 22 | /** 23 | * Exposes constants used to interact with the download manager's 24 | * content provider. 25 | * The constants URI ... STATUS are the names of columns in the downloads table. 26 | * 27 | * @hide 28 | */ 29 | // For 1.0 the download manager can't deal with abuse from untrusted apps, so 30 | // this API is hidden. 31 | public final class Downloads implements BaseColumns { 32 | private Downloads() {} 33 | 34 | /** 35 | * The permission to access the download manager 36 | */ 37 | public static final String PERMISSION_ACCESS = "android.permission.ACCESS_DOWNLOAD_MANAGER"; 38 | 39 | /** 40 | * The permission to access the download manager's advanced functions 41 | */ 42 | public static final String PERMISSION_ACCESS_ADVANCED = 43 | "android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED"; 44 | 45 | /** 46 | * The permission to directly access the download manager's cache directory 47 | */ 48 | public static final String PERMISSION_CACHE = "android.permission.ACCESS_CACHE_FILESYSTEM"; 49 | 50 | /** 51 | * The permission to send broadcasts on download completion 52 | */ 53 | public static final String PERMISSION_SEND_INTENTS = 54 | "android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS"; 55 | 56 | /** 57 | * The content:// URI for the data table in the provider 58 | */ 59 | public static final Uri CONTENT_URI = 60 | Uri.parse("content://downloads/download"); 61 | 62 | /** 63 | * Broadcast Action: this is sent by the download manager to the app 64 | * that had initiated a download when that download completes. The 65 | * download's content: uri is specified in the intent's data. 66 | */ 67 | public static final String ACTION_DOWNLOAD_COMPLETED = 68 | "android.intent.action.DOWNLOAD_COMPLETED"; 69 | 70 | /** 71 | * Broadcast Action: this is sent by the download manager to the app 72 | * that had initiated a download when the user selects the notification 73 | * associated with that download. The download's content: uri is specified 74 | * in the intent's data if the click is associated with a single download, 75 | * or Downloads.CONTENT_URI if the notification is associated with 76 | * multiple downloads. 77 | * Note: this is not currently sent for downloads that have completed 78 | * successfully. 79 | */ 80 | public static final String ACTION_NOTIFICATION_CLICKED = 81 | "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"; 82 | 83 | /** 84 | * The name of the column containing the URI of the data being downloaded. 85 | * Type: TEXT 86 | * Owner can Init/Read 87 | */ 88 | public static final String COLUMN_URI = "uri"; 89 | 90 | /** 91 | * The name of the column containing application-specific data. 92 | * Type: TEXT 93 | * Owner can Init/Read/Write 94 | */ 95 | public static final String COLUMN_APP_DATA = "entity"; 96 | 97 | /** 98 | * The name of the column containing the flags that indicates whether 99 | * the initiating application is capable of verifying the integrity of 100 | * the downloaded file. When this flag is set, the download manager 101 | * performs downloads and reports success even in some situations where 102 | * it can't guarantee that the download has completed (e.g. when doing 103 | * a byte-range request without an ETag, or when it can't determine 104 | * whether a download fully completed). 105 | * Type: BOOLEAN 106 | * Owner can Init 107 | */ 108 | public static final String COLUMN_NO_INTEGRITY = "no_integrity"; 109 | 110 | /** 111 | * The name of the column containing the filename that the initiating 112 | * application recommends. When possible, the download manager will attempt 113 | * to use this filename, or a variation, as the actual name for the file. 114 | * Type: TEXT 115 | * Owner can Init 116 | */ 117 | public static final String COLUMN_FILE_NAME_HINT = "hint"; 118 | 119 | /** 120 | * The name of the column containing the filename where the downloaded data 121 | * was actually stored. 122 | * Type: TEXT 123 | * Owner can Read 124 | */ 125 | public static final String _DATA = "_data"; 126 | 127 | /** 128 | * The name of the column containing the MIME type of the downloaded data. 129 | * Type: TEXT 130 | * Owner can Init/Read 131 | */ 132 | public static final String COLUMN_MIME_TYPE = "mimetype"; 133 | 134 | /** 135 | * The name of the column containing the flag that controls the destination 136 | * of the download. See the DESTINATION_* constants for a list of legal values. 137 | * Type: INTEGER 138 | * Owner can Init 139 | */ 140 | public static final String COLUMN_DESTINATION = "destination"; 141 | 142 | /** 143 | * The name of the column containing the flags that controls whether the 144 | * download is displayed by the UI. See the VISIBILITY_* constants for 145 | * a list of legal values. 146 | * Type: INTEGER 147 | * Owner can Init/Read/Write 148 | */ 149 | public static final String COLUMN_VISIBILITY = "visibility"; 150 | 151 | /** 152 | * The name of the column containing the current control state of the download. 153 | * Applications can write to this to control (pause/resume) the download. 154 | * the CONTROL_* constants for a list of legal values. 155 | * Type: INTEGER 156 | * Owner can Read 157 | */ 158 | public static final String COLUMN_CONTROL = "control"; 159 | 160 | /** 161 | * The name of the column containing the current status of the download. 162 | * Applications can read this to follow the progress of each download. See 163 | * the STATUS_* constants for a list of legal values. 164 | * Type: INTEGER 165 | * Owner can Read 166 | */ 167 | public static final String COLUMN_STATUS = "status"; 168 | 169 | /** 170 | * The name of the column containing the date at which some interesting 171 | * status changed in the download. Stored as a System.currentTimeMillis() 172 | * value. 173 | * Type: BIGINT 174 | * Owner can Read 175 | */ 176 | public static final String COLUMN_LAST_MODIFICATION = "lastmod"; 177 | 178 | /** 179 | * The name of the column containing the package name of the application 180 | * that initiating the download. The download manager will send 181 | * notifications to a component in this package when the download completes. 182 | * Type: TEXT 183 | * Owner can Init/Read 184 | */ 185 | public static final String COLUMN_NOTIFICATION_PACKAGE = "notificationpackage"; 186 | 187 | /** 188 | * The name of the column containing the component name of the class that 189 | * will receive notifications associated with the download. The 190 | * package/class combination is passed to 191 | * Intent.setClassName(String,String). 192 | * Type: TEXT 193 | * Owner can Init/Read 194 | */ 195 | public static final String COLUMN_NOTIFICATION_CLASS = "notificationclass"; 196 | 197 | /** 198 | * If extras are specified when requesting a download they will be provided in the intent that 199 | * is sent to the specified class and package when a download has finished. 200 | * Type: TEXT 201 | * Owner can Init 202 | */ 203 | public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras"; 204 | 205 | /** 206 | * The name of the column contain the values of the cookie to be used for 207 | * the download. This is used directly as the value for the Cookie: HTTP 208 | * header that gets sent with the request. 209 | * Type: TEXT 210 | * Owner can Init 211 | */ 212 | public static final String COLUMN_COOKIE_DATA = "cookiedata"; 213 | 214 | /** 215 | * The name of the column containing the user agent that the initiating 216 | * application wants the download manager to use for this download. 217 | * Type: TEXT 218 | * Owner can Init 219 | */ 220 | public static final String COLUMN_USER_AGENT = "useragent"; 221 | 222 | /** 223 | * The name of the column containing the referer (sic) that the initiating 224 | * application wants the download manager to use for this download. 225 | * Type: TEXT 226 | * Owner can Init 227 | */ 228 | public static final String COLUMN_REFERER = "referer"; 229 | 230 | /** 231 | * The name of the column containing the total size of the file being 232 | * downloaded. 233 | * Type: INTEGER 234 | * Owner can Read 235 | */ 236 | public static final String COLUMN_TOTAL_BYTES = "total_bytes"; 237 | 238 | /** 239 | * The name of the column containing the size of the part of the file that 240 | * has been downloaded so far. 241 | * Type: INTEGER 242 | * Owner can Read 243 | */ 244 | public static final String COLUMN_CURRENT_BYTES = "current_bytes"; 245 | 246 | /** 247 | * The name of the column where the initiating application can provide the 248 | * UID of another application that is allowed to access this download. If 249 | * multiple applications share the same UID, all those applications will be 250 | * allowed to access this download. This column can be updated after the 251 | * download is initiated. This requires the permission 252 | * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED. 253 | * Type: INTEGER 254 | * Owner can Init 255 | */ 256 | public static final String COLUMN_OTHER_UID = "otheruid"; 257 | 258 | /** 259 | * The name of the column where the initiating application can provided the 260 | * title of this download. The title will be displayed ito the user in the 261 | * list of downloads. 262 | * Type: TEXT 263 | * Owner can Init/Read/Write 264 | */ 265 | public static final String COLUMN_TITLE = "title"; 266 | 267 | /** 268 | * The name of the column where the initiating application can provide the 269 | * description of this download. The description will be displayed to the 270 | * user in the list of downloads. 271 | * Type: TEXT 272 | * Owner can Init/Read/Write 273 | */ 274 | public static final String COLUMN_DESCRIPTION = "description"; 275 | 276 | /* 277 | * Lists the destinations that an application can specify for a download. 278 | */ 279 | 280 | /** 281 | * This download will be saved to the external storage. This is the 282 | * default behavior, and should be used for any file that the user 283 | * can freely access, copy, delete. Even with that destination, 284 | * unencrypted DRM files are saved in secure internal storage. 285 | * Downloads to the external destination only write files for which 286 | * there is a registered handler. The resulting files are accessible 287 | * by filename to all applications. 288 | */ 289 | public static final int DESTINATION_EXTERNAL = 0; 290 | 291 | /** 292 | * This download will be saved to the download manager's private 293 | * partition. This is the behavior used by applications that want to 294 | * download private files that are used and deleted soon after they 295 | * get downloaded. All file types are allowed, and only the initiating 296 | * application can access the file (indirectly through a content 297 | * provider). This requires the 298 | * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission. 299 | */ 300 | public static final int DESTINATION_CACHE_PARTITION = 1; 301 | 302 | /** 303 | * This download will be saved to the download manager's private 304 | * partition and will be purged as necessary to make space. This is 305 | * for private files (similar to CACHE_PARTITION) that aren't deleted 306 | * immediately after they are used, and are kept around by the download 307 | * manager as long as space is available. 308 | */ 309 | public static final int DESTINATION_CACHE_PARTITION_PURGEABLE = 2; 310 | 311 | /** 312 | * This download will be saved to the download manager's private 313 | * partition, as with DESTINATION_CACHE_PARTITION, but the download 314 | * will not proceed if the user is on a roaming data connection. 315 | */ 316 | public static final int DESTINATION_CACHE_PARTITION_NOROAMING = 3; 317 | 318 | /** 319 | * This download is allowed to run. 320 | */ 321 | public static final int CONTROL_RUN = 0; 322 | 323 | /** 324 | * This download must pause at the first opportunity. 325 | */ 326 | public static final int CONTROL_PAUSED = 1; 327 | 328 | /* 329 | * Lists the states that the download manager can set on a download 330 | * to notify applications of the download progress. 331 | * The codes follow the HTTP families: 332 | * 1xx: informational 333 | * 2xx: success 334 | * 3xx: redirects (not used by the download manager) 335 | * 4xx: client errors 336 | * 5xx: server errors 337 | */ 338 | 339 | /** 340 | * Returns whether the status is informational (i.e. 1xx). 341 | */ 342 | public static boolean isStatusInformational(int status) { 343 | return (status >= 100 && status < 200); 344 | } 345 | 346 | /** 347 | * Returns whether the download is suspended. (i.e. whether the download 348 | * won't complete without some action from outside the download 349 | * manager). 350 | */ 351 | public static boolean isStatusSuspended(int status) { 352 | return (status == STATUS_PENDING_PAUSED || status == STATUS_RUNNING_PAUSED); 353 | } 354 | 355 | /** 356 | * Returns whether the status is a success (i.e. 2xx). 357 | */ 358 | public static boolean isStatusSuccess(int status) { 359 | return (status >= 200 && status < 300); 360 | } 361 | 362 | /** 363 | * Returns whether the status is an error (i.e. 4xx or 5xx). 364 | */ 365 | public static boolean isStatusError(int status) { 366 | return (status >= 400 && status < 600); 367 | } 368 | 369 | /** 370 | * Returns whether the status is a client error (i.e. 4xx). 371 | */ 372 | public static boolean isStatusClientError(int status) { 373 | return (status >= 400 && status < 500); 374 | } 375 | 376 | /** 377 | * Returns whether the status is a server error (i.e. 5xx). 378 | */ 379 | public static boolean isStatusServerError(int status) { 380 | return (status >= 500 && status < 600); 381 | } 382 | 383 | /** 384 | * Returns whether the download has completed (either with success or 385 | * error). 386 | */ 387 | public static boolean isStatusCompleted(int status) { 388 | return (status >= 200 && status < 300) || (status >= 400 && status < 600); 389 | } 390 | 391 | /** 392 | * This download hasn't stated yet 393 | */ 394 | public static final int STATUS_PENDING = 190; 395 | 396 | /** 397 | * This download hasn't stated yet and is paused 398 | */ 399 | public static final int STATUS_PENDING_PAUSED = 191; 400 | 401 | /** 402 | * This download has started 403 | */ 404 | public static final int STATUS_RUNNING = 192; 405 | 406 | /** 407 | * This download has started and is paused 408 | */ 409 | public static final int STATUS_RUNNING_PAUSED = 193; 410 | 411 | /** 412 | * This download has successfully completed. 413 | * Warning: there might be other status values that indicate success 414 | * in the future. 415 | * Use isSucccess() to capture the entire category. 416 | */ 417 | public static final int STATUS_SUCCESS = 200; 418 | 419 | /** 420 | * This request couldn't be parsed. This is also used when processing 421 | * requests with unknown/unsupported URI schemes. 422 | */ 423 | public static final int STATUS_BAD_REQUEST = 400; 424 | 425 | /** 426 | * This download can't be performed because the content type cannot be 427 | * handled. 428 | */ 429 | public static final int STATUS_NOT_ACCEPTABLE = 406; 430 | 431 | /** 432 | * This download cannot be performed because the length cannot be 433 | * determined accurately. This is the code for the HTTP error "Length 434 | * Required", which is typically used when making requests that require 435 | * a content length but don't have one, and it is also used in the 436 | * client when a response is received whose length cannot be determined 437 | * accurately (therefore making it impossible to know when a download 438 | * completes). 439 | */ 440 | public static final int STATUS_LENGTH_REQUIRED = 411; 441 | 442 | /** 443 | * This download was interrupted and cannot be resumed. 444 | * This is the code for the HTTP error "Precondition Failed", and it is 445 | * also used in situations where the client doesn't have an ETag at all. 446 | */ 447 | public static final int STATUS_PRECONDITION_FAILED = 412; 448 | 449 | /** 450 | * This download was canceled 451 | */ 452 | public static final int STATUS_CANCELED = 490; 453 | 454 | /** 455 | * This download has completed with an error. 456 | * Warning: there will be other status values that indicate errors in 457 | * the future. Use isStatusError() to capture the entire category. 458 | */ 459 | public static final int STATUS_UNKNOWN_ERROR = 491; 460 | 461 | /** 462 | * This download couldn't be completed because of a storage issue. 463 | * Typically, that's because the filesystem is missing or full. 464 | */ 465 | public static final int STATUS_FILE_ERROR = 492; 466 | 467 | /** 468 | * This download couldn't be completed because of an HTTP 469 | * redirect response that the download manager couldn't 470 | * handle. 471 | */ 472 | public static final int STATUS_UNHANDLED_REDIRECT = 493; 473 | 474 | /** 475 | * This download couldn't be completed because of an 476 | * unspecified unhandled HTTP code. 477 | */ 478 | public static final int STATUS_UNHANDLED_HTTP_CODE = 494; 479 | 480 | /** 481 | * This download couldn't be completed because of an 482 | * error receiving or processing data at the HTTP level. 483 | */ 484 | public static final int STATUS_HTTP_DATA_ERROR = 495; 485 | 486 | /** 487 | * This download couldn't be completed because of an 488 | * HttpException while setting up the request. 489 | */ 490 | public static final int STATUS_HTTP_EXCEPTION = 496; 491 | 492 | /** 493 | * This download couldn't be completed because there were 494 | * too many redirects. 495 | */ 496 | public static final int STATUS_TOO_MANY_REDIRECTS = 497; 497 | 498 | /** 499 | * This download is visible but only shows in the notifications 500 | * while it's in progress. 501 | */ 502 | public static final int VISIBILITY_VISIBLE = 0; 503 | 504 | /** 505 | * This download is visible and shows in the notifications while 506 | * in progress and after completion. 507 | */ 508 | public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1; 509 | 510 | /** 511 | * This download doesn't show in the UI or in the notifications. 512 | */ 513 | public static final int VISIBILITY_HIDDEN = 2; 514 | } 515 | -------------------------------------------------------------------------------- /src/org/metalev/multitouch/controller/MultiTouchController.java: -------------------------------------------------------------------------------- 1 | package org.metalev.multitouch.controller; 2 | 3 | /** 4 | * MultiTouchController.java 5 | * 6 | * Author: Luke Hutchison (luke.hutch@mit.edu) 7 | * Please drop me an email if you use this code so I can list your project here! 8 | * 9 | * Usage: 10 | * 11 | * public class MyMTView extends View implements MultiTouchObjectCanvas { 12 | * 13 | * private MultiTouchController multiTouchController = new MultiTouchController(this); 14 | * 15 | * // Pass touch events to the MT controller 16 | * public boolean onTouchEvent(MotionEvent event) { 17 | * return multiTouchController.onTouchEvent(event); 18 | * } 19 | * 20 | * // ... then implement the MultiTouchObjectCanvas interface here, see details in the comments of that interface. 21 | * } 22 | * 23 | * 24 | * Changelog: 25 | * 2010-06-09 v1.5.1 Some API changes to make it possible to selectively update or not update scale / rotation. 26 | * Fixed anisotropic zoom. Cleaned up rotation code. Added more comments. Better var names. (LH) 27 | * 2010-06-09 v1.4 Added ability to track pinch rotation (Mickael Despesse, author of "Face Frenzy") and anisotropic pinch-zoom (LH) 28 | * 2010-06-09 v1.3.3 Bugfixes for Android-2.1; added optional debug info (LH) 29 | * 2010-06-09 v1.3 Ported to Android-2.2 (handle ACTION_POINTER_* actions); fixed several bugs; refactoring; documentation (LH) 30 | * 2010-05-17 v1.2.1 Dual-licensed under Apache and GPL licenses 31 | * 2010-02-18 v1.2 Support for compilation under Android 1.5/1.6 using introspection (mmin, author of handyCalc) 32 | * 2010-01-08 v1.1.1 Bugfixes to Cyanogen's patch that only showed up in more complex uses of controller (LH) 33 | * 2010-01-06 v1.1 Modified for official level 5 MT API (Cyanogen) 34 | * 2009-01-25 v1.0 Original MT controller, released for hacked G1 kernel (LH) 35 | * 36 | * Planned features: 37 | * - Add inertia (flick-pinch-zoom or flick-scroll) 38 | * 39 | * Known usages: 40 | * - Mickael Despesse's "Face Frenzy" face distortion app, to be published to the Market soon 41 | * - Yuan Chin's fork of ADW Launcher to support multitouch 42 | * - David Byrne's fractal viewing app Fractoid 43 | * - mmin's handyCalc calculator 44 | * - My own "MultiTouch Visualizer 2" in the Market 45 | * - Formerly: The browser in cyanogenmod (and before that, JesusFreke), and other firmwares like dwang5. This usage has been 46 | * replaced with official pinch/zoom in Maps, Browser and Gallery[3D] as of API level 5. 47 | * 48 | * License: 49 | * Dual-licensed under the Apache License v2 and the GPL v2. 50 | */ 51 | 52 | import java.lang.reflect.Method; 53 | 54 | import android.util.Log; 55 | import android.view.MotionEvent; 56 | 57 | /** 58 | * A class that simplifies the implementation of multitouch in applications. Subclass this and read the fields here as needed in subclasses. 59 | * 60 | * @author Luke Hutchison 61 | */ 62 | public class MultiTouchController { 63 | 64 | /** 65 | * Time in ms required after a change in event status (e.g. putting down or lifting off the second finger) before events actually do anything -- 66 | * helps eliminate noisy jumps that happen on change of status 67 | */ 68 | private static final long EVENT_SETTLE_TIME_INTERVAL = 20; 69 | 70 | /** 71 | * The biggest possible abs val of the change in x or y between multitouch events (larger dx/dy events are ignored) -- helps eliminate jumps in 72 | * pointer position on finger 2 up/down. 73 | */ 74 | private static final float MAX_MULTITOUCH_POS_JUMP_SIZE = 30.0f; 75 | 76 | /** 77 | * The biggest possible abs val of the change in multitouchWidth or multitouchHeight between multitouch events (larger-jump events are ignored) -- 78 | * helps eliminate jumps in pointer position on finger 2 up/down. 79 | */ 80 | private static final float MAX_MULTITOUCH_DIM_JUMP_SIZE = 40.0f; 81 | 82 | /** The smallest possible distance between multitouch points (used to avoid div-by-zero errors and display glitches) */ 83 | private static final float MIN_MULTITOUCH_SEPARATION = 30.0f; 84 | 85 | /** The max number of touch points that can be present on the screen at once */ 86 | public static final int MAX_TOUCH_POINTS = 10; 87 | 88 | /** Generate tons of log entries for debugging */ 89 | private static final boolean DEBUG = false; 90 | 91 | // ---------------------------------------------------------------------------------------------------------------------- 92 | 93 | MultiTouchObjectCanvas objectCanvas; 94 | 95 | /** The current touch point */ 96 | private PointInfo mCurrPt; 97 | 98 | /** The previous touch point */ 99 | private PointInfo mPrevPt; 100 | 101 | /** Fields extracted from mCurrPt */ 102 | private float mCurrPtX, mCurrPtY, mCurrPtDiam, mCurrPtWidth, mCurrPtHeight, mCurrPtAng; 103 | 104 | /** 105 | * Extract fields from mCurrPt, respecting the update* fields of mCurrPt. This just avoids code duplication. I hate that Java doesn't support 106 | * higher-order functions, tuples or multiple return values from functions. 107 | */ 108 | private void extractCurrPtInfo() { 109 | // Get new drag/pinch params. Only read multitouch fields that are needed, 110 | // to avoid unnecessary computation (diameter and angle are expensive operations). 111 | mCurrPtX = mCurrPt.getX(); 112 | mCurrPtY = mCurrPt.getY(); 113 | mCurrPtDiam = Math.max(MIN_MULTITOUCH_SEPARATION * .71f, !mCurrXform.updateScale ? 0.0f : mCurrPt.getMultiTouchDiameter()); 114 | mCurrPtWidth = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchWidth()); 115 | mCurrPtHeight = Math.max(MIN_MULTITOUCH_SEPARATION, !mCurrXform.updateScaleXY ? 0.0f : mCurrPt.getMultiTouchHeight()); 116 | mCurrPtAng = !mCurrXform.updateAngle ? 0.0f : mCurrPt.getMultiTouchAngle(); 117 | } 118 | 119 | // ---------------------------------------------------------------------------------------------------------------------- 120 | 121 | /** Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses */ 122 | private boolean handleSingleTouchEvents; 123 | 124 | /** The object being dragged/stretched */ 125 | private T selectedObject = null; 126 | 127 | /** Current position and scale of the dragged object */ 128 | private PositionAndScale mCurrXform = new PositionAndScale(); 129 | 130 | /** Drag/pinch start time and time to ignore spurious events until (to smooth over event noise) */ 131 | private long mSettleStartTime, mSettleEndTime; 132 | 133 | /** Conversion from object coords to screen coords */ 134 | private float startPosX, startPosY; 135 | 136 | /** Conversion between scale and width, and object angle and start pinch angle */ 137 | private float startScaleOverPinchDiam, startAngleMinusPinchAngle; 138 | 139 | /** Conversion between X scale and width, and Y scale and height */ 140 | private float startScaleXOverPinchWidth, startScaleYOverPinchHeight; 141 | 142 | // ---------------------------------------------------------------------------------------------------------------------- 143 | 144 | /** No touch points down. */ 145 | private static final int MODE_NOTHING = 0; 146 | 147 | /** One touch point down, dragging an object. */ 148 | private static final int MODE_DRAG = 1; 149 | 150 | /** Two or more touch points down, stretching/rotating an object using the first two touch points. */ 151 | private static final int MODE_PINCH = 2; 152 | 153 | /** Current drag mode */ 154 | private int mMode = MODE_NOTHING; 155 | 156 | // ---------------------------------------------------------------------------------------------------------------------- 157 | 158 | /** Constructor that sets handleSingleTouchEvents to true */ 159 | public MultiTouchController(MultiTouchObjectCanvas objectCanvas) { 160 | this(objectCanvas, true); 161 | } 162 | 163 | /** Full constructor */ 164 | public MultiTouchController(MultiTouchObjectCanvas objectCanvas, boolean handleSingleTouchEvents) { 165 | this.mCurrPt = new PointInfo(); 166 | this.mPrevPt = new PointInfo(); 167 | this.handleSingleTouchEvents = handleSingleTouchEvents; 168 | this.objectCanvas = objectCanvas; 169 | } 170 | 171 | // ------------------------------------------------------------------------------------ 172 | 173 | /** 174 | * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true 175 | */ 176 | protected void setHandleSingleTouchEvents(boolean handleSingleTouchEvents) { 177 | this.handleSingleTouchEvents = handleSingleTouchEvents; 178 | } 179 | 180 | /** 181 | * Whether to handle single-touch events/drags before multi-touch is initiated or not; if not, they are handled by subclasses. Default: true 182 | */ 183 | protected boolean getHandleSingleTouchEvents() { 184 | return handleSingleTouchEvents; 185 | } 186 | 187 | // ------------------------------------------------------------------------------------ 188 | 189 | public static final boolean multiTouchSupported; 190 | private static Method m_getPointerCount; 191 | private static Method m_findPointerIndex; 192 | private static Method m_getPressure; 193 | private static Method m_getHistoricalX; 194 | private static Method m_getHistoricalY; 195 | private static Method m_getHistoricalPressure; 196 | private static Method m_getX; 197 | private static Method m_getY; 198 | private static int ACTION_POINTER_UP = 6; 199 | private static int ACTION_POINTER_INDEX_SHIFT = 8; 200 | 201 | static { 202 | boolean succeeded = false; 203 | try { 204 | // Android 2.0.1 stuff: 205 | m_getPointerCount = MotionEvent.class.getMethod("getPointerCount"); 206 | m_findPointerIndex = MotionEvent.class.getMethod("findPointerIndex", Integer.TYPE); 207 | m_getPressure = MotionEvent.class.getMethod("getPressure", Integer.TYPE); 208 | m_getHistoricalX = MotionEvent.class.getMethod("getHistoricalX", Integer.TYPE, Integer.TYPE); 209 | m_getHistoricalY = MotionEvent.class.getMethod("getHistoricalY", Integer.TYPE, Integer.TYPE); 210 | m_getHistoricalPressure = MotionEvent.class.getMethod("getHistoricalPressure", Integer.TYPE, Integer.TYPE); 211 | m_getX = MotionEvent.class.getMethod("getX", Integer.TYPE); 212 | m_getY = MotionEvent.class.getMethod("getY", Integer.TYPE); 213 | succeeded = true; 214 | } catch (Exception e) { 215 | Log.e("MultiTouchController", "static initializer failed", e); 216 | } 217 | multiTouchSupported = succeeded; 218 | if (multiTouchSupported) { 219 | // Android 2.2+ stuff (the original Android 2.2 consts are declared above, 220 | // and these actions aren't used previous to Android 2.2): 221 | try { 222 | ACTION_POINTER_UP = MotionEvent.class.getField("ACTION_POINTER_UP").getInt(null); 223 | ACTION_POINTER_INDEX_SHIFT = MotionEvent.class.getField("ACTION_POINTER_INDEX_SHIFT").getInt(null); 224 | } catch (Exception e) { 225 | } 226 | } 227 | } 228 | 229 | // ------------------------------------------------------------------------------------ 230 | 231 | private static final float[] xVals = new float[MAX_TOUCH_POINTS]; 232 | private static final float[] yVals = new float[MAX_TOUCH_POINTS]; 233 | private static final float[] pressureVals = new float[MAX_TOUCH_POINTS]; 234 | private static final int[] pointerIdxs = new int[MAX_TOUCH_POINTS]; 235 | 236 | /** Process incoming touch events */ 237 | public boolean onTouchEvent(MotionEvent event) { 238 | try { 239 | int pointerCount = multiTouchSupported ? (Integer) m_getPointerCount.invoke(event) : 1; 240 | if (DEBUG) 241 | Log.i("MultiTouch", "Got here 1 - " + multiTouchSupported + " " + mMode + " " + handleSingleTouchEvents + " " + pointerCount); 242 | if (mMode == MODE_NOTHING && !handleSingleTouchEvents && pointerCount == 1) 243 | // Not handling initial single touch events, just pass them on 244 | return false; 245 | if (DEBUG) 246 | Log.i("MultiTouch", "Got here 2"); 247 | 248 | // Handle history first (we sometimes get history with ACTION_MOVE events) 249 | int action = event.getAction(); 250 | int histLen = event.getHistorySize() / pointerCount; 251 | for (int histIdx = 0; histIdx <= histLen; histIdx++) { 252 | // Read from history entries until histIdx == histLen, then read from current event 253 | boolean processingHist = histIdx < histLen; 254 | if (!multiTouchSupported || pointerCount == 1) { 255 | // Use single-pointer methods -- these are needed as a special case (for some weird reason) even if 256 | // multitouch is supported but there's only one touch point down currently -- event.getX(0) etc. throw 257 | // an exception if there's only one point down. 258 | if (DEBUG) 259 | Log.i("MultiTouch", "Got here 3"); 260 | xVals[0] = processingHist ? event.getHistoricalX(histIdx) : event.getX(); 261 | yVals[0] = processingHist ? event.getHistoricalY(histIdx) : event.getY(); 262 | pressureVals[0] = processingHist ? event.getHistoricalPressure(histIdx) : event.getPressure(); 263 | } else { 264 | // Read x, y and pressure of each pointer 265 | if (DEBUG) 266 | Log.i("MultiTouch", "Got here 4"); 267 | int numPointers = Math.min(pointerCount, MAX_TOUCH_POINTS); 268 | for (int i = 0; i < numPointers; i++) { 269 | int ptrIdx = (Integer) m_findPointerIndex.invoke(event, i); 270 | pointerIdxs[i] = ptrIdx; 271 | // N.B. if pointerCount == 1, then the following methods throw an array index out of range exception, 272 | // and the code above is therefore required not just for Android 1.5/1.6 but also for when there is 273 | // only one touch point on the screen -- pointlessly inconsistent :( 274 | xVals[i] = (Float) (processingHist ? m_getHistoricalX.invoke(event, ptrIdx, histIdx) : m_getX.invoke(event, ptrIdx)); 275 | yVals[i] = (Float) (processingHist ? m_getHistoricalY.invoke(event, ptrIdx, histIdx) : m_getY.invoke(event, ptrIdx)); 276 | pressureVals[i] = (Float) (processingHist ? m_getHistoricalPressure.invoke(event, ptrIdx, histIdx) : m_getPressure.invoke( 277 | event, ptrIdx)); 278 | } 279 | } 280 | // Decode event 281 | decodeTouchEvent(pointerCount, xVals, yVals, pressureVals, pointerIdxs, // 282 | /* action = */processingHist ? MotionEvent.ACTION_MOVE : action, // 283 | /* down = */processingHist ? true : action != MotionEvent.ACTION_UP // 284 | && (action & ((1 << ACTION_POINTER_INDEX_SHIFT) - 1)) != ACTION_POINTER_UP // 285 | && action != MotionEvent.ACTION_CANCEL, // 286 | processingHist ? event.getHistoricalEventTime(histIdx) : event.getEventTime()); 287 | } 288 | 289 | return true; 290 | } catch (Exception e) { 291 | // In case any of the introspection stuff fails (it shouldn't) 292 | Log.e("MultiTouchController", "onTouchEvent() failed", e); 293 | return false; 294 | } 295 | } 296 | 297 | private void decodeTouchEvent(int pointerCount, float[] x, float[] y, float[] pressure, int[] pointerIdxs, int action, boolean down, 298 | long eventTime) { 299 | if (DEBUG) 300 | Log.i("MultiTouch", "Got here 5 - " + pointerCount + " " + action + " " + down); 301 | 302 | // Swap curr/prev points 303 | PointInfo tmp = mPrevPt; 304 | mPrevPt = mCurrPt; 305 | mCurrPt = tmp; 306 | // Overwrite old prev point 307 | mCurrPt.set(pointerCount, x, y, pressure, pointerIdxs, action, down, eventTime); 308 | multiTouchController(); 309 | } 310 | 311 | // ------------------------------------------------------------------------------------ 312 | 313 | /** Start dragging/pinching, or reset drag/pinch to current point if something goes out of range */ 314 | private void anchorAtThisPositionAndScale() { 315 | if (selectedObject == null) 316 | return; 317 | 318 | // Get selected object's current position and scale 319 | objectCanvas.getPositionAndScale(selectedObject, mCurrXform); 320 | 321 | // Figure out the object coords of the drag start point's screen coords. 322 | // All stretching should be around this point in object-coord-space. 323 | // Also figure out out ratio between object scale factor and multitouch 324 | // diameter at beginning of drag; same for angle and optional anisotropic 325 | // scale. 326 | float currScaleInv = 1.0f / (!mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale); 327 | extractCurrPtInfo(); 328 | startPosX = (mCurrPtX - mCurrXform.xOff) * currScaleInv; 329 | startPosY = (mCurrPtY - mCurrXform.yOff) * currScaleInv; 330 | startScaleOverPinchDiam = mCurrXform.scale / mCurrPtDiam; 331 | startScaleXOverPinchWidth = mCurrXform.scaleX / mCurrPtWidth; 332 | startScaleYOverPinchHeight = mCurrXform.scaleY / mCurrPtHeight; 333 | startAngleMinusPinchAngle = mCurrXform.angle - mCurrPtAng; 334 | } 335 | 336 | /** Drag/stretch/rotate the selected object using the current touch position(s) relative to the anchor position(s). */ 337 | private void performDragOrPinch() { 338 | // Don't do anything if we're not dragging anything 339 | if (selectedObject == null) 340 | return; 341 | 342 | // Calc new position of dragged object 343 | float currScale = !mCurrXform.updateScale ? 1.0f : mCurrXform.scale == 0.0f ? 1.0f : mCurrXform.scale; 344 | extractCurrPtInfo(); 345 | float newPosX = mCurrPtX - startPosX * currScale; 346 | float newPosY = mCurrPtY - startPosY * currScale; 347 | float newScale = startScaleOverPinchDiam * mCurrPtDiam; 348 | float newScaleX = startScaleXOverPinchWidth * mCurrPtWidth; 349 | float newScaleY = startScaleYOverPinchHeight * mCurrPtHeight; 350 | float newAngle = startAngleMinusPinchAngle + mCurrPtAng; 351 | 352 | // Set the new obj coords, scale, and angle as appropriate (notifying the subclass of the change). 353 | mCurrXform.set(newPosX, newPosY, newScale, newScaleX, newScaleY, newAngle); 354 | 355 | boolean success = objectCanvas.setPositionAndScale(selectedObject, mCurrXform, mCurrPt); 356 | if (!success) 357 | ; // If we could't set those params, do nothing currently 358 | } 359 | 360 | /** 361 | * State-based controller for tracking switches between no-touch, single-touch and multi-touch situations. Includes logic for cleaning up the 362 | * event stream, as events around touch up/down are noisy at least on early Synaptics sensors. 363 | */ 364 | private void multiTouchController() { 365 | if (DEBUG) 366 | Log.i("MultiTouch", "Got here 6 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); 367 | 368 | switch (mMode) { 369 | case MODE_NOTHING: 370 | // Not doing anything currently 371 | if (mCurrPt.isDown()) { 372 | // Start a new single-point drag 373 | selectedObject = objectCanvas.getDraggableObjectAtPoint(mCurrPt); 374 | if (selectedObject != null) { 375 | // Started a new single-point drag 376 | mMode = MODE_DRAG; 377 | objectCanvas.selectObject(selectedObject, mCurrPt); 378 | anchorAtThisPositionAndScale(); 379 | // Don't need any settling time if just placing one finger, there is no noise 380 | mSettleStartTime = mSettleEndTime = mCurrPt.getEventTime(); 381 | } 382 | } 383 | break; 384 | 385 | case MODE_DRAG: 386 | // Currently in a single-point drag 387 | if (!mCurrPt.isDown()) { 388 | // First finger was released, stop dragging 389 | mMode = MODE_NOTHING; 390 | objectCanvas.selectObject((selectedObject = null), mCurrPt); 391 | 392 | } else if (mCurrPt.isMultiTouch()) { 393 | // Point 1 was already down and point 2 was just placed down 394 | mMode = MODE_PINCH; 395 | // Restart the drag with the new drag position (that is at the midpoint between the touchpoints) 396 | anchorAtThisPositionAndScale(); 397 | // Need to let events settle before moving things, to help with event noise on touchdown 398 | mSettleStartTime = mCurrPt.getEventTime(); 399 | mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; 400 | 401 | } else { 402 | // Point 1 is still down and point 2 did not change state, just do single-point drag to new location 403 | if (mCurrPt.getEventTime() < mSettleEndTime) { 404 | // Ignore the first few events if we just stopped stretching, because if finger 2 was kept down while 405 | // finger 1 is lifted, then point 1 gets mapped to finger 2. Restart the drag from the new position. 406 | anchorAtThisPositionAndScale(); 407 | } else { 408 | // Keep dragging, move to new point 409 | performDragOrPinch(); 410 | } 411 | } 412 | break; 413 | 414 | case MODE_PINCH: 415 | // Two-point pinch-scale/rotate/translate 416 | if (!mCurrPt.isMultiTouch() || !mCurrPt.isDown()) { 417 | // Dropped one or both points, stop stretching 418 | 419 | if (!mCurrPt.isDown()) { 420 | // Dropped both points, go back to doing nothing 421 | mMode = MODE_NOTHING; 422 | objectCanvas.selectObject((selectedObject = null), mCurrPt); 423 | 424 | } else { 425 | // Just dropped point 2, downgrade to a single-point drag 426 | mMode = MODE_DRAG; 427 | // Restart the pinch with the single-finger position 428 | anchorAtThisPositionAndScale(); 429 | // Ignore the first few events after the drop, in case we dropped finger 1 and left finger 2 down 430 | mSettleStartTime = mCurrPt.getEventTime(); 431 | mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; 432 | } 433 | 434 | } else { 435 | // Still pinching 436 | if (Math.abs(mCurrPt.getX() - mPrevPt.getX()) > MAX_MULTITOUCH_POS_JUMP_SIZE 437 | || Math.abs(mCurrPt.getY() - mPrevPt.getY()) > MAX_MULTITOUCH_POS_JUMP_SIZE 438 | || Math.abs(mCurrPt.getMultiTouchWidth() - mPrevPt.getMultiTouchWidth()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE 439 | || Math.abs(mCurrPt.getMultiTouchHeight() - mPrevPt.getMultiTouchHeight()) * .5f > MAX_MULTITOUCH_DIM_JUMP_SIZE) { 440 | // Jumped too far, probably event noise, reset and ignore events for a bit 441 | anchorAtThisPositionAndScale(); 442 | mSettleStartTime = mCurrPt.getEventTime(); 443 | mSettleEndTime = mSettleStartTime + EVENT_SETTLE_TIME_INTERVAL; 444 | 445 | } else if (mCurrPt.eventTime < mSettleEndTime) { 446 | // Events have not yet settled, reset 447 | anchorAtThisPositionAndScale(); 448 | } else { 449 | // Stretch to new position and size 450 | performDragOrPinch(); 451 | } 452 | } 453 | break; 454 | } 455 | if (DEBUG) 456 | Log.i("MultiTouch", "Got here 7 - " + mMode + " " + mCurrPt.getNumTouchPoints() + " " + mCurrPt.isDown() + mCurrPt.isMultiTouch()); 457 | } 458 | 459 | // ------------------------------------------------------------------------------------ 460 | 461 | /** A class that packages up all MotionEvent information with all derived multitouch information (if available) */ 462 | public static class PointInfo { 463 | // Multitouch information 464 | private int numPoints; 465 | private float[] xs = new float[MAX_TOUCH_POINTS]; 466 | private float[] ys = new float[MAX_TOUCH_POINTS]; 467 | private float[] pressures = new float[MAX_TOUCH_POINTS]; 468 | private int[] pointerIdxs = new int[MAX_TOUCH_POINTS]; 469 | 470 | // Midpoint of pinch operations 471 | private float xMid, yMid, pressureMid; 472 | 473 | // Width/diameter/angle of pinch operations 474 | private float dx, dy, diameter, diameterSq, angle; 475 | 476 | // Whether or not there is at least one finger down (isDown) and/or at least two fingers down (isMultiTouch) 477 | private boolean isDown, isMultiTouch; 478 | 479 | // Whether or not these fields have already been calculated, for caching purposes 480 | private boolean diameterSqIsCalculated, diameterIsCalculated, angleIsCalculated; 481 | 482 | // Event action code and event time 483 | private int action; 484 | private long eventTime; 485 | 486 | // ------------------------------------------------------------------------------------------------------------------------------------------- 487 | 488 | /** Set all point info */ 489 | private void set(int numPoints, float[] x, float[] y, float[] pressure, int[] pointerIdxs, int action, boolean isDown, long eventTime) { 490 | if (DEBUG) 491 | Log.i("MultiTouch", "Got here 8 - " + +numPoints + " " + x[0] + " " + y[0] + " " + (numPoints > 1 ? x[1] : x[0]) + " " 492 | + (numPoints > 1 ? y[1] : y[0]) + " " + action + " " + isDown); 493 | this.eventTime = eventTime; 494 | this.action = action; 495 | this.numPoints = numPoints; 496 | for (int i = 0; i < numPoints; i++) { 497 | this.xs[i] = x[i]; 498 | this.ys[i] = y[i]; 499 | this.pressures[i] = pressure[i]; 500 | this.pointerIdxs[i] = pointerIdxs[i]; 501 | } 502 | this.isDown = isDown; 503 | this.isMultiTouch = numPoints >= 2; 504 | 505 | if (isMultiTouch) { 506 | xMid = (x[0] + x[1]) * .5f; 507 | yMid = (y[0] + y[1]) * .5f; 508 | pressureMid = (pressure[0] + pressure[1]) * .5f; 509 | dx = Math.abs(x[1] - x[0]); 510 | dy = Math.abs(y[1] - y[0]); 511 | 512 | } else { 513 | // Single-touch event 514 | xMid = x[0]; 515 | yMid = y[0]; 516 | pressureMid = pressure[0]; 517 | dx = dy = 0.0f; 518 | } 519 | // Need to re-calculate the expensive params if they're needed 520 | diameterSqIsCalculated = diameterIsCalculated = angleIsCalculated = false; 521 | } 522 | 523 | /** 524 | * Copy all fields from one PointInfo class to another. PointInfo objects are volatile so you should use this if you want to keep track of the 525 | * last touch event in your own code. 526 | */ 527 | public void set(PointInfo other) { 528 | this.numPoints = other.numPoints; 529 | for (int i = 0; i < numPoints; i++) { 530 | this.xs[i] = other.xs[i]; 531 | this.ys[i] = other.ys[i]; 532 | this.pressures[i] = other.pressures[i]; 533 | this.pointerIdxs[i] = other.pointerIdxs[i]; 534 | } 535 | this.xMid = other.xMid; 536 | this.yMid = other.yMid; 537 | this.pressureMid = other.pressureMid; 538 | this.dx = other.dx; 539 | this.dy = other.dy; 540 | this.diameter = other.diameter; 541 | this.diameterSq = other.diameterSq; 542 | this.angle = other.angle; 543 | this.isDown = other.isDown; 544 | this.action = other.action; 545 | this.isMultiTouch = other.isMultiTouch; 546 | this.diameterIsCalculated = other.diameterIsCalculated; 547 | this.diameterSqIsCalculated = other.diameterSqIsCalculated; 548 | this.angleIsCalculated = other.angleIsCalculated; 549 | this.eventTime = other.eventTime; 550 | } 551 | 552 | // ------------------------------------------------------------------------------------------------------------------------------------------- 553 | 554 | /** True if number of touch points >= 2. */ 555 | public boolean isMultiTouch() { 556 | return isMultiTouch; 557 | } 558 | 559 | /** Difference between x coords of touchpoint 0 and 1. */ 560 | public float getMultiTouchWidth() { 561 | return isMultiTouch ? dx : 0.0f; 562 | } 563 | 564 | /** Difference between y coords of touchpoint 0 and 1. */ 565 | public float getMultiTouchHeight() { 566 | return isMultiTouch ? dy : 0.0f; 567 | } 568 | 569 | /** Fast integer sqrt, by Jim Ulery. Much faster than Math.sqrt() for integers. */ 570 | private int julery_isqrt(int val) { 571 | int temp, g = 0, b = 0x8000, bshft = 15; 572 | do { 573 | if (val >= (temp = (((g << 1) + b) << bshft--))) { 574 | g += b; 575 | val -= temp; 576 | } 577 | } while ((b >>= 1) > 0); 578 | return g; 579 | } 580 | 581 | /** Calculate the squared diameter of the multitouch event, and cache it. Use this if you don't need to perform the sqrt. */ 582 | public float getMultiTouchDiameterSq() { 583 | if (!diameterSqIsCalculated) { 584 | diameterSq = (isMultiTouch ? dx * dx + dy * dy : 0.0f); 585 | diameterSqIsCalculated = true; 586 | } 587 | return diameterSq; 588 | } 589 | 590 | /** Calculate the diameter of the multitouch event, and cache it. Uses fast int sqrt but gives accuracy to 1/16px. */ 591 | public float getMultiTouchDiameter() { 592 | if (!diameterIsCalculated) { 593 | if (!isMultiTouch) { 594 | diameter = 0.0f; 595 | } else { 596 | // Get 1/16 pixel's worth of subpixel accuracy, works on screens up to 2048x2048 597 | // before we get overflow (at which point you can reduce or eliminate subpix 598 | // accuracy, or use longs in julery_isqrt()) 599 | float diamSq = getMultiTouchDiameterSq(); 600 | diameter = (diamSq == 0.0f ? 0.0f : (float) julery_isqrt((int) (256 * diamSq)) / 16.0f); 601 | // Make sure diameter is never less than dx or dy, for trig purposes 602 | if (diameter < dx) 603 | diameter = dx; 604 | if (diameter < dy) 605 | diameter = dy; 606 | } 607 | diameterIsCalculated = true; 608 | } 609 | return diameter; 610 | } 611 | 612 | /** 613 | * Calculate the angle of a multitouch event, and cache it. Actually gives the smaller of the two angles between the x axis and the line 614 | * between the two touchpoints, so range is [0,Math.PI/2]. Uses Math.atan2(). 615 | */ 616 | public float getMultiTouchAngle() { 617 | if (!angleIsCalculated) { 618 | if (!isMultiTouch) 619 | angle = 0.0f; 620 | else 621 | angle = (float) Math.atan2(ys[1] - ys[0], xs[1] - xs[0]); 622 | angleIsCalculated = true; 623 | } 624 | return angle; 625 | } 626 | 627 | // ------------------------------------------------------------------------------------------------------------------------------------------- 628 | 629 | /** Return the total number of touch points */ 630 | public int getNumTouchPoints() { 631 | return numPoints; 632 | } 633 | 634 | /** Return the X coord of the first touch point if there's only one, or the midpoint between first and second touch points if two or more. */ 635 | public float getX() { 636 | return xMid; 637 | } 638 | 639 | /** Return the array of X coords -- only the first getNumTouchPoints() of these is defined. */ 640 | public float[] getXs() { 641 | return xs; 642 | } 643 | 644 | /** Return the X coord of the first touch point if there's only one, or the midpoint between first and second touch points if two or more. */ 645 | public float getY() { 646 | return yMid; 647 | } 648 | 649 | /** Return the array of Y coords -- only the first getNumTouchPoints() of these is defined. */ 650 | public float[] getYs() { 651 | return ys; 652 | } 653 | 654 | /** 655 | * Return the array of pointer indices -- only the first getNumTouchPoints() of these is defined. These don't have to be all the numbers from 656 | * 0 to getNumTouchPoints()-1 inclusive, numbers can be skipped if a finger is lifted and the touch sensor is capable of detecting that that 657 | * particular touch point is no longer down. Note that most sensors do not have this capability: when finger 1 is lifted up finger 2 becomes 658 | * the new finger 1. 659 | */ 660 | public int[] getPointerIndices() { 661 | return pointerIdxs; 662 | } 663 | 664 | /** Return the pressure the first touch point if there's only one, or the average pressure of first and second touch points if two or more. */ 665 | public float getPressure() { 666 | return pressureMid; 667 | } 668 | 669 | /** Return the array of pressures -- only the first getNumTouchPoints() of these is defined. */ 670 | public float[] getPressures() { 671 | return pressures; 672 | } 673 | 674 | // ------------------------------------------------------------------------------------------------------------------------------------------- 675 | 676 | public boolean isDown() { 677 | return isDown; 678 | } 679 | 680 | public int getAction() { 681 | return action; 682 | } 683 | 684 | public long getEventTime() { 685 | return eventTime; 686 | } 687 | } 688 | 689 | // ------------------------------------------------------------------------------------ 690 | 691 | /** 692 | * A class that is used to store scroll offsets and scale information for objects that are managed by the multitouch controller 693 | */ 694 | public static class PositionAndScale { 695 | private float xOff, yOff, scale, scaleX, scaleY, angle; 696 | private boolean updateScale, updateScaleXY, updateAngle; 697 | 698 | /** 699 | * Set position and optionally scale, anisotropic scale, and/or angle. Where if the corresponding "update" flag is set to false, the field's 700 | * value will not be changed during a pinch operation. If the value is not being updated *and* the value is not used by the client 701 | * application, then the value can just be zero. However if the value is not being updated but the value *is* being used by the client 702 | * application, the value should still be specified and the update flag should be false (e.g. angle of the object being dragged should still 703 | * be specified even if the program is in "resize" mode rather than "rotate" mode). 704 | */ 705 | public void set(float xOff, float yOff, boolean updateScale, float scale, boolean updateScaleXY, float scaleX, float scaleY, 706 | boolean updateAngle, float angle) { 707 | this.xOff = xOff; 708 | this.yOff = yOff; 709 | this.updateScale = updateScale; 710 | this.scale = scale == 0.0f ? 1.0f : scale; 711 | this.updateScaleXY = updateScaleXY; 712 | this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; 713 | this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; 714 | this.updateAngle = updateAngle; 715 | this.angle = angle; 716 | } 717 | 718 | /** Set position and optionally scale, anisotropic scale, and/or angle, without changing the "update" flags. */ 719 | protected void set(float xOff, float yOff, float scale, float scaleX, float scaleY, float angle) { 720 | this.xOff = xOff; 721 | this.yOff = yOff; 722 | this.scale = scale == 0.0f ? 1.0f : scale; 723 | this.scaleX = scaleX == 0.0f ? 1.0f : scaleX; 724 | this.scaleY = scaleY == 0.0f ? 1.0f : scaleY; 725 | this.angle = angle; 726 | } 727 | 728 | public float getXOff() { 729 | return xOff; 730 | } 731 | 732 | public float getYOff() { 733 | return yOff; 734 | } 735 | 736 | public float getScale() { 737 | return !updateScale ? 1.0f : scale; 738 | } 739 | 740 | /** Included in case you want to support anisotropic scaling */ 741 | public float getScaleX() { 742 | return !updateScaleXY ? 1.0f : scaleX; 743 | } 744 | 745 | /** Included in case you want to support anisotropic scaling */ 746 | public float getScaleY() { 747 | return !updateScaleXY ? 1.0f : scaleY; 748 | } 749 | 750 | public float getAngle() { 751 | return !updateAngle ? 0.0f : angle; 752 | } 753 | } 754 | 755 | // ------------------------------------------------------------------------------------ 756 | 757 | public static interface MultiTouchObjectCanvas { 758 | 759 | /** 760 | * See if there is a draggable object at the current point. Returns the object at the point, or null if nothing to drag. To start a multitouch 761 | * drag/stretch operation, this routine must return some non-null reference to an object. This object is passed into the other methods in this 762 | * interface when they are called. 763 | * 764 | * @param touchPoint 765 | * The point being tested (in object coordinates). Return the topmost object under this point, or if dragging/stretching the whole 766 | * canvas, just return a reference to the canvas. 767 | * @return a reference to the object under the point being tested, or null to cancel the drag operation. If dragging/stretching the whole 768 | * canvas (e.g. in a photo viewer), always return non-null, otherwise the stretch operation won't work. 769 | */ 770 | public T getDraggableObjectAtPoint(PointInfo touchPoint); 771 | 772 | /** 773 | * Get the screen coords of the dragged object's origin, and scale multiplier to convert screen coords to obj coords. The job of this routine 774 | * is to call the .set() method on the passed PositionAndScale object to record the initial position and scale of the object (in object 775 | * coordinates) before any dragging/stretching takes place. 776 | * 777 | * @param obj 778 | * The object being dragged/stretched. 779 | * @param objPosAndScaleOut 780 | * Output parameter: You need to call objPosAndScaleOut.set() to record the current position and scale of obj. 781 | */ 782 | public void getPositionAndScale(T obj, PositionAndScale objPosAndScaleOut); 783 | 784 | /** 785 | * Callback to update the position and scale (in object coords) of the currently-dragged object. 786 | * 787 | * @param obj 788 | * The object being dragged/stretched. 789 | * @param newObjPosAndScale 790 | * The new position and scale of the object, in object coordinates. Use this to move/resize the object before returning. 791 | * @param touchPoint 792 | * Info about the current touch point, including multitouch information and utilities to calculate and cache multitouch pinch 793 | * diameter etc. (Note: touchPoint is volatile, if you want to keep any fields of touchPoint, you must copy them before the method 794 | * body exits.) 795 | * @return true if setting the position and scale of the object was successful, or false if the position or scale parameters are out of range 796 | * for this object. 797 | */ 798 | public boolean setPositionAndScale(T obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint); 799 | 800 | /** 801 | * Select an object at the given point. Can be used to bring the object to top etc. Only called when first touchpoint goes down, not when 802 | * multitouch is initiated. Also called with null on touch-up. 803 | * 804 | * @param obj 805 | * The object being selected by single-touch, or null on touch-up. 806 | * @param touchPoint 807 | * The current touch point. 808 | */ 809 | public void selectObject(T obj, PointInfo touchPoint); 810 | } 811 | } 812 | --------------------------------------------------------------------------------
Type: TEXT
Owner can Init/Read
Owner can Init/Read/Write
Type: BOOLEAN
Owner can Init
Owner can Read
Type: INTEGER
Type: BIGINT
11 | * public class MyMTView extends View implements MultiTouchObjectCanvas { 12 | * 13 | * private MultiTouchController multiTouchController = new MultiTouchController(this); 14 | * 15 | * // Pass touch events to the MT controller 16 | * public boolean onTouchEvent(MotionEvent event) { 17 | * return multiTouchController.onTouchEvent(event); 18 | * } 19 | * 20 | * // ... then implement the MultiTouchObjectCanvas interface here, see details in the comments of that interface. 21 | * } 22 | *