|
import android |
|
from android.app import AlertDialog |
|
from android.graphics import Color |
|
from android.graphics.drawable import ColorDrawable |
|
from android.os import AsyncTask |
|
from android.os import Looper |
|
from android.os import Handler |
|
from android.util import TypedValue |
|
from android.view import View |
|
from android.view import MenuItem |
|
from android.widget import LinearLayout |
|
from android.widget import ArrayAdapter |
|
from android.widget import ImageView |
|
from android.widget import TextView |
|
from android.widget import EditText |
|
from android.support.v4.widget import SwipeRefreshLayout |
|
from android.util import Log |
|
|
|
from java.io import BufferedInputStream, BufferedReader, InputStreamReader, OutputStreamWriter |
|
from java.net import URL |
|
|
|
from org.json import JSONArray, JSONObject |
|
|
|
from com.baoyz.swipemenulistview import SwipeMenuListView, SwipeMenuCreator, SwipeMenuItem |
|
|
|
|
|
class RefreshTask(implements=java.lang.Runnable): |
|
def __init__(self, adapter): |
|
self.adapter = adapter |
|
|
|
def run(self) -> void: |
|
Log.i("TESTAPP", "BACKGROUND REFRESH DONE TASK") |
|
self.adapter.notifyDataSetChanged() |
|
Log.i("TESTAPP", "BACKGROUND REFRESH DONE TASK DONE") |
|
|
|
|
|
class UpdateDataTask(implements=java.lang.Runnable): |
|
def __init__(self, adapter): |
|
self.adapter = adapter |
|
|
|
def run(self) -> void: |
|
url = URL("http://freakboy3742.pythonanywhere.com/api/todo/?format=json") |
|
connection = url.openConnection() |
|
|
|
Log.i("TESTAPP", "GET RESPONSE: %s" % connection.getResponseCode()) |
|
|
|
reader = BufferedReader(InputStreamReader(connection.getInputStream())) |
|
content = "" |
|
line = reader.readLine() |
|
while line: |
|
content = content + '\n' + line |
|
line = reader.readLine() |
|
|
|
json = JSONArray(content) |
|
|
|
self.adapter.data = [] |
|
for i in range(0, json.length()): |
|
obj = json.get(i) |
|
self.adapter.data.append(obj) |
|
|
|
handler = Handler(Looper.getMainLooper()) |
|
handler.post(RefreshTask(self.adapter)) |
|
|
|
connection.disconnect() |
|
|
|
|
|
class DeleteItemTask(implements=java.lang.Runnable): |
|
def __init__(self, adapter, index): |
|
self.adapter = adapter |
|
self.index = index |
|
|
|
def run(self) -> void: |
|
Log.i("TESTAPP", "DELETE ITEM %s" % self.index) |
|
url = URL("http://freakboy3742.pythonanywhere.com/api/todo/%s/" % self.adapter.data[self.index].get('id')) |
|
Log.i("TESTAPP", "DELETE URL %s" % url) |
|
connection = url.openConnection() |
|
connection.setRequestMethod("DELETE") |
|
|
|
Log.i("TESTAPP", "DELETE RESPONSE: %s" % connection.getResponseCode()) |
|
|
|
del self.adapter.data[self.index] |
|
|
|
handler = Handler(Looper.getMainLooper()) |
|
handler.post(RefreshTask(self.adapter)) |
|
|
|
|
|
class AddItemTask(implements=java.lang.Runnable): |
|
def __init__(self, adapter, description): |
|
self.adapter = adapter |
|
self.description = description |
|
|
|
def run(self) -> void: |
|
url = URL("http://freakboy3742.pythonanywhere.com/api/todo/") |
|
connection = url.openConnection() |
|
connection.setRequestMethod("POST") |
|
connection.setDoOutput(True) |
|
connection.setRequestProperty("Content-Type", "application/json") |
|
writer = OutputStreamWriter(connection.getOutputStream()) |
|
writer.write('{"description": "%s", "completed": false}' % self.description) |
|
writer.flush() |
|
# writer.close() |
|
|
|
Log.i("TESTAPP", "PUT RESPONSE: %s %s" % (connection.getResponseCode(), connection.getResponseMessage())) |
|
|
|
reader = BufferedReader(InputStreamReader(connection.getInputStream())) |
|
content = "" |
|
line = reader.readLine() |
|
while line: |
|
content = content + '\n' + line |
|
line = reader.readLine() |
|
|
|
self.adapter.data.append(JSONObject(content)) |
|
|
|
handler = Handler(Looper.getMainLooper()) |
|
handler.post(RefreshTask(self.adapter)) |
|
|
|
connection.disconnect() |
|
|
|
|
|
class DataAdapter(extends=com.baoyz.swipemenulistview.BaseSwipeListAdapter): |
|
def __init__(self, context): |
|
Log.i("TESTAPP", "INIT DATA ADAPTER " + str(data)) |
|
self.context = context |
|
self.updateData() |
|
|
|
def updateData(self): |
|
task = UpdateDataTask(self) |
|
AsyncTask.execute(task) |
|
|
|
def deleteItem(self, item): |
|
task = DeleteItemTask(self, item) |
|
AsyncTask.execute(task) |
|
|
|
def addItem(self, description): |
|
task = AddItemTask(self, description) |
|
AsyncTask.execute(task) |
|
|
|
def getCount(self) -> int: |
|
# Log.i("TESTAPP", "GET COUNT") |
|
# Log.i("TESTAPP", "DATA SIZE of " + str(self.data)) |
|
# Log.i("TESTAPP", "DATA SIZE " + str(len(self.data))) |
|
return len(self.data) |
|
|
|
def getItem(self, position: int) -> java.lang.String: |
|
# Log.i("TESTAPP", "GET ITEM " + str(position)) |
|
return self.data[position] |
|
|
|
def getItemId(self, position: int) -> long: |
|
# Log.i("TESTAPP", "GET ITEM ID " + str(position)) |
|
return position |
|
|
|
def getView(self, position: int, convertView: android.view.View, parent: android.view.ViewGroup) -> android.view.View: |
|
if convertView is None: |
|
convertView = View.inflate(self.context, android.R.layout.simple_list_item_1, None) |
|
|
|
item = self.getItem(position) |
|
convertView.setText(item.get('description')) |
|
|
|
return convertView |
|
|
|
|
|
class RefreshListener(implements=android.support.v4.widget.SwipeRefreshLayout[OnRefreshListener]): |
|
def __init__(self, layout: android.support.v4.widget.SwipeRefreshLayout, adapter): |
|
self.layout = layout |
|
self.adapter = adapter |
|
|
|
def onRefresh(self) -> void: |
|
Log.i("TESTAPP", "REFRESH!!") |
|
self.adapter.updateData() |
|
self.layout.setRefreshing(False) |
|
|
|
|
|
class ListSwipeMenuCreator(implements=com.baoyz.swipemenulistview.SwipeMenuCreator): |
|
def __init__(self, context): |
|
self.context = context |
|
|
|
def create(self, menu: com.baoyz.swipemenulistview.SwipeMenu) -> void: |
|
# # Create "Open" item |
|
# openItem = SwipeMenuItem(self.context) |
|
# openItem.setBackground(ColorDrawable(Color.rgb(0xC9, 0xC9, 0xCE))) |
|
# openItem.setWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 90, self.context.getResources().getDisplayMetrics())) |
|
# openItem.setTitle("Open") |
|
# openItem.setTitleSize(18) |
|
# openItem.setTitleColor(Color.WHITE) |
|
# menu.addMenuItem(openItem) |
|
|
|
# create "delete" item |
|
deleteItem = SwipeMenuItem(self.context) |
|
deleteItem.setBackground(ColorDrawable(Color.rgb(0xF9, 0x3F, 0x25))) |
|
deleteItem.setWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 90, self.context.getResources().getDisplayMetrics())) |
|
deleteItem.setIcon(android.R.drawable.ic_delete) |
|
menu.addMenuItem(deleteItem) |
|
|
|
|
|
class ListMenuItemClickListener(implements=com.baoyz.swipemenulistview.SwipeMenuListView[OnMenuItemClickListener]): |
|
def __init__(self, listview, adapter): |
|
self.listview = listview |
|
self.adapter = adapter |
|
|
|
def onMenuItemClick(self, position: int, menu: com.baoyz.swipemenulistview.SwipeMenu, index: int) -> bool: |
|
Log.i("TESTAPP", "CLICK MENU ITEM %s, action %s" % (position, index)) |
|
# if index == 0: |
|
# Log.i("TESTAPP", "OPEN item %s" % position) |
|
# self.adapter.addItem('New item') |
|
# elif index == 1: |
|
|
|
Log.i("TESTAPP", "DELETE item %s" % position) |
|
self.adapter.deleteItem(position) |
|
|
|
return True |
|
|
|
|
|
class DialogOKClickListener(implements=android.content.DialogInterface[OnClickListener]): |
|
def __init__(self, input_field, adapter): |
|
self.input_field = input_field |
|
self.adapter = adapter |
|
|
|
def onClick(self, dialog: android.content.DialogInterface, id: int) -> void: |
|
Log.i("TESTAPP", "User input: %s" % self.input_field.getText()) |
|
self.adapter.addItem(self.input_field.getText()) |
|
|
|
|
|
class DialogCancelClickListener(implements=android.content.DialogInterface[OnClickListener]): |
|
def onClick(self, dialog: android.content.DialogInterface, id: int) -> void: |
|
Log.i("TESTAPP", "Cancel dialog") |
|
dialog.cancel() |
|
|
|
|
|
class MainActivity(extends=android.support.v4.app.FragmentActivity): |
|
# /** Called when the activity is first created. */ |
|
|
|
def onCreate(self, savedInstanceState: android.os.Bundle) -> void: |
|
super().onCreate(savedInstanceState) |
|
Log.i("TESTAPP", "CREATE APP") |
|
|
|
adapter = DataAdapter(self) |
|
adapter.data = [] |
|
adapter.context = self |
|
adapter.updateData() |
|
|
|
self.adapter = adapter |
|
|
|
listview = SwipeMenuListView(self) |
|
listview.setSwipeDirection(SwipeMenuListView.DIRECTION_LEFT) |
|
|
|
layout = SwipeRefreshLayout(self) |
|
layout.setLayoutParams( |
|
LinearLayout.LayoutParams( |
|
LinearLayout.LayoutParams.MATCH_PARENT, |
|
LinearLayout.LayoutParams.WRAP_CONTENT |
|
) |
|
) |
|
|
|
listener = RefreshListener(layout, adapter) |
|
layout.setOnRefreshListener(listener) |
|
|
|
listview.setAdapter(adapter) |
|
listview.setMenuCreator(ListSwipeMenuCreator(self)) |
|
listview.setOnMenuItemClickListener(ListMenuItemClickListener(listview, adapter)) |
|
|
|
layout.addView(listview) |
|
layout.setId(1234) |
|
self.layout_id = layout.getId() |
|
|
|
self.setContentView(layout) |
|
|
|
def onCreateOptionsMenu(self, menu: android.view.Menu) -> bool: |
|
Log.i("TESTAPP", "CREATE OPTIONS MENU %s" % menu) |
|
|
|
item = menu.add(0, 42, 0, 'Add Item') |
|
# item.setIcon(android.R.drawable.ic_input_add) |
|
item.setIcon(android.R.drawable.ic_menu_add) |
|
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) |
|
|
|
return True |
|
|
|
def onOptionsItemSelected(self, item: android.view.MenuItem) -> bool: |
|
item_id = item.getItemId() |
|
|
|
if item_id == 42: |
|
alertDialogBuilder = AlertDialog.Builder(self) |
|
|
|
layout = LinearLayout(self) |
|
|
|
layout.setLayoutParams( |
|
LinearLayout.LayoutParams( |
|
LinearLayout.LayoutParams.MATCH_PARENT, |
|
LinearLayout.LayoutParams.MATCH_PARENT |
|
) |
|
) |
|
|
|
layout.setPadding( |
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()), |
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()), |
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()), |
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()) |
|
) |
|
layout.setOrientation(LinearLayout.VERTICAL) |
|
|
|
input_field = EditText(self) |
|
input_field.setLayoutParams( |
|
LinearLayout.LayoutParams( |
|
LinearLayout.LayoutParams.MATCH_PARENT, |
|
LinearLayout.LayoutParams.WRAP_CONTENT |
|
) |
|
) |
|
input_field.setHint("What to do...") |
|
|
|
layout.addView(input_field) |
|
|
|
alertDialogBuilder.setView(layout) |
|
|
|
# set dialog message |
|
alertDialogBuilder.setCancelable(False) |
|
alertDialogBuilder.setPositiveButton("OK", DialogOKClickListener(input_field, self.adapter)) |
|
alertDialogBuilder.setNegativeButton("Cancel", DialogCancelClickListener()) |
|
|
|
# create alert dialog |
|
alertDialog = alertDialogBuilder.create() |
|
|
|
# show it |
|
alertDialog.show() |
|
return True |
This comment has been minimized.
Three platforms (Web, iOS and Android), one app, native on each of them.
The Web app is delivered using Toga-web, a prototype wrapper API that that builds on top of Django's APIs. In the next few weeks, I'm going to provide a similar wrapped API for the Android and iOS versions. For the moment, Android and iOS are implemented using native platform APIs.
The iOS implementation uses Rubicon to bridge between Python and the Objective C runtime. It's running on a simulator because I needed my iPhone to shoot the video - but the app will run on the device as well.
The Android implementation uses VOC to compile Python to a set of Java classfiles. It also uses @baoyongzhang's SwipeListView implementation to get the "Swipe to Delete" behavior. It should run on any Android 3 device or higher.
Video of the apps running is on YouTube