P.S. the big chat bubbles at the bottom of the screen are an image (just like when you send an image via WhatsApp and other messaging apps)
I'm making a simple chatting app. The problem is, whenever I scroll to the top, the view comes back to the very bottom. Here's my OnBindViewHolder code (some parts are omitted because it's too long):
#Override
public void onBindViewHolder(final MessageViewHolder viewHolder, final int i) {
final ChatMessages c = messageList.get(i);
final String from_user = c.getFrom();
String message_type = c.getType();
if (currentUser.equals(from_user)) {
viewHolder.mydisplayName.setText(dummyright);
} else {
viewHolder.displayName.setText(dummyleft);
}
if (currentUser.equals(from_user)) {
leftLayout.setVisibility(View.VISIBLE);
rightLayout.setVisibility(View.GONE);
person = "leftLayout";
} else {
leftLayout.setVisibility(View.GONE);
rightLayout.setVisibility(View.VISIBLE);
person = "rightLayout";
}
if (message_type.equals("text")) {
//set visibility of TextViews and other elements according to which user is sending the message (left/right)
} else if (message_type.equals("image")) {
if (person.equals("leftLayout")) {
viewHolder.mymessageText.setVisibility(View.GONE);
viewHolder.myview_data.setVisibility(View.GONE);
viewHolder.mymessageImage.setVisibility(View.VISIBLE);
Cursor cursor = null;
final String tempUri = c.getDownload_link();
filename = null;
byte[] decodedString = Base64.decode(c.getMessage(), Base64.DEFAULT);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inSampleSize=512;
decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length,options);
try {
Glide.with(viewHolder.mymessageImage.getContext())
.asBitmap()
.load(decodedString)
.thumbnail(0.5f)
.into(viewHolder.mymessageImage);
decodedString=null;
} catch (Exception e) {
Log.e("chat image right", e.getMessage());
}
File file = null;
if (tempUri != null) {
file = new File(tempUri);
String path = file.getAbsolutePath();
if (tempUri.startsWith("content://")) {
try {
cursor = context.getContentResolver().query(Uri.parse(c.getDownload_link()), null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
filename = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
cursor.close();
}
} else if (tempUri.startsWith("file://")) {
filename = file.getName();
}
viewHolder.mymessageImage.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
AlertDialog.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder = new AlertDialog.Builder(context, android.R.style.Theme_Material_Light_Dialog_Alert);
} else {
builder = new AlertDialog.Builder(context);
}
builder.setTitle("Save Image")
.setMessage("Do you want to save this image?")
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// continue with delete
startSaveImageToGallery();
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// do nothing
}
})
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
}
});
} else {
}
} else if (person.equals("rightLayout")) {
//same as above but for the right layout
}
}
}
I've read a lot of questions about this issue and some of the answers said that it's caused by setting visibility inside onBindViewHolder. I don't know where else I would set them because my layouts' visibility are dependent on some data (in this case, who's sending the chats).
Edit: I've just tried my app several times and apparently it only jumps to the bottom when there are pictures being sent, especially when they're 600KB or more in size. I'm loading my images with Glide, they are stored in Base64 and converted to compressed Bitmaps. Do these images cause the messed up scrolling?
1.If the ImageView is wrap_content,set a fixed size value for it.
2.If the OnBindViewHolder method has other asyn operations which are relatived to the UI,deal with them before setData.
3.Check your code,if there are methods such as scroll to
Related
I have a folder with about 10 images which I like to OCR extract text.
That works excellent for 1 picture, but my java skills are not good enough to implement that for multiple images.
I'm really appreciate if someone could show me a clean solution for that.
Thanks a lot
br Lukas
TextView output1;
ArrayList<Bitmap> bitmapArray = new ArrayList<Bitmap>();
TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);
private void OCR_list()
{
String path = Environment.getExternalStorageDirectory().toString()+"/folder_with_images";
File directory = new File(path);
File[] files = directory.listFiles();
for (int i = 0; i < files.length; i++) {
output1.setText(output1.getText() + ", " + files[i].getName());
File imgFile = files[i];
if (imgFile.exists()) {
bitmapArray.add(BitmapFactory.decodeFile(imgFile.getAbsolutePath()));
} else {
output1.setText(output1.getText()+"\n Bitmap not found!");
return;
}
}
InputImage image = InputImage.fromBitmap(bitmapArray.get(0), 0);
recognizer.process(image)
.addOnSuccessListener(
new OnSuccessListener<Text>() {
#Override
public void onSuccess(Text texts) {
processTextRecognitionResult(texts);
}
})
.addOnFailureListener(
new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
e.printStackTrace();
}
});
Edit:
I solved it now this way, but looks awful:
private void new_Recognition(InputImage image) {
recognizer.process(image)
.addOnSuccessListener(
new OnSuccessListener<Text>() {
#Override
public void onSuccess(Text texts) {
processTextRecognitionResult(texts);
bitmapArray.remove(0);
if (!bitmapArray.isEmpty()) {
InputImage image = InputImage.fromBitmap(bitmapArray.get(0), 0);
new_Recognition(image);
}
}
})
.addOnFailureListener(
new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
e.printStackTrace();
}
});
}
You can iterate on inputs directly, and recognition tasks will be queued up and then processed in order internally.
for (Bitmap input : inputs) {
recognizer.process(input)
.addOnSuccessListener(text -> ...)
}
in the recycle view, I'm loading data by this query it's loading fine. But after scrolling down it's getting scattered.
Query query = db.collection(USER_MASTER_KEY)
.document(Uid)
.collection("following")
.document(xpertId)
.collection("chat_transcript")
.orderBy(TIMESTAMP_KEY);
here is my onBind method.here i'm loading image, text , video at the same time.I build this recycler in a chatting bot app.where response of the data is previously loaded in the firebase. According to the qustion the ans will be loaded.everything loading currectly but after two three video loading problem occuring.
#Override
public void onBindViewHolder(#NonNull final ChatViewHolder holder, int position) {
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder()
.permitAll().build();
StrictMode.setThreadPolicy(policy);
final ChatViewData chatViewData1 = chatViewDataList.get(position);
klog.d("## CHAT ITEM-", new Gson().toJson(chatViewData1));
String chatData = chatViewData1.getMsgContent();
// If the message is still pending show placeholder
if (chatViewData1.MSG_TYPE_PACEHOLDER.equalsIgnoreCase(chatViewData1.getMsgType())) {
holder.gifImageViewLoading.setVisibility(VISIBLE);
holder.leftMsgTextView.setVisibility(GONE);
holder.rightMsgTextView.setVisibility(GONE);
holder.leftImageView.setVisibility(GONE);
holder.relativeLayout.setVisibility(GONE);
}
// If the message type is blank, show nothing
if (chatViewData1.getMsgType().equalsIgnoreCase("")) {
holder.gifImageViewLoading.setVisibility(GONE);
holder.leftMsgTextView.setVisibility(GONE);
holder.rightMsgTextView.setVisibility(GONE);
holder.leftImageView.setVisibility(GONE);
holder.relativeLayout.setVisibility(GONE);
}
// If the message is a received message.
if (chatViewData1.MSG_TYPE_RECEIVED.equals(chatViewData1.getMsgType())) {
holder.leftMsgTextView.setText(HtmlCompat.fromHtml(chatData, HtmlCompat.FROM_HTML_MODE_COMPACT), TextView.BufferType.SPANNABLE);
holder.gifImageViewLoading.setVisibility(GONE);
holder.leftMsgTextView.setVisibility(VISIBLE);
//holder.chatLikeOption.setVisibility(VISIBLE);
//holder.chatDislikeOption.setVisibility(VISIBLE);
holder.rightMsgTextView.setVisibility(GONE);
holder.leftImageView.setVisibility(GONE);
holder.relativeLayout.setVisibility(GONE);
}
// If the message is a sent message.
else if (chatViewData1.MSG_TYPE_SENT.equals(chatViewData1.getMsgType())) {
holder.rightMsgTextView.setText(HtmlCompat.fromHtml(chatData, HtmlCompat.FROM_HTML_MODE_COMPACT), TextView.BufferType.SPANNABLE);
holder.gifImageViewLoading.setVisibility(GONE);
holder.rightMsgTextView.setVisibility(VISIBLE);
holder.leftMsgTextView.setVisibility(GONE);
holder.leftImageView.setVisibility(GONE);
holder.relativeLayout.setVisibility(GONE);
}
// If the message is an image message.
else if (chatViewData1.MSG_TYPE_IMAGE.equals(chatViewData1.getMsgType())) {
//Load Image using Picasso library
Picasso.get().load(chatViewData1.getMsgContent()).into(holder.leftImageView);
holder.gifImageViewLoading.setVisibility(GONE);
holder.leftImageView.setVisibility(VISIBLE);
holder.rightMsgTextView.setVisibility(GONE);
holder.leftMsgTextView.setVisibility(GONE);
holder.relativeLayout.setVisibility(GONE);
}
// If the message is a video message.
else if (chatViewData1.MSG_TYPE_VIDEO.equals(chatViewData1.getMsgType())) {
final String videoId = chatViewData1.getMsgContent();
final ImageView yimageView = holder.YoutubeImageView;
final String url = "https://img.youtube.com/vi/" + videoId + "/default.jpg";
BitmapFactory.Options bmOptions;
bmOptions = new BitmapFactory.Options();
bmOptions.inSampleSize = 1;
Bitmap bm = loadBitmap(url, bmOptions);
yimageView.setVisibility(VISIBLE);
//holder.videoLikeOption.setVisibility(VISIBLE);
//holder.videoDislikeOption.setVisibility(VISIBLE);
yimageView.setImageBitmap(bm);
yimageView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent("custom-message");
// intent.putExtra("quantity",Integer.parseInt(quantity.getText().toString()));
intent.putExtra("startTime", chatViewData1.getStartSeconds());
intent.putExtra("endTime", chatViewData1.getEndSeconds());
intent.putExtra("videoId", videoId);
LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
}
});
}
}
Use this two method on adpter of recycleview
#Override
public long getItemId(int position) {
return position;
}
#Override
public int getItemViewType(int position) {
return position;
}
The app I am making need to take pictures to send them to a server.
I need to take 6 photos at least. I have a recyclerView in which I am displaying a preview of my photo. It's working perfectly (I am using Picasso as photo library).
I need to be able to delete photos before sending them away (and consequently their preview). With a click on the preview, I remove it from my photo tab and update my recyclerview with notifyDataSetChanged().
The photo disappears.
When I take an other picture, I have its preview but the preview from the deleted picture is coming back.
If I delete three pictures and take a new one, I have the preview of the new one and the preview of the 3 deleted pictures.
Here's part of my adapter where I bind my view
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
mImageView = holder.mPhotoIV;
File mFile = new File(String.valueOf(Uri.parse(mListOfPhotos.get(position))));
int width = mImageView.getLayoutParams().width;
int height = mImageView.getLayoutParams().height;
Picasso.get()
.load(mFile)
.resize(200, 200)
.error(R.drawable.ic_no_photo_foreground)
.into(mImageView, new com.squareup.picasso.Callback() {
#Override
public void onSuccess() {
}
#Override
public void onError(Exception e) {
}
});
}
Here part of my activity where I'm calling my adapter
mTakePhotoFAB.setOnClickListener(view -> {
mDir = new File(getExternalCacheDir(), "PhotosAuthentifier");
boolean success = true;
if (!mDir.exists()) {
success = mDir.mkdir();
}
if (success) {
File mFile = new File(mDir, new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()).format(new Date()) + ".jpg");
mImageCapture.takePicture(mFile,
new ImageCapture.OnImageSavedListener() {
#Override
public void onImageSaved(#NonNull File file) {
mListOfPhotos.add(file.getAbsolutePath());
mNumberOfPhotoTV.setText(getResources().getString(R.string.minPhotos, mListOfPhotos.size()));
if (mListOfPhotos.size() >= 6) {
mSendPhotoFAB.setVisibility(View.VISIBLE);
}
mAdapter = new CameraPhotoAdapter(mListOfPhotos, getBaseContext());
mRecyclerView.setAdapter(mAdapter);
}
#Override
public void onError(#NonNull ImageCapture.ImageCaptureError imageCaptureError, #NonNull String message, #Nullable Throwable cause) {
String mMessage = "Photo capture failed: " + message;
Toast.makeText(CameraActivity.this, mMessage, Toast.LENGTH_SHORT).show();
assert cause != null;
cause.printStackTrace();
}
});
}
});
Function (in my adapter) to remove picture
protected void removePhoto(Context context, ArrayList<String> array, int position) {
File mDir = new File(context.getExternalCacheDir(), "PhotosAuthentifier");
File mFile = new File(array.get(position));
if (mDir.exists()) {
File[] mFilesIntoDir = mDir.listFiles();
if (mFilesIntoDir == null) {
return;
} else {
for (int i = 0; i < mFilesIntoDir.length; i++) {
if (mFilesIntoDir[i].getAbsolutePath().equals(mFile.getAbsolutePath())) {
boolean mSuccess = mFilesIntoDir[i].delete();
if (mSuccess) {
Picasso.get().invalidate(mFile.getAbsolutePath());
mListOfPhotos.remove(mFile.getAbsolutePath());
notifyDataSetChanged();
}
}
}
}
}
}
I tried to invalidate Picasso cache but when I take a new picture, instead of the deleted picture reappearing I have the default behavior when I don't have a good url to upload (a black cross)
Could anyone help please :)?
I dinamically add some buttons to my TableL like this
TableLayout table = (TableLayout) findViewById( R.id.tableLayout1 );
Bitmap bmp = BitmapFactory.decodeFile(Environment.getExternalStorageDirectory()+ myPrefLocalMedia + mat_foto);
final ImageButton b = new ImageButton(this);
b.setScaleType(ImageView.ScaleType.FIT_CENTER);
b.setAdjustViewBounds(true);
b.setImageBitmap(bmp);
b.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
ProgressDialog progressDialog = new ProgressDialog(SuperlineaActivity.this);
progressDialog.setMessage("Realizando el pedido...");
progressDialog.setCancelable(false);
b.setPressed(true);
if ( (miPedido == null) || ( miPedido.isEnproceso()==false) ) {
Log.e("log_","Hago post! Creo pedido!!");
Hacerpost eginpost = new Hacerpost(SuperlineaActivity.this, progressDialog);
eginpost.execute(myPrefApiPeticionMaterial,mat_id, PUESTOID, LINEAID, USUARIOID, PRODUCTOID);
} else {
Hacerput puttask = new Hacerput(SuperlineaActivity.this, progressDialog);
puttask.execute(myPrefApiPeticionMaterial,miPedido.getId());
}
}
});
row.addView( b, 300,300 );
When it is pressed it performs some AsyncTask (HttpPost) and it returns one color code. I need to change the pressed background color with that color code but how do I know witch button I clicked?
This is the function where I receive the HttpPost data:
#TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public void erantzuna(JSONObject myJson) {
if ( miPedido == null ) {
AlertDialog alerta = new AlertDialog.Builder(SuperlineaActivity.this).create();
alerta.setTitle("PEDIDO EN PROCESO");
alerta.setMessage("El pedido se a procesado. Espera.");
alerta.setButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(getApplicationContext(), "Registrado", Toast.LENGTH_SHORT).show();
}
});
alerta.show();
String kolorea = null;
String pedidoid = null;
try {
kolorea = myJson.getString("kolorea");
pedidoid = myJson.getString("pedidoid");
} catch (JSONException e) {
e.printStackTrace();
}
Log.e("log_", "Bueltan da: => " + kolorea);
Log.e("log_", "Bueltan da: => " + pedidoid);
miPedido = new Pedido(pedidoid, kolorea);
try {
if (miPedido.getKolorea().equals("kuadro-gorria")) {
//
// Here I need to change the pressed ImageButton color to RED
//
} elseif ( miPedido.getKolorea().equals("kuadro-urdina")) {
//
// Here I need to change the pressed ImageButton color to BLUE
//
} else {
//
// Here I need to change the pressed ImageButton color to transparent
//
}
} catch (Exception e) {
Log.e("log_","Boton Color => " + e.toString());
}
} else {
miPedido = null;
}
}
Any help or clue?
Thanks in advance
Maybe you should manage your dynamically added buttons, like put them into a list, or, if you won't need a reference for them later, just give the Button to the asyncTask (like in the constructor or something) and change its color in the onPostExecute method of the asynctask. But don't forget to prepare for cases like the Activity gets destroyed while the task waits for the response, as in that case the Button will no longer exist, and this could cause memory leaks as well.
Hi i did one application here i need to share my score on twiter,i did using below code my score is posing fine,but now i need to share score along with app icon.but i dont know how to share that image along with text can any one help me,thankyou
TestPost.class:
public class TestPost extends Activity {
String review;
private TwitterApp mTwitter;
private String username = "";
private boolean postToTwitter = false;
private static final String twitter_consumer_key = "";
private static final String twitter_secret_key = "";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.post);
Button postBtn = (Button) findViewById(R.id.button1);
final EditText reviewEdit = (EditText) findViewById(R.id.revieew);
postBtn.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
review = reviewEdit.getText().toString();
postToTwitter = true;
onTwitterClick();
}
});
mTwitter = new TwitterApp(this, twitter_consumer_key,twitter_secret_key);
mTwitter.setListener(mTwLoginDialogListener);
if (mTwitter.hasAccessToken()) {
username = mTwitter.getUsername();
username = (username.equals("")) ? "No Name" : username;
}
}
private void postToTwitter(final String review) {
new Thread() {
#Override
public void run() {
int what = 0;
try {
mTwitter.updateStatus(review);
} catch (Exception e) {
what = 1;
}
mHandler.sendMessage(mHandler.obtainMessage(what));
}
}.start();
}
private Handler mHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
String text = (msg.what == 0) ? "Posted to Twitter" : "Post to Twitter failed";
Toast.makeText(TestPost.this, text, Toast.LENGTH_SHORT).show();
}
};
private final TwDialogListener mTwLoginDialogListener = new TwDialogListener() {
#Override
public void onComplete(String value) {
username = mTwitter.getUsername();
username = (username.equals("")) ? "No Name" : username;
postToTwitter = true;
postToTwitter(review);
Toast.makeText(TestPost.this, "Connected to Twitter as " + username, Toast.LENGTH_LONG).show();
}
#Override
public void onError(String value) {
Toast.makeText(TestPost.this, "Twitter connection failed", Toast.LENGTH_LONG).show();
}
};
private void onTwitterClick() {
if (mTwitter.hasAccessToken()) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("Delete current Twitter connection?")
.setCancelable(false)
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
mTwitter.resetAccessToken();
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
final AlertDialog alert = builder.create();
alert.show();
} else {
mTwitter.authorize();
}
}
}
This is how I upload an Image along with text to the logged in Twitter User's account. I make use of the TwitPic API for that. You will have to login and register a developer account there.
The solution can be thought of as a two step solution.
First, when the user clicks the post button, I first grab the Image and upload to TwitPic. From there, I grab the URL that is returned by TwitPic (String url = upload.upload(finalFile);).
In step two of this code, in a String instance (String finalStatusWithURL), I grab the content of an EditText and then append the URL from step 1 to it. With this done, the post is finally posted to Twitter.
Configuration conf = new ConfigurationBuilder()
.setOAuthConsumerKey(TWITTER_CONSUMER_KEY)
.setOAuthConsumerSecret(TWITTER_CONSUMER_SECRET)
.setOAuthAccessToken(twit_access_token)
.setOAuthAccessTokenSecret(twit_access_token_secret)
.setMediaProviderAPIKey(TWIT_PIC_API)
.build();
// SET THE FILE PATH
File finalFile = new File(getRealPathFromURI(initialURI));
// THIS IS IMPORTANT. TWITPIC NEEDS THE ACTUAL PATH ON THE DEVICE. JUST THE URI DOES NOT WORK!!!!
ImageUpload upload = new ImageUploadFactory(conf).getInstance(MediaProvider.TWITPIC);
String url = upload.upload(finalFile);
Log.e("TWITTER URL RESPONSE", url);
// NOW, UPLOAD TO TWITTER
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.setOAuthConsumerKey(YOUR_TWITTER_CONSUMER_KEY);
builder.setOAuthConsumerSecret(YOUR_TWITTER_CONSUMER_SECRET);
AccessToken accessToken = new AccessToken(your_twit_access_token, your_twit_access_token_secret);
Twitter twitter = new TwitterFactory(builder.build()).getInstance(accessToken);
String finalStatusWithURL = null;
if (finalStatusMessage.trim().length() > 0) {
finalStatusMessage = editStatusUpdate.getText().toString();
finalStatusWithURL = finalStatusMessage + ":\n " + url;
} else {
finalStatusWithURL = url;
}
twitter4j.Status response = twitter.updateStatus(finalStatusWithURL);
Log.e("TWITTER RESPONSE", response.getText());
This is a method to get the real path of the image you want to upload:
// HELPER METHOD TO GET REAL PATH FOR THE SELECTED IMAGE
public String getRealPathFromURI(Uri uri) {
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
cursor.moveToFirst();
int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
return cursor.getString(idx);
}
Hope this helps. This is production code from an app of mine.
UPDATED:
Change this in the code:
File finalFile = new File(getRealPathFromURI(initialURI));
To this:
Uri tempUri = getImageUri(getApplicationContext(), icon);
File finalFile = new File(getRealPathFromURI(tempUri));
And the helper method for Uri tempUri = getImageUri(getApplicationContext(), bmpFinal);:
public Uri getImageUri(Context inContext, Bitmap inImage) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes);
String path = Images.Media.insertImage(inContext.getContentResolver(), inImage, "Title", null);
return Uri.parse(path);
}
You will still need to pass a Bitmap object. To convert your app icon to a Bitmap, use this:
Bitmap icon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher);
UPDATED 2:
A little while after you asking and me answering your question, Twitter wrapped up a developer meet in SF. They have introduced a few new Cards which I think may be of use to your specific use-case. It is fresh off the block and I haven't gotten around to testing/using it. But if it fits your purpose, take a look at these links:
http://www.engadget.com/2013/04/02/twitter-cards-apps-products-photo-galleries/ (Where I found out about the new feature)
https://dev.twitter.com/blog/mobile-app-deep-linking-and-new-cards (The official Twitter blog)
https://dev.twitter.com/cards (Developer page for details of the features and how to use them)