JS组件Bootstrap Table表格多行拖拽效果实现代码


Posted in Javascript onDecember 08, 2015

前言:前天刚写了篇JS组件Bootstrap Table表格行拖拽效果,今天接到新的需要,需要在之前表格行拖拽的基础上能够同时拖拽选中的多行。用了半天时间研究了下,效果是出来了,但是感觉不尽如人意。先把它分享出来,以后想到更好的办法再优化吧。

一、效果展示

1、拖动前

JS组件Bootstrap Table表格多行拖拽效果实现代码

2、拖动中

JS组件Bootstrap Table表格多行拖拽效果实现代码

3、拖动后

JS组件Bootstrap Table表格多行拖拽效果实现代码

4、撤销回到拖动前状态

JS组件Bootstrap Table表格多行拖拽效果实现代码

二、需求分析
通过上篇我们知道,如果要实现拖拽,必须要有一个可以拖拽的标签,或者叫容器,比如上篇里面的tr就是一个拖拽的容器,那么如果要实现选择行的拖拽,那么博主的第一反应是将选中的行放到一个容器里面,比如放到一个div中,然后注册这个div的可拖拽,可是实际情况是,tr是在table里面的标签,如果将tr用div包起来,势必将table里面的样式打乱,这个界面就真的是乱掉了。很显然,这条路走不通。然后通过谷歌浏览器审核元素知道,用Bootstrap table生成的表格tr的父级元素实际上是tbody,于是在想是否可以注册tbody的拖拽,实践证明,此法可行。于是就此开干。

三、代码示例
cshtm的代码就不再重复,和上篇相同。我们重点来看看js代码。

var i_statuindex = 0;
var arrdata = [];

var m_oTable = null;

$(function () {
 //1.初始化表格
 m_oTable = new TableInit();
 m_oTable.Init();

 //2.初始化按钮事件
 var oButtonInit = new ButtonInit();
 oButtonInit.Init();

 //3.日期控件的初始化
 $(".datetimepicker").datetimepicker({
 format: 'yyyy-mm-dd hh:ii',
 autoclose: true,
 todayBtn: true,
 });

});

//表格相关事件和方法
var TableInit = function () {
 var oTableInit = new Object();

 oTableInit.Init = function () {
 $('#tb_order_left').bootstrapTable({
 url: '/api/OrderApi/get',
 method: 'get',
 striped: true,
 cache: false,
 striped: true,
 pagination: true,
 height: 600,
 uniqueId:"TO_ORDER_ID",
 queryParams: oTableInit.queryParams,
 queryParamsType: "limit",
 sidePagination: "server",
 pageSize: 10,
 pageList: [10, 25, 50, 100],
 search: true,
 strictSearch: true,
 showColumns: true,
 showRefresh: true,
 minimumCountColumns: 2,
 clickToSelect: true,
 columns: [{
 checkbox: true
 },
 {
 field: 'ORDER_NO',
 title: '订单号'
 },
 {
 field: 'BODY_NO',
 title: '车身号'
 }, {
 field: 'VIN',
 title: 'VIN码'
 }, {
 field: 'TM_MODEL_MATERIAL_ID',
 title: '整车编码'
 },
 {
 field: 'ORDER_TYPE',
 title: '订单类型'
 },
 {
 field: 'ORDER_STATUS',
 title: '订单状态'
 },
 {
 field: 'CREATE_DATE',
 title: '订单导入时间'
 },
 {
 field: 'PLAN_DATE',
 title: '订单计划上线日期'
 },
 {
 field: 'VMS_NO',
 title: 'VMS号'
 },
 {
 field: 'ENGIN_CODE',
 title: '发动机号'
 },
 {
 field: 'TRANS_CODE',
 title: '变速箱号'
 },
 {
 field: 'OFFLINE_DATE_ACT',
 title: '实际下线日期'
 },
 {
 field: 'HOLD_RES',
 title: 'hold理由'
 },
 {
 field: 'SPC_FLAG',
 title: '特殊标记'
 },
 ],
 onLoadSuccess: function (data) {
 oTableInit.InitDrag();
 if (data.total > 0) {
 var iheight = $('#div_tableleft').find(".fixed-table-container").height();
 $('#div_tableleft').find(".fixed-table-container").height(iheight + 36);
 }
 },
 onCheckAll: function (rows) {
 $("#tb_order_left tbody tr").addClass("selected");
 },
 onUncheckAll: function (rows) {
 $("#tb_order_left tbody tr").removeClass("selected");
 }
 });

 $('#tb_order_right').bootstrapTable({
 url: '/api/OrderApi/get',
 method: 'get',
 toolbar: '#toolbar_right',
 striped: true,
 cache: false,
 striped: true,
 pagination: true,
 height: 600,
 queryParams: oTableInit.queryParamsRight,
 queryParamsType: "limit",
 //ajaxOptions: { departmentname: "", statu: "" },
 sidePagination: "server",
 pageSize: 10,
 pageList: [10, 25, 50, 100],
 search: true,
 strictSearch: true,
 showRefresh: true,
 minimumCountColumns: 2,
 columns: [
 {
 field: 'ORDER_NO',
 title: '订单号'
 },
 {
 field: 'BODY_NO',
 title: '车身号'
 }, {
 field: 'VIN',
 title: 'VIN码'
 }, {
 field: 'TM_MODEL_MATERIAL_ID',
 title: '整车编码'
 },
 {
 field: 'ORDER_TYPE',
 title: '订单类型'
 },
 {
 field: 'ORDER_STATUS',
 title: '订单状态'
 },
 {
 field: 'CREATE_DATE',
 title: '订单导入时间'
 },
 {
 field: 'PLAN_DATE',
 title: '订单计划上线日期'
 },
 {
 field: 'VMS_NO',
 title: 'VMS号'
 },
 {
 field: 'ENGIN_CODE',
 title: '发动机号'
 },
 {
 field: 'TRANS_CODE',
 title: '变速箱号'
 },
 {
 field: 'OFFLINE_DATE_ACT',
 title: '实际下线日期'
 },
 {
 field: 'HOLD_RES',
 title: 'hold理由'
 },
 {
 field: 'SPC_FLAG',
 title: '特殊标记'
 },
 ],
 onLoadSuccess: function (data) {
 oTableInit.InitDrop();
 }
 });
 };

 oTableInit.InitDrag = function () {
 $('#tb_order_left tbody').draggable({
 helper: "clone",
 start: function (event, ui) {
 var old_left_data = JSON.stringify($('#tb_order_left').bootstrapTable("getData"));
 var old_right_data = JSON.stringify($('#tb_order_right').bootstrapTable("getData"));
 var odata = { index: ++i_statuindex, left_data: old_left_data, right_data: old_right_data };
 arrdata.push(odata);
 },
 stop: function (event, ui) {
 }
 });
 };

 oTableInit.InitDrop = function () {
 $("#div_tableright div[class=fixed-table-container]").droppable({
 drop: function (event, ui) {
 var arrtr = $(ui.helper[0]).find("tr[class=selected]");
 if (arrtr.length <= 0) {
 alert("请先选中要插单的行");
 return;
 }
 var oTop = ui.helper[0].offsetTop;
 var iRowHeadHeight = 40;
 var iRowHeight = 37;
 var rowIndex = 0;
 if (oTop <= iRowHeadHeight + iRowHeight / 2) {
 rowIndex = 0;
 }
 else {
 rowIndex = Math.ceil((oTop - iRowHeadHeight) / iRowHeight);
 }
 for (var i = 0; i < arrtr.length; i++) {
 var arrtd = $(arrtr[i]).find("td");
 var uniqueid = $(arrtr[i]).attr("data-uniqueid");
 var rowdata = {
 ORDER_NO: $(arrtd[1]).text(),
 BODY_NO: $(arrtd[2]).text(),
 VIN: $(arrtd[3]).text(),
 TM_MODEL_MATERIAL_ID: $(arrtd[4]).text(),
 ORDER_TYPE: $(arrtd[5]).text(),
 ORDER_STATUS: $(arrtd[6]).text(),
 CREATE_DATE: $(arrtd[7]).text() == "-" ? null : $(arrtd[7]).text(),
 PLAN_DATE: $(arrtd[8]).text() == "-" ? null : $(arrtd[8]).text(),
 VMS_NO: $(arrtd[9]).text(),
 ENGIN_CODE: $(arrtd[10]).text(),
 TRANS_CODE: $(arrtd[11]).text(),
 OFFLINE_DATE_ACT: $(arrtd[12]).text() == "-" ? null : $(arrtd[12]).text(),
 HOLD_RES: $(arrtd[13]).text(),
 SPC_FLAG: $(arrtd[14]).text(),
 TO_ORDER_ID: uniqueid

 };
 $("#tb_order_right").bootstrapTable("insertRow", { index: rowIndex++, row: rowdata });
 $('#tb_order_left').bootstrapTable("removeByUniqueId", uniqueid);
 }
 
 
 oTableInit.InitDrag();
 }
 });
 };

 oTableInit.queryParams = function (params) { //配置参数
 var temp = { //这里的键的名字和控制器的变量名必须一直,这边改动,控制器也需要改成一样的
 limit: params.limit, //页面大小
 offset: params.offset, //页码
 strBodyno: $("#txt_search_bodynumber").val(),
 strVin: $("#txt_search_vinnumber").val(),
 strOrderno: $("#txt_search_ordernumber").val(),
 strEngincode: $("#txt_search_engin_code").val(),
 strOrderstatus: 0,
 strTranscode: $("#txt_search_trans_code").val(),
 strVms: $("#txt_search_vms").val(),
 strCarcode: $("#txt_search_carcode").val(),
 strImportStartdate: $("#txt_search_import_startdate").val(),
 strImportEnddate: $("#txt_search_import_enddate").val(),
 strSendStartdate: $("#txt_search_send_startdate").val(),
 strSendEnddate: $("#txt_search_send_enddate").val(),

 };
 return temp;
 };

 oTableInit.queryParamsRight = function (params) { //配置参数
 var temp = { //这里的键的名字和控制器的变量名必须一直,这边改动,控制器也需要改成一样的
 limit: params.limit, //页面大小
 offset: params.offset, //页码
 strBodyno: "",
 strVin: "",
 strOrderno: "",
 strEngincode: "",
 strOrderstatus: 5,
 strTranscode: "",
 strVms: "",
 strCarcode: "",
 strImportStartdate: "",
 strImportEnddate: "",
 strSendStartdate: "",
 strSendEnddate: "",

 };
 return temp;
 };

 return oTableInit;
};

//页面按钮初始化事件
var ButtonInit = function () {
 var oInit = new Object();
 var postdata = {};

 oInit.Init = function () {

 //查询点击事件
 $("#btn_query").click(function () {
 $("#tb_order_left").bootstrapTable('refresh');
 });

 //重置点击事件
 $("#btn_reset").click(function () {
 $(".container-fluid").find(".form-control").val("");
 $("#tb_order_left").bootstrapTable('refresh');
 });

 //插单操作点击事件
 $("#btn_insertorder").click(function () {
 
 });

 //撤销操作点击事件
 $("#btn_cancel").click(function () {
 if (i_statuindex <= 0) {
 return;
 }
 for (var i = 0; i < arrdata.length; i++) {
 if (arrdata[i].index != i_statuindex) {
 continue;
 }
 var arr_left_data = eval(arrdata[i].left_data);
 var arr_right_data = eval(arrdata[i].right_data);

 $('#tb_order_left').bootstrapTable('removeAll');
 $('#tb_order_right').bootstrapTable('removeAll');
 $('#tb_order_left').bootstrapTable('append', arr_left_data);
 for (var x = 0; x < arr_right_data.length; x++) {
 $("#tb_order_right").bootstrapTable("insertRow", { index: x, row: arr_right_data[x] });
 }
 
 //$('#tb_order_right').bootstrapTable('append', arr_right_data);//append之后不能drop
 break;
 }
 i_statuindex--;

 //重新注册可拖拽
 m_oTable.InitDrag();
 //m_oTable.InitDrop();
 });
 };

 return oInit;
};

还是重点看看部分代码

1、注册左边可拖拽

$('#tb_order_left tbody').draggable({
 helper: "clone",
 start: function (event, ui) {
 var old_left_data = JSON.stringify($('#tb_order_left').bootstrapTable("getData"));
 var old_right_data = JSON.stringify($('#tb_order_right').bootstrapTable("getData"));
 var odata = { index: ++i_statuindex, left_data: old_left_data, right_data: old_right_data };
 arrdata.push(odata);
 },
 stop: function (event, ui) {
 }
 });

这里代码很简单,主要做了两件事:

(1)注册tbody的可拖拽,同样适用的JQuery UI的draggable事件。

(2)在开始拖拽前,保存两边表格的数据,用于还原的操作。

2、注册右边drop

$("#div_tableright div[class=fixed-table-container]").droppable({
 drop: function (event, ui) {
 var arrtr = $(ui.helper[0]).find("tr[class=selected]");
 if (arrtr.length <= 0) {
 alert("请先选中要插单的行");
 return;
 }
 var oTop = ui.helper[0].offsetTop;
 var iRowHeadHeight = 40;
 var iRowHeight = 37;
 var rowIndex = 0;
 if (oTop <= iRowHeadHeight + iRowHeight / 2) {
 rowIndex = 0;
 }
 else {
 rowIndex = Math.ceil((oTop - iRowHeadHeight) / iRowHeight);
 }
 for (var i = 0; i < arrtr.length; i++) {
 var arrtd = $(arrtr[i]).find("td");
 var uniqueid = $(arrtr[i]).attr("data-uniqueid");
 var rowdata = {
 ORDER_NO: $(arrtd[1]).text(),
 BODY_NO: $(arrtd[2]).text(),
 VIN: $(arrtd[3]).text(),
 TM_MODEL_MATERIAL_ID: $(arrtd[4]).text(),
 ORDER_TYPE: $(arrtd[5]).text(),
 ORDER_STATUS: $(arrtd[6]).text(),
 CREATE_DATE: $(arrtd[7]).text() == "-" ? null : $(arrtd[7]).text(),
 PLAN_DATE: $(arrtd[8]).text() == "-" ? null : $(arrtd[8]).text(),
 VMS_NO: $(arrtd[9]).text(),
 ENGIN_CODE: $(arrtd[10]).text(),
 TRANS_CODE: $(arrtd[11]).text(),
 OFFLINE_DATE_ACT: $(arrtd[12]).text() == "-" ? null : $(arrtd[12]).text(),
 HOLD_RES: $(arrtd[13]).text(),
 SPC_FLAG: $(arrtd[14]).text(),
 TO_ORDER_ID: uniqueid

 };
 $("#tb_order_right").bootstrapTable("insertRow", { index: rowIndex++, row: rowdata });
 $('#tb_order_left').bootstrapTable("removeByUniqueId", uniqueid);
 }
 
 
 oTableInit.InitDrag();
 }
 });

这里代码和之前有点变化

(1)注册#div_tableright div[class=fixed-table-container]标签的droppable,这个标签是Bootstrap Table表格初始化后自动生成的,为什么不直接注册表格#tb_order_right的droppable,是因为这个标签作用域太小,会导致拖过来的tbody捕捉不到drop事件。而注册表格外部的#div_tableright div[class=fixed-table-container]这个div标签可以解决问题。

(2)通过var arrtr = $(ui.helper[0]).find("tr[class=selected]");找到拖过来tbody里面选中的行,然后将数据取出依次插入右边表格,左边表格则依次删除行数据。

(3)这里有点麻烦的是找drop的位置,我们知道,要想将左边选中的行放到右边指定的位置,那么就得得到当前鼠标drop的位置,这里通过ui.helper[0].offsetTop属性来获得鼠标的Y轴位置,通过计算得到要插入的位置。

3、撤销操作

 $("#btn_cancel").click(function () {
 if (i_statuindex <= 0) {
 return;
 }
 for (var i = 0; i < arrdata.length; i++) {
 if (arrdata[i].index != i_statuindex) {
 continue;
 }
 var arr_left_data = eval(arrdata[i].left_data);
 var arr_right_data = eval(arrdata[i].right_data);

 $('#tb_order_left').bootstrapTable('removeAll');
 $('#tb_order_right').bootstrapTable('removeAll');
 $('#tb_order_left').bootstrapTable('append', arr_left_data);
 for (var x = 0; x < arr_right_data.length; x++) {
 $("#tb_order_right").bootstrapTable("insertRow", { index: x, row: arr_right_data[x] });
 }
 
 //$('#tb_order_right').bootstrapTable('append', arr_right_data);//append之后不能drop
 break;
 }
 i_statuindex--;

 //重写注册可拖拽
 m_oTable.InitDrag();
 //m_oTable.InitDrop();
 });

撤销操作和之前也基本相同。

四、总结
效果是完成了,美中不足的是每次拖过去的都是整个tbody,而不是选中的行,奈何多个选中的行无法用某一个容器包起来。暂时没找到合适的解决方案。先这样吧,等以后想到好的方案了再优化吧。 

五、优化方案

1、注册drap的方法

oTableInit.InitDrag = function () {
 $('#tb_order_left tbody').draggable({
 helper: "clone",
 start: function (event, ui) {
 var old_left_data = JSON.stringify($('#tb_order_left').bootstrapTable("getData"));
 var old_right_data = JSON.stringify($('#tb_order_right').bootstrapTable("getData"));
 var odata = { index: ++i_statuindex, left_data: old_left_data, right_data: old_right_data };
 arrdata.push(odata);
 $(ui.helper[0]).find("tr[class!=selected]").remove();
 },
 stop: function (event, ui) {
 }
 });
 };

增加了这一句$(ui.helper[0]).find("tr[class!=selected]").remove();这样在拖动的时候就看不到未选中的行了。
2、精准定位到右边表格指定位置:

oTableInit.InitDrop = function () {
 $("#div_tableright div[class=fixed-table-container]").droppable({
 drop: function (event, ui) {
 var arrtr = $(ui.helper[0]).find("tr[class=selected]");
 if (arrtr.length <= 0) {
 toastr.warning('请先选中要插单的行');
 return;
 }
 var oTop = ui.helper[0].offsetTop;
 //因为表格每行的高度可能不一致,所以这里取插入行位置的办法是:取右边表格的行高依次累加,计算行索引。
 var rowIndex = 0;
 var bIsBottom = true;
 var iRowHeadHeight = 40;
 var addHeight = iRowHeadHeight;
 var trs = $("#tb_order_right tbody tr");
 for (var i = 0; i < trs.length; i++) {
 addHeight += $(trs[i]).height();
 if (addHeight > oTop) {
 rowIndex = i;
 bIsBottom = false;//这里参数用来定义拖动到右边表格最下面的情况,这时没有进入到此条件判断里面。
 break;
 }
 }
 if (bIsBottom) {
 rowIndex = trs.length;
 }

 for (var i = 0; i < arrtr.length; i++) {
 var arrtd = $(arrtr[i]).find("td");
 var uniqueid = $(arrtr[i]).attr("data-uniqueid");
 var rowdata = {
 ORDER_NO: $(arrtd[1]).text(),
 BODY_NO: $(arrtd[2]).text(),
 VIN: $(arrtd[3]).text(),
 TM_MODEL_MATERIAL_ID: $(arrtd[4]).text(),
 ORDER_TYPE: $(arrtd[5]).text(),
 ORDER_STATUS_NAME: $(arrtd[6]).text(),
 CREATE_DATE: $(arrtd[7]).text() == "-" ? null : $(arrtd[7]).text(),
 PLAN_DATE: $(arrtd[8]).text() == "-" ? null : $(arrtd[8]).text(),
 VMS_NO: $(arrtd[9]).text(),
 ENGIN_CODE: $(arrtd[10]).text(),
 TRANS_CODE: $(arrtd[11]).text(),
 OFFLINE_DATE_ACT: $(arrtd[12]).text() == "-" ? null : $(arrtd[12]).text(),
 HOLD_RES: $(arrtd[13]).text(),
 SPC_FLAG: $(arrtd[14]).text(),
 TO_ORDER_ID: uniqueid,
 ORDER_STATUS:0

 };
 $("#tb_order_right").bootstrapTable("insertRow", { index: rowIndex++, row: rowdata });
 $('#tb_order_left').bootstrapTable("removeByUniqueId", uniqueid);
 }

 oTableInit.InitDrag();

 }
 });
 };

因为每一行的行高不确定,是由行里面的数据动态撑出来的,所以这里也动态计算drop的位置。

至此,这个小的功能基本告一段落,基本的效果和待优化点也完成了。

源码下载:Bootstrap Table表格多行拖拽效果

如果大家还想深入学习,可以点击这里进行学习,再为大家附两个精彩的专题:Bootstrap学习教程 Bootstrap实战教程

以上就是本文的全部内容,希望本文所述对大家学习javascript程序设计有所帮助。

Javascript 相关文章推荐
Mac地址验证的javascript代码
Nov 09 Javascript
jQuery将所有被选中的checkbox某个属性值连接成字符串的方法
Jan 24 Javascript
JQuery CheckBox(复选框)操作方法汇总
Apr 15 Javascript
JS+CSS实现简易实用的滑动门菜单效果
Sep 18 Javascript
微信小程序 开发工具快捷键整理
Oct 31 Javascript
jQuery实现frame之间互通的方法
Jun 26 jQuery
JS去掉字符串中所有的逗号
Oct 18 Javascript
webpack配置导致字体图标无法显示的解决方法
Mar 06 Javascript
Node.js npm命令运行node.js脚本的方法
Oct 10 Javascript
Next.js实现react服务器端渲染的方法示例
Jan 06 Javascript
VUE table表格动态添加一列数据,新增的这些数据不可以编辑(v-model绑定的数据不能实时更新)
Apr 03 Javascript
QT与javascript交互数据的实现
May 26 Javascript
AngularJS实现全选反选功能
Dec 08 #Javascript
JS操作XML实例总结(加载与解析XML文件、字符串)
Dec 08 #Javascript
JS组件Bootstrap Table表格行拖拽效果实现代码
Aug 27 #Javascript
JS获取月份最后天数、最大天数与某日周数的方法
Dec 08 #Javascript
AngularJS Module方法详解
Dec 08 #Javascript
JS组件Bootstrap实现弹出框和提示框效果代码
Dec 08 #Javascript
JS与jQ读取xml文件的方法
Dec 08 #Javascript
You might like
可快速识别放射性物质-国外大神教你diy一个开放式辐射探测器
2020/03/12 无线电
php 获取本地IP代码
2013/06/23 PHP
使用PHPCMS搭建wap手机网站
2015/09/20 PHP
php提交post数组参数实例分析
2015/12/17 PHP
CI框架文件上传类及图像处理类用法分析
2016/05/18 PHP
详细解读php的命名空间(二)
2018/02/21 PHP
浅谈js的setInterval事件
2014/12/05 Javascript
基于JQuery制作可编辑的表格特效
2014/12/23 Javascript
详解JavaScript中的客户端消息框架设计原理
2015/06/24 Javascript
浅谈JavaScript中的对象及Promise对象的实现
2015/11/15 Javascript
全屏js头像上传插件源码高清版
2016/03/29 Javascript
jquery实现上传文件大小类型的验证例子(推荐)
2016/06/25 Javascript
Bootstrap基本布局实现方法详解
2016/11/25 Javascript
微信小程序开发之Tabbar实例详解
2017/01/09 Javascript
利用node.js搭建简单web服务器的方法教程
2017/02/20 Javascript
使用async、enterproxy控制并发数量的方法详解
2018/01/02 Javascript
详解html-webpack-plugin插件(用法总结)
2018/09/12 Javascript
使用eslint和githooks统一前端风格的技巧
2020/07/29 Javascript
详解JavaScript中的数据类型,以及检测数据类型的方法
2020/09/17 Javascript
[44:09]DOTA2上海特级锦标赛A组小组赛#1 EHOME VS MVP.Phx第二局
2016/02/25 DOTA
Python实现爬虫从网络上下载文档的实例代码
2018/06/13 Python
对Python多线程读写文件加锁的实例详解
2019/01/14 Python
django列表筛选功能的实现代码
2020/03/27 Python
django执行数据库查询之后实现返回的结果集转json
2020/03/31 Python
python对 MySQL 数据库进行增删改查的脚本
2020/10/22 Python
HTML5之SVG 2D入门7—SVG元素的重用与引用
2013/01/30 HTML / CSS
台湾旅游网站:雄狮旅游网
2017/08/16 全球购物
中东奢侈品市场:Coveti
2019/05/12 全球购物
有模特经验的简历自我评价
2013/09/19 职场文书
给排水工程师岗位职责
2013/11/21 职场文书
清明节网上祭英烈活动总结
2014/04/30 职场文书
2014年房地产销售工作总结
2014/12/01 职场文书
毕业设计指导教师评语
2014/12/30 职场文书
2019入党申请书范文3篇
2019/08/21 职场文书
如何用JavaScript检测当前浏览器是无头浏览器
2021/04/27 Javascript
在Spring-Boot中如何使用@Value注解注入集合类
2021/08/02 Java/Android